com.google.nigori.client.HashMigoriDatastore.java Source code

Java tutorial

Introduction

Here is the source code for com.google.nigori.client.HashMigoriDatastore.java

Source

/*
 * Copyright (C) 2012 Daniel Thomas (drt24)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.nigori.client;

import static com.google.nigori.client.HashDAG.HASH_SIZE;

import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.apache.commons.codec.binary.Hex;

import com.google.nigori.common.Index;
import com.google.nigori.common.NigoriConstants;
import com.google.nigori.common.NigoriCryptographyException;
import com.google.nigori.common.RevValue;
import com.google.nigori.common.Revision;
import com.google.nigori.common.UnauthorisedException;

/**
 * The canonical implementation of {@link MigoriDatastore} providing versioning using SHA-1 hashes
 * on the values and parent revisions.
 * 
 * @author drt24
 * 
 */
public class HashMigoriDatastore implements MigoriDatastore {

    private final NigoriDatastore store;

    public HashMigoriDatastore(NigoriDatastore store) {
        this.store = store;
    }

    @Override
    public boolean register() throws IOException, NigoriCryptographyException {
        return store.register();
    }

    @Override
    public boolean unregister() throws IOException, NigoriCryptographyException, UnauthorisedException {
        return store.unregister();
    }

    @Override
    public boolean authenticate() throws IOException, NigoriCryptographyException {
        return store.authenticate();
    }

    @Override
    public List<Index> getIndices() throws NigoriCryptographyException, IOException, UnauthorisedException {
        return store.getIndices();
    }

    @Override
    public RevValue removeValue(Index index, RevValue... parents) {
        // TODO(drt24) implement removeValue
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public RevValue getMerging(Index index, MigoriMerger merger)
            throws NigoriCryptographyException, IOException, UnauthorisedException {
        List<RevValue> heads = get(index);
        if (heads == null || heads.size() == 0) {
            return null;
        }
        for (RevValue head : heads) {
            validateHash(head.getValue(), head.getRevision());
        }
        if (heads.size() == 1) {
            for (RevValue head : heads) {
                return head;
            }
            throw new IllegalStateException("Can never happen as must be one head to return");
        } else {
            assert heads.size() > 1 : "Must be more than one head before we merge and we tested for 0 and 1, was: "
                    + heads.size();
            return merger.merge(this, index, heads);
        }
    }

    @Override
    public List<RevValue> get(Index index) throws NigoriCryptographyException, IOException, UnauthorisedException {
        DAG<Revision> history = getHistory(index);
        if (history == null) {
            return null;
        }
        Collection<Node<Revision>> heads = history.getHeads();
        List<RevValue> answer = new ArrayList<RevValue>();
        for (Node<Revision> rev : heads) {
            Revision revision = rev.getValue();
            byte[] value = getRevision(index, revision);
            // TODO(drt24) value might be null
            if (value != null) {
                answer.add(new RevValue(revision, value));
            }
        }
        return answer;
    }

    @Override
    public RevValue put(Index index, byte[] value, RevValue... parents)
            throws IOException, NigoriCryptographyException, UnauthorisedException {
        byte[] revBytes = generateHash(value, toIDByte(parents));
        Revision rev = new Revision(revBytes);
        RevValue rv = new RevValue(rev, value);
        boolean success = store.put(index, rev, value);
        if (!success) {
            throw new IOException("Could not put into the store");
        }
        return rv;

    }

    private byte[] toIDByte(RevValue... parents) {
        Arrays.sort(parents);
        byte[] idBytes = new byte[parents.length * HASH_SIZE];
        int insertPoint = 0;
        for (RevValue rev : parents) {
            System.arraycopy(rev.getRevision().getBytes(), 0, idBytes, insertPoint, HASH_SIZE);
            insertPoint += HASH_SIZE;
        }
        return idBytes;
    }

    private byte[] generateHash(byte[] value, byte[] idBytes) throws NigoriCryptographyException {
        byte[] toHash = new byte[value.length + idBytes.length];
        System.arraycopy(value, 0, toHash, 0, value.length);
        System.arraycopy(idBytes, 0, toHash, value.length, idBytes.length);

        MessageDigest crypt;
        try {
            crypt = MessageDigest.getInstance(NigoriConstants.A_REVHASH);

            crypt.reset();
            crypt.update(toHash);
            byte[] hashBytes = crypt.digest();
            byte[] revBytes = new byte[HASH_SIZE + idBytes.length];
            System.arraycopy(hashBytes, 0, revBytes, 0, HASH_SIZE);
            System.arraycopy(idBytes, 0, revBytes, HASH_SIZE, idBytes.length);
            return revBytes;
        } catch (NoSuchAlgorithmException e) {
            throw new NigoriCryptographyException(e);
        }
    }

    @Override
    public boolean removeIndex(Index index, Revision position)
            throws NigoriCryptographyException, IOException, UnauthorisedException {
        return store.delete(index, position.getBytes());
    }

    @Override
    public DAG<Revision> getHistory(Index index)
            throws NigoriCryptographyException, IOException, UnauthorisedException {
        List<Revision> revisions = store.getRevisions(index);
        if (revisions == null) {
            return null;
        }
        return new HashDAG(revisions);
    }

    /**
     * Verify that the value provided gives the correct hash when combined with the Revision
     * 
     * @param value
     * @param revision
     * @return
     * @throws NigoriCryptographyException
     * @throws InvalidHashException
     */
    private byte[] validateHash(byte[] value, Revision revision)
            throws NigoriCryptographyException, InvalidHashException {
        byte[] revBytes = revision.getBytes();
        byte[] hash = generateHash(value, Arrays.copyOfRange(revBytes, HASH_SIZE, revBytes.length));
        if (Arrays.equals(hash, revBytes)) {
            return value;
        } else {
            throw new InvalidHashException(revBytes, hash);
        }
    }

    @Override
    public byte[] getRevision(Index index, Revision revision)
            throws IOException, NigoriCryptographyException, UnauthorisedException {
        return validateHash(store.getRevision(index, revision), revision);
    }

    /**
     * Thrown if a hash is found to be invalid.
     * 
     * This either means that there has been some corruption between decryption and use (unlikely) or
     * that there is a bug, or that someone has used some other incompatible implementation of
     * MigoriDatastore or directly used NigoriDatstore or that the server is being malicious.
     * 
     * @author drt24
     * 
     */
    public static class InvalidHashException extends IOException {

        private static final long serialVersionUID = 1L;

        public InvalidHashException(byte[] expectedHash, byte[] gotHash) {
            super("Expected: " + new String(Hex.encodeHex(expectedHash)) + " and got: "
                    + new String(Hex.encodeHex(gotHash)));
        }

    }
}