/* 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.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.util.Arrays;
import freenet.crypt.CryptFormatException;
import freenet.crypt.DSAPublicKey;
import freenet.crypt.SHA256;
import freenet.io.WritableToDataOutputStream;
import freenet.support.Fields;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.SimpleReadOnlyArrayBucket;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
import freenet.support.compress.CompressionOutputSizeException;
import freenet.support.compress.InvalidCompressionCodecException;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
import freenet.support.io.ArrayBucket;
import freenet.support.io.ArrayBucketFactory;
import freenet.support.io.BucketTools;
/**
* @author amphibian
*
* Base class for node keys.
*
* WARNING: Changing non-transient members on classes that are Serializable can result in
* restarting downloads or losing uploads.
*/
public abstract class Key implements WritableToDataOutputStream, Comparable<Key> {
final int hash;
double cachedNormalizedDouble;
/** Whatever its type, it will need a routingKey ! */
final byte[] routingKey;
/** Code for 256-bit AES with PCFB and SHA-256 */
public static final byte ALGO_AES_PCFB_256_SHA256 = 2;
public static final byte ALGO_AES_CTR_256_SHA256 = 3;
private static volatile boolean logMINOR;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback() {
@Override
public void shouldUpdate() {
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
}
});
}
protected Key(byte[] routingKey) {
this.routingKey = routingKey;
hash = Fields.hashCode(routingKey);
cachedNormalizedDouble = -1;
}
protected Key(Key key) {
this.hash = key.hash;
this.cachedNormalizedDouble = key.cachedNormalizedDouble;
this.routingKey = new byte[key.routingKey.length];
System.arraycopy(key.routingKey, 0, routingKey, 0, routingKey.length);
}
public abstract Key cloneKey();
/**
* Write to disk.
* Take up exactly 22 bytes.
* @param _index
*/
public abstract void write(DataOutput _index) throws IOException;
/**
* Read a Key from a RandomAccessFile
* @param raf The file to read from.
* @return a Key, or throw an exception, or return null if the key is not parsable.
*/
public static Key read(DataInput raf) throws IOException {
byte type = raf.readByte();
byte subtype = raf.readByte();
if(type == NodeCHK.BASE_TYPE) {
return NodeCHK.readCHK(raf, subtype);
} else if(type == NodeSSK.BASE_TYPE)
return NodeSSK.readSSK(raf, subtype);
throw new IOException("Unrecognized format: "+type);
}
public static KeyBlock createBlock(short keyType, byte[] keyBytes, byte[] headersBytes, byte[] dataBytes, byte[] pubkeyBytes) throws KeyVerifyException {
byte type = (byte)(keyType >> 8);
byte subtype = (byte)(keyType & 0xFF);
if(type == NodeCHK.BASE_TYPE) {
// For CHKs, the subtype is the crypto algorithm.
return CHKBlock.construct(dataBytes, headersBytes, subtype);
} else if(type == NodeSSK.BASE_TYPE) {
DSAPublicKey pubKey;
try {
pubKey = new DSAPublicKey(pubkeyBytes);
} catch (IOException e) {
throw new KeyVerifyException("Failed to construct pubkey: "+e, e);
} catch (CryptFormatException e) {
throw new KeyVerifyException("Failed to construct pubkey: "+e, e);
}
NodeSSK key = new NodeSSK(pubKey.asBytesHash(), keyBytes, pubKey, subtype);
return new SSKBlock(dataBytes, headersBytes, key, false);
} else {
throw new KeyVerifyException("No such key type "+Integer.toHexString(type));
}
}
/**
* Convert the key to a double between 0.0 and 1.0.
* Normally we will hash the key first, in order to
* make chosen-key attacks harder.
*/
public synchronized double toNormalizedDouble() {
if(cachedNormalizedDouble > 0) return cachedNormalizedDouble;
MessageDigest md = SHA256.getMessageDigest();
if(routingKey == null) throw new NullPointerException();
md.update(routingKey);
int TYPE = getType();
md.update((byte)(TYPE >> 8));
md.update((byte)TYPE);
byte[] digest = md.digest();
SHA256.returnMessageDigest(md); md = null;
long asLong = Math.abs(Fields.bytesToLong(digest));
// Math.abs can actually return negative...
if(asLong == Long.MIN_VALUE)
asLong = Long.MAX_VALUE;
cachedNormalizedDouble = ((double)asLong)/((double)Long.MAX_VALUE);
return cachedNormalizedDouble;
}
/**
* Get key type
* <ul>
* <li>
* High 8 bit (<tt>(type >> 8) & 0xFF</tt>) is the base type ({@link NodeCHK#BASE_TYPE} or
* {@link NodeSSK#BASE_TYPE}).
* <li>Low 8 bit (<tt>type & 0xFF</tt>) is the crypto algorithm. (Currently only
* {@link #ALGO_AES_PCFB_256_SHA256} is supported).
* </ul>
*/
public abstract short getType();
@Override
public int hashCode() {
return hash;
}
@Override
public boolean equals(Object o){
if(o == null || !(o instanceof Key)) return false;
return Arrays.equals(routingKey, ((Key)o).routingKey);
}
static Bucket decompress(boolean isCompressed, byte[] input, int inputLength, BucketFactory bf, long maxLength, short compressionAlgorithm, boolean shortLength) throws CHKDecodeException, IOException {
if(maxLength < 0)
throw new IllegalArgumentException("maxlength="+maxLength);
if(input.length < inputLength)
throw new IndexOutOfBoundsException(""+input.length+"<"+inputLength);
if(isCompressed) {
if(logMINOR)
Logger.minor(Key.class, "Decompressing "+inputLength+" bytes in decode with codec "+compressionAlgorithm);
final int inputOffset = (shortLength ? 2 : 4);
if(inputLength < inputOffset + 1) throw new CHKDecodeException("No bytes to decompress");
// Decompress
// First get the length
int len;
if(shortLength)
len = ((input[0] & 0xff) << 8) + (input[1] & 0xff);
else
len = ((((((input[0] & 0xff) << 8) + (input[1] & 0xff)) << 8) + (input[2] & 0xff)) << 8) +
(input[3] & 0xff);
if(len > maxLength)
throw new TooBigException("Invalid precompressed size: "+len + " maxlength="+maxLength);
COMPRESSOR_TYPE decompressor = COMPRESSOR_TYPE.getCompressorByMetadataID(compressionAlgorithm);
if (decompressor==null)
throw new CHKDecodeException("Unknown compression algorithm: "+compressionAlgorithm);
InputStream inputStream = null;
OutputStream outputStream = null;
Bucket inputBucket = new SimpleReadOnlyArrayBucket(input, inputOffset, inputLength-inputOffset);
Bucket outputBucket = bf.makeBucket(maxLength);
outputStream = outputBucket.getOutputStream();
inputStream = inputBucket.getInputStream();
try {
decompressor.decompress(inputStream, outputStream, maxLength, -1);
} catch (CompressionOutputSizeException e) {
throw new TooBigException("Too big");
} finally {
inputStream.close();
outputStream.close();
inputBucket.free();
}
return outputBucket;
} else {
return BucketTools.makeImmutableBucket(bf, input, inputLength);
}
}
static class Compressed {
public Compressed(byte[] finalData, short compressionAlgorithm2) {
this.compressedData = finalData;
this.compressionAlgorithm = compressionAlgorithm2;
}
byte[] compressedData;
short compressionAlgorithm;
}
/**
* Compress data.
* @param sourceData
* @param dontCompress
* @param alreadyCompressedCodec
* @param sourceLength
* @param MAX_LENGTH_BEFORE_COMPRESSION
* @param MAX_COMPRESSED_DATA_LENGTH
* @param shortLength
* @param compressordescriptor
* @param pre1254 If this is set, use OldLZMA not NewLZMA.
* @return
* @throws KeyEncodeException
* @throws IOException
* @throws InvalidCompressionCodecException
*/
static Compressed compress(Bucket sourceData, boolean dontCompress, short alreadyCompressedCodec, long sourceLength, long MAX_LENGTH_BEFORE_COMPRESSION, int MAX_COMPRESSED_DATA_LENGTH, boolean shortLength, String compressordescriptor, boolean pre1254) throws KeyEncodeException, IOException, InvalidCompressionCodecException {
byte[] finalData = null;
short compressionAlgorithm = -1;
int maxCompressedDataLength = MAX_COMPRESSED_DATA_LENGTH;
if(shortLength)
maxCompressedDataLength -= 2;
else
maxCompressedDataLength -= 4;
if(sourceData.size() > MAX_LENGTH_BEFORE_COMPRESSION)
throw new KeyEncodeException("Too big");
if((!dontCompress) || (alreadyCompressedCodec >= 0)) {
byte[] cbuf = null;
if(alreadyCompressedCodec >= 0) {
if(sourceData.size() > maxCompressedDataLength)
throw new TooBigException("Too big (precompressed)");
compressionAlgorithm = alreadyCompressedCodec;
cbuf = BucketTools.toByteArray(sourceData);
if(sourceLength > MAX_LENGTH_BEFORE_COMPRESSION)
throw new TooBigException("Too big");
} else {
if (sourceData.size() > maxCompressedDataLength) {
// Determine the best algorithm
COMPRESSOR_TYPE[] comps = COMPRESSOR_TYPE.getCompressorsArray(compressordescriptor, pre1254);
for (COMPRESSOR_TYPE comp : comps) {
ArrayBucket compressedData;
try {
compressedData = (ArrayBucket) comp.compress(
sourceData, new ArrayBucketFactory(), Long.MAX_VALUE, maxCompressedDataLength);
} catch (CompressionOutputSizeException e) {
continue;
}
if (compressedData.size() <= maxCompressedDataLength) {
compressionAlgorithm = comp.metadataID;
sourceLength = sourceData.size();
try {
cbuf = BucketTools.toByteArray(compressedData);
// FIXME provide a method in ArrayBucket
} catch (IOException e) {
throw new Error(e);
}
break;
}
}
}
}
if(cbuf != null) {
// Use it
int compressedLength = cbuf.length;
finalData = new byte[compressedLength+(shortLength?2:4)];
System.arraycopy(cbuf, 0, finalData, shortLength?2:4, compressedLength);
if(!shortLength) {
finalData[0] = (byte) ((sourceLength >> 24) & 0xff);
finalData[1] = (byte) ((sourceLength >> 16) & 0xff);
finalData[2] = (byte) ((sourceLength >> 8) & 0xff);
finalData[3] = (byte) ((sourceLength) & 0xff);
} else {
finalData[0] = (byte) ((sourceLength >> 8) & 0xff);
finalData[1] = (byte) ((sourceLength) & 0xff);
}
}
}
if(finalData == null) {
// Not compressed or not compressible; no size bytes to be added.
if(sourceData.size() > MAX_COMPRESSED_DATA_LENGTH) {
throw new CHKEncodeException("Too big: "+sourceData.size()+" should be "+MAX_COMPRESSED_DATA_LENGTH);
}
finalData = BucketTools.toByteArray(sourceData);
}
return new Compressed(finalData, compressionAlgorithm);
}
public byte[] getRoutingKey() {
return routingKey;
}
// Not just the routing key, enough data to reconstruct the key (excluding any pubkey needed)
public byte[] getKeyBytes() {
return routingKey;
}
public static ClientKeyBlock createKeyBlock(ClientKey key, KeyBlock block) throws KeyVerifyException {
if(key instanceof ClientSSK)
return ClientSSKBlock.construct((SSKBlock)block, (ClientSSK)key);
else //if(key instanceof ClientCHK
return new ClientCHKBlock((CHKBlock)block, (ClientCHK)key);
}
/** Get the full key, including any crypto type bytes, everything needed to construct a Key object */
public abstract byte[] getFullKey();
/** Get a copy of the key with any unnecessary information stripped, for long-term
* in-memory storage. E.g. for SSKs, strips the DSAPublicKey. Copies it whether or
* not we need to copy it because the original might pick up a pubkey after this
* call. And the returned key will not accidentally pick up extra data. */
public abstract Key archivalCopy();
public static boolean isValidCryptoAlgorithm(byte cryptoAlgorithm) {
return cryptoAlgorithm == ALGO_AES_PCFB_256_SHA256 ||
cryptoAlgorithm == ALGO_AES_CTR_256_SHA256;
}
}