/*
* 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)));
}
}
}