/*
* Copyright (C) 2011 Google Inc. Copyright (C) 2011 Alastair R. Beresford
*
* 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.common.MessageLibrary.toBytes;
import static com.google.nigori.common.MessageLibrary.bytesToString;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import com.google.nigori.common.Index;
import com.google.nigori.common.MessageLibrary;
import com.google.nigori.common.NigoriCryptographyException;
import com.google.nigori.common.NigoriMessages.GetIndicesResponse;
import com.google.nigori.common.NigoriMessages.GetResponse;
import com.google.nigori.common.NigoriMessages.GetRevisionsResponse;
import com.google.nigori.common.NigoriMessages.RevisionValue;
import com.google.nigori.common.NigoriProtocol;
import com.google.nigori.common.NotFoundException;
import com.google.nigori.common.RevValue;
import com.google.nigori.common.Revision;
import com.google.nigori.common.UnauthorisedException;
import com.google.protobuf.ByteString;
/**
* A client API capable of managing a session with a Nigori Server.
*
* @author Alastair Beresford
*
* It is worth looking at {@link java.util.Collection} but we can't implement that or most
* of the methods in it until we have a "list indexes" method which at least for now we
* don't intend to do. putAll from {@link java.util.Map} might be worth implementing
*/
public class CryptoNigoriDatastore implements NigoriDatastore {
private final KeyManager keyManager;
private final NigoriProtocol protocol;
public CryptoNigoriDatastore(NigoriProtocol protocol, String username, String password,
String serverName) throws UnsupportedEncodingException, NigoriCryptographyException {
this.protocol = protocol;
this.keyManager = new RealKeyManager(serverName, toBytes(username), toBytes(password));
}
/**
* Represents communication with a Nigori datastore for a specific user.
*
* @param server DNS name or IP Address of the server.
* @param port Port number the service is running on.
* @param serverPrefix URI path on the server for the Nigori service.
* @param username name of account used to communicate with the Nigori service.
* @param password password of the account used to communicate with the Nigori service.
* @throws UnsupportedEncodingException if UTF-8 is unavailable on this platform.
* @throws NigoriCryptographyException if appropriate cryptography libraries are unavailable.
*/
public CryptoNigoriDatastore(String server, int port, String serverPrefix, String username,
String password) throws NigoriCryptographyException, UnsupportedEncodingException {
String servername = server + ":" + port;
protocol = new JsonHTTPProtocol(server, port, serverPrefix);
keyManager = new RealKeyManager(servername, toBytes(username), toBytes(password));
}
/**
* Represents communication with a Nigori datastore for a newly created user.
*
* The username and password for the new user are generated automatically, and can be retrieved by
* calling getUsername and getPassword on this object.
*
* @param server DNS name or IP Address of the server.
* @param port Port number the service is running on.
* @param serverPrefix URI path on the server for the Nigori service.
* @throws UnsupportedEncodingException if MessageLibrary.CHARSET is unavailable on this platform.
* @throws NigoriCryptographyException if appropriate cryptography libraries are unavailable.
*/
public CryptoNigoriDatastore(String server, int port, String serverPrefix)
throws NigoriCryptographyException, UnsupportedEncodingException {
String servername = server + ":" + port;
protocol = new JsonHTTPProtocol(server, port, serverPrefix);
keyManager = new RealKeyManager(servername);
}
/**
* Retrieve the username used to connect with the Nigori datastore.
*
* @return the username.
*/
public String getUsername() {
return bytesToString(keyManager.getUsername());
}
/**
* Retrieve the password used to connect with the Nigori datastore.
*
* @return the password.
*/
public String getPassword() {
return bytesToString(keyManager.getPassword());
}
/**
* Retrieve the public key associated with the username and password.
*
* @return the public key.
*/
public byte[] getPublicKey() throws NigoriCryptographyException {
return keyManager.signer().getPublicKey();
}
@Override
public boolean authenticate() throws IOException, NigoriCryptographyException {
return protocol.authenticate(MessageLibrary.authenticateRequestAsProtobuf(
keyManager.getServerName(), keyManager.signer()));
}
@Override
public boolean register() throws IOException, NigoriCryptographyException {
byte[] token = {};
return protocol.register(MessageLibrary.registerRequestAsProtobuf(keyManager.signer(), token));
}
@Override
public boolean unregister() throws IOException, NigoriCryptographyException,
UnauthorisedException {
return protocol.unregister(MessageLibrary.unregisterRequestAsProtobuf(
keyManager.getServerName(), keyManager.signer()));
}
/**
* @return WARNING: there is no assurance that the value for the revision is a pair once specified
* by a valid client - the server can pair any value with any revision.
*/
@Override
public List<RevValue> get(Index index) throws IOException, NigoriCryptographyException,
UnauthorisedException {
return get(null, index, null);
}
/**
* @param index
* @param revision
* @return WARNING: there is no assurance that the value for the revision is a pair once specified
* by a valid client - the server can pair any value with any revision.
* @throws NigoriCryptographyException
* @throws IOException
* @throws UnauthorisedException
*/
@Override
public byte[] getRevision(Index index, Revision revision) throws IOException,
NigoriCryptographyException, UnauthorisedException {
List<RevValue> rev = get(null, index, revision);
if (rev != null && rev.size() == 1) {
return rev.get(0).getValue();
} else {
assert rev == null || rev.size() == 0 : "Rev size: " + rev.size();
return null;
}
}
/**
* Retrieve the value associated with {@code index} on the server.
*
* @param index
* @param revision
* @return a list of RevValues containing the data associated with {@code index} or {@code null}
* if no data exists. WARNING: there is no assurance that the value for the revision is a
* pair once specified by a valid client - the server can pair any value with any
* revision.
*/
private List<RevValue> get(byte[] encKey, Index index, Revision revision) throws IOException,
NigoriCryptographyException, UnauthorisedException {
byte[] encIndex;
byte[] encRevision = null;
if (encKey == null) {
encIndex = keyManager.encryptDeterministically(index.getBytes());
if (revision != null) {
encRevision = keyManager.encryptDeterministically(revision.getBytes());
}
} else {
encIndex = keyManager.encryptDeterministically(encKey, index.getBytes());
if (revision != null) {
encRevision = keyManager.encryptDeterministically(encKey, revision.getBytes());
}
}
try {
GetResponse getResponse =
protocol.get(MessageLibrary.getRequestAsProtobuf(keyManager.getServerName(),
keyManager.signer(), encIndex, encRevision));
if (getResponse == null) {
return null;
}
List<RevisionValue> revisions = getResponse.getRevisionsList();
List<RevValue> answer = new ArrayList<RevValue>(revisions.size());
for (RevisionValue revisionValue : revisions) {
byte[] revisionciphertext = revisionValue.getRevision().toByteArray();
byte[] valueciphertext = revisionValue.getValue().toByteArray();
if (encKey == null) {
answer.add(new RevValue(keyManager.decrypt(revisionciphertext), keyManager
.decrypt(valueciphertext)));
} else {
answer.add(new RevValue(keyManager.decrypt(encKey, revisionciphertext), keyManager
.decrypt(encKey, valueciphertext)));
}
}
return answer;
} catch (NotFoundException e) {
return null;
}
}
@Override
public List<Index> getIndices() throws NigoriCryptographyException, IOException,
UnauthorisedException {
try {
GetIndicesResponse getResponse =
protocol.getIndices(MessageLibrary.getIndicesRequestAsProtobuf(
keyManager.getServerName(), keyManager.signer()));
if (getResponse == null) {
return null;
}
List<ByteString> indices = getResponse.getIndicesList();
List<Index> answer = new ArrayList<Index>(indices.size());
for (ByteString index : indices) {
answer.add(new Index(keyManager.decrypt(index.toByteArray())));
}
return answer;
} catch (NotFoundException e) {
return null;
}
}
@Override
public List<Revision> getRevisions(Index index) throws NigoriCryptographyException,
UnsupportedEncodingException, IOException, UnauthorisedException {
byte[] encIndex = keyManager.encryptDeterministically(index.getBytes());
try {
GetRevisionsResponse getResponse =
protocol.getRevisions(MessageLibrary.getRevisionsRequestAsProtobuf(
keyManager.getServerName(), keyManager.signer(), encIndex));
if (getResponse == null) {
return null;
}
List<ByteString> revisions = getResponse.getRevisionsList();
List<Revision> answer = new ArrayList<Revision>(revisions.size());
for (ByteString revision : revisions) {
answer.add(new Revision(keyManager.decrypt(revision.toByteArray())));
}
return answer;
} catch (NotFoundException e) {
return null;
}
}
@Override
public boolean put(Index index, Revision revision, byte[] value) throws IOException,
NigoriCryptographyException, UnauthorisedException {
if (value == null) {
throw new IllegalArgumentException("Null values not yet supported");
}
return put(null, index, revision, value);
}
/**
* Insert a new key-value pair into the datastore of the server.
*
* @param key the key
* @param value the data value associated with the key.
* @param readAuthorities list of public keys of people permitted to read this key-value pair.
* @param writeAuthorities list of public keys of people permitted to read this key-value pair.
* @return true if the data was successfully inserted; false otherwise.
* @throws UnauthorisedException
*/
private boolean put(byte[] encKey, Index index, Revision revision, byte[] value)
throws IOException, NigoriCryptographyException, UnauthorisedException {
byte[] encIndex;
byte[] encRevision;
byte[] encValue;
if (encKey == null) {
encIndex = keyManager.encryptDeterministically(index.getBytes());
encRevision = keyManager.encryptDeterministically(revision.getBytes());
encValue = keyManager.encrypt(value);
} else {
encIndex = keyManager.encryptDeterministically(encKey, index.getBytes());
encRevision = keyManager.encryptDeterministically(encKey, revision.getBytes());
encValue = keyManager.encrypt(encKey, value);
}
return protocol.put(MessageLibrary.putRequestAsProtobuf(keyManager.getServerName(),
keyManager.signer(), encIndex, encRevision, encValue));
}
@Override
public boolean delete(Index index, byte[] token) throws UnsupportedEncodingException,
NigoriCryptographyException, IOException, UnauthorisedException {
return delete(null, index, token);
}
private boolean delete(byte[] encKey, Index index, byte[] token)
throws NigoriCryptographyException, UnsupportedEncodingException, IOException,
UnauthorisedException {
byte[] encIndex;
if (encKey == null) {
encIndex = keyManager.encryptDeterministically(index.getBytes());
} else {
encIndex = keyManager.encryptDeterministically(encKey, index.getBytes());
}
try {
return protocol.delete(MessageLibrary.deleteRequestAsProtobuf(keyManager.getServerName(),
keyManager.signer(), encIndex));
} catch (NotFoundException e) {
return false;
}
}
}