/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.keys;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import freenet.support.Base64;
import freenet.support.ByteArrayWrapper;
import freenet.support.Fields;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
/**
* Client level CHK. Can be converted into a FreenetURI, can be used to decrypt
* a CHKBlock, can be produced by a CHKBlock.
*/
public class ClientCHK extends ClientKey implements Serializable {
private static final long serialVersionUID = 1L;
/** Lazily constructed: the NodeCHK */
transient NodeCHK nodeKey;
/** Routing key */
final byte[] routingKey;
/** Decryption key */
final byte[] cryptoKey;
/** Is the data a control document? */
final boolean controlDocument;
/** Encryption algorithm */
final byte cryptoAlgorithm;
/** Compression algorithm, negative means uncompressed */
final short compressionAlgorithm;
final int hashCode;
/* We use EXTRA_LENGTH above for consistency, rather than dis.read etc. Some code depends on this
* being accurate. Change those uses if you like. */
/** The length of the "extra" bytes in the key */
public static final short EXTRA_LENGTH = 5;
/** The length of the decryption key */
public static final short CRYPTO_KEY_LENGTH = 32;
/** Useful for e.g. length checks */
public static final ClientCHK TEST_KEY;
static {
try {
TEST_KEY = new ClientCHK(FreenetURI.generateRandomCHK(new Random()));
} catch (MalformedURLException e) {
throw new Error(e);
}
}
private ClientCHK(ClientCHK key) {
this.routingKey = key.routingKey.clone();
this.nodeKey = null;
this.cryptoKey = key.cryptoKey.clone();
this.controlDocument = key.controlDocument;
this.cryptoAlgorithm = key.cryptoAlgorithm;
this.compressionAlgorithm = key.compressionAlgorithm;
if(routingKey == null) throw new NullPointerException();
hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(routingKey) ^ compressionAlgorithm;
}
/**
* @param routingKey The routing key. This is the overall hash of the
* header and content of the key.
* @param encKey The decryption key. This is not passed to other nodes
* and is extracted from the URI.
* @param isCompressed True if the data was gzipped before encoding.
* @param isControlDocument True if the document is a Control Document.
* These carry metadata, whereas ordinary keys carry data, and have no
* type.
* @param algo The encryption algorithm's identifier. See ALGO_* for
* values.
*/
public ClientCHK(byte[] routingKey, byte[] encKey,
boolean isControlDocument, byte algo, short compressionAlgorithm) {
this.routingKey = routingKey;
this.cryptoKey = encKey;
this.controlDocument = isControlDocument;
this.cryptoAlgorithm = algo;
this.compressionAlgorithm = compressionAlgorithm;
if(routingKey == null) throw new NullPointerException();
hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(encKey) ^ compressionAlgorithm;
}
public ClientCHK(byte[] routingKey, byte[] encKey, byte[] extra) throws MalformedURLException {
this.routingKey = routingKey;
this.cryptoKey = encKey;
if((extra == null) || (extra.length < 5))
throw new MalformedURLException("No extra bytes in CHK - maybe a 0.5 key?");
// byte 0 is reserved, for now
cryptoAlgorithm = extra[1];
if(!(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256 || cryptoAlgorithm == Key.ALGO_AES_CTR_256_SHA256))
throw new MalformedURLException("Invalid crypto algorithm");
controlDocument = (extra[2] & 0x02) != 0;
compressionAlgorithm = (short)(((extra[3] & 0xff) << 8) + (extra[4] & 0xff));
hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(cryptoKey) ^ compressionAlgorithm;
}
/**
* Create from a URI.
*/
public ClientCHK(FreenetURI uri) throws MalformedURLException {
if(!uri.getKeyType().equals("CHK"))
throw new MalformedURLException("Not CHK");
routingKey = uri.getRoutingKey();
cryptoKey = uri.getCryptoKey();
byte[] extra = uri.getExtra();
if((extra == null) || (extra.length < 5))
throw new MalformedURLException("No extra bytes in CHK - maybe a 0.5 key?");
// byte 0 is reserved, for now
cryptoAlgorithm = extra[1];
if(!(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256 || cryptoAlgorithm == Key.ALGO_AES_CTR_256_SHA256))
throw new MalformedURLException("Invalid crypto algorithm");
controlDocument = (extra[2] & 0x02) != 0;
compressionAlgorithm = (short)(((extra[3] & 0xff) << 8) + (extra[4] & 0xff));
hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(cryptoKey) ^ compressionAlgorithm;
}
/**
* Create from a raw binary CHK. This expresses the key information
* in as few bytes as possible.
* @throws IOException
*/
public ClientCHK(DataInputStream dis) throws IOException {
byte[] extra = new byte[EXTRA_LENGTH];
dis.readFully(extra);
// byte 0 is reserved, for now
cryptoAlgorithm = extra[1];
if(!(cryptoAlgorithm == Key.ALGO_AES_PCFB_256_SHA256 || cryptoAlgorithm == Key.ALGO_AES_CTR_256_SHA256))
throw new MalformedURLException("Invalid crypto algorithm");
compressionAlgorithm = (short)(((extra[3] & 0xff) << 8) + (extra[4] & 0xff));
controlDocument = (extra[2] & 0x02) != 0;
routingKey = new byte[NodeCHK.KEY_LENGTH];
dis.readFully(routingKey);
cryptoKey = new byte[CRYPTO_KEY_LENGTH];
dis.readFully(cryptoKey);
hashCode = Fields.hashCode(routingKey) ^ Fields.hashCode(cryptoKey) ^ compressionAlgorithm;
}
protected ClientCHK() {
// Only for serialization.
routingKey = null;
cryptoKey = null;
controlDocument = false;
cryptoAlgorithm = 0;
compressionAlgorithm = 0;
hashCode = 0;
}
/**
* Write an ultra-compact representation.
* @throws IOException If a write failed.
*/
public void writeRawBinaryKey(DataOutputStream dos) throws IOException {
dos.write(getExtra());
dos.write(routingKey);
dos.write(cryptoKey);
}
static byte[] lastExtra;
public byte[] getExtra() {
return getExtra(cryptoAlgorithm, compressionAlgorithm, controlDocument);
}
public static byte[] getExtra(byte cryptoAlgorithm, short compressionAlgorithm, boolean controlDocument) {
byte[] extra = new byte[EXTRA_LENGTH];
extra[0] = (byte) (cryptoAlgorithm >> 8);
extra[1] = cryptoAlgorithm;
extra[2] = (byte) (controlDocument ? 2 : 0);
extra[3] = (byte) (compressionAlgorithm >> 8);
extra[4] = (byte) compressionAlgorithm;
byte[] last = lastExtra;
// No synchronization required IMHO
if(Arrays.equals(last, extra)) return last;
assert(extra.length == EXTRA_LENGTH);
lastExtra = extra;
return extra;
}
public static byte getCryptoAlgorithmFromExtra(byte[] extra) {
return extra[1];
}
static HashSet<ByteArrayWrapper> standardExtras = new HashSet<ByteArrayWrapper>();
static {
for(byte cryptoAlgorithm = Key.ALGO_AES_PCFB_256_SHA256; cryptoAlgorithm <= Key.ALGO_AES_CTR_256_SHA256; cryptoAlgorithm++) {
for(short compressionAlgorithm = -1; compressionAlgorithm <= (short)(COMPRESSOR_TYPE.countCompressors()); compressionAlgorithm++) {
standardExtras.add(new ByteArrayWrapper(getExtra(cryptoAlgorithm, compressionAlgorithm, true)));
standardExtras.add(new ByteArrayWrapper(getExtra(cryptoAlgorithm, compressionAlgorithm, false)));
}
}
}
public static byte[] internExtra(byte[] extra) {
for(ByteArrayWrapper baw : standardExtras) {
if(Arrays.equals(baw.get(), extra)) return baw.get();
}
return extra;
}
@Override
public String toString() {
return super.toString()+ ':' +Base64.encode(routingKey)+ ',' +
Base64.encode(cryptoKey)+ ',' +compressionAlgorithm+ ',' +controlDocument+
',' +cryptoAlgorithm;
}
@Override
public Key getNodeKey(boolean cloneKey) {
return cloneKey ? getNodeCHK().cloneKey() : getNodeCHK();
}
public synchronized NodeCHK getNodeCHK() {
// This costs us more or less nothing: we have to keep the routingKey anyway.
// Therefore, keeping a NodeCHK as well is a net saving, since it's frequently
// asked for. (A SoftReference would cost more).
if(nodeKey == null)
nodeKey = new NodeCHK(routingKey, cryptoAlgorithm);
return nodeKey;
}
/**
* @return URI form of this key.
*/
@Override
public FreenetURI getURI() {
byte[] extra = getExtra();
return new FreenetURI("CHK", null, routingKey, cryptoKey, extra);
}
/**
* Read a raw binary CHK. This is an ultra-compact representation, for
* splitfile metadata etc.
*/
public static ClientCHK readRawBinaryKey(DataInputStream dis) throws IOException {
return new ClientCHK(dis);
}
public boolean isMetadata() {
return controlDocument;
}
public boolean isCompressed() {
return compressionAlgorithm >= 0;
}
@Override
public ClientCHK cloneKey() {
return new ClientCHK(this);
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof ClientCHK)) return false;
ClientCHK key = (ClientCHK) o;
if(controlDocument != key.controlDocument) return false;
if(cryptoAlgorithm != key.cryptoAlgorithm) return false;
if(compressionAlgorithm != key.compressionAlgorithm) return false;
if(!Arrays.equals(routingKey, key.routingKey)) return false;
if(!Arrays.equals(cryptoKey, key.cryptoKey)) return false;
return true;
}
public byte[] getRoutingKey() {
return routingKey;
}
public byte[] getCryptoKey() {
return cryptoKey;
}
public byte getCryptoAlgorithm() {
return cryptoAlgorithm;
}
}