/* 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 org.bouncycastle.crypto.params.DSAPublicKeyParameters; import org.bouncycastle.crypto.signers.DSASigner; import java.math.BigInteger; import java.security.MessageDigest; import java.util.Arrays; import freenet.crypt.DSAPublicKey; import freenet.crypt.Global; import freenet.crypt.SHA256; import freenet.support.Fields; import freenet.support.HexUtil; import freenet.support.Logger; /** * SSKBlock. Contains a full fetched key. Can do a node-level verification. Can * decode original data when fed a ClientSSK. */ public class SSKBlock implements KeyBlock { private static volatile boolean logMINOR; static { Logger.registerClass(SSKBlock.class); } // how much of the headers we compare in order to consider two // SSKBlocks equal - necessary because the last 64 bytes need not // be the same for the same data and the same key (see comments below) private static final int HEADER_COMPARE_TO = 71; final byte[] data; final byte[] headers; /** The index of the first byte of encrypted fields in the headers, after E(H(docname)) */ final int headersOffset; /* HEADERS FORMAT: * 2 bytes - hash ID * 2 bytes - symmetric cipher ID * 32 bytes - E(H(docname)) * ENCRYPTED WITH E(H(docname)) AS IV: * 32 bytes - H(decrypted data), = data decryption key * 2 bytes - data length + metadata flag * 2 bytes - data compression algorithm or -1 * IMPLICIT - hash of data * IMPLICIT - hash of remaining fields, including the implicit hash of data * * SIGNATURE ON THE ABOVE HASH: * 32 bytes - signature: R (unsigned bytes) * 32 bytes - signature: S (unsigned bytes) * * PLUS THE PUBKEY: * Pubkey * Group */ final NodeSSK nodeKey; final DSAPublicKey pubKey; final short hashIdentifier; final short symCipherIdentifier; final int hashCode; public static final short DATA_LENGTH = 1024; /* Maximum length of compressed payload */ public static final int MAX_COMPRESSED_DATA_LENGTH = DATA_LENGTH - 2; static final short SIG_R_LENGTH = 32; static final short SIG_S_LENGTH = 32; static final short E_H_DOCNAME_LENGTH = 32; static public final short TOTAL_HEADERS_LENGTH = 2 + SIG_R_LENGTH + SIG_S_LENGTH + 2 + E_H_DOCNAME_LENGTH + ClientSSKBlock.DATA_DECRYPT_KEY_LENGTH + 2 + 2; static final short ENCRYPTED_HEADERS_LENGTH = 36; @Override public boolean equals(Object o) { if(!(o instanceof SSKBlock)) return false; SSKBlock block = (SSKBlock)o; if(!block.pubKey.equals(pubKey)) return false; if(!block.nodeKey.equals(nodeKey)) return false; if(block.headersOffset != headersOffset) return false; if(block.hashIdentifier != hashIdentifier) return false; if(block.symCipherIdentifier != symCipherIdentifier) return false; // only compare some of the headers (see top) for (int i = 0; i < HEADER_COMPARE_TO; i++) { if (block.headers[i] != headers[i]) return false; } //if(!Arrays.equals(block.headers, headers)) return false; if(!Arrays.equals(block.data, data)) return false; return true; } @Override public int hashCode(){ return hashCode; } /** * Initialize, and verify data, headers against key. Provided * key must have a pubkey, or we throw. */ public SSKBlock(byte[] data, byte[] headers, NodeSSK nodeKey, boolean dontVerify) throws SSKVerifyException { if(headers.length != TOTAL_HEADERS_LENGTH) throw new IllegalArgumentException("Headers.length="+headers.length+" should be "+TOTAL_HEADERS_LENGTH); this.data = data; this.headers = headers; this.nodeKey = nodeKey; if(data.length != DATA_LENGTH) throw new SSKVerifyException("Data length wrong: "+data.length+" should be "+DATA_LENGTH); this.pubKey = nodeKey.getPubKey(); if(pubKey == null) throw new SSKVerifyException("PubKey was null from "+nodeKey); // Now verify it hashIdentifier = (short)(((headers[0] & 0xff) << 8) + (headers[1] & 0xff)); if(hashIdentifier != HASH_SHA256) throw new SSKVerifyException("Hash not SHA-256"); int x = 2; symCipherIdentifier = (short)(((headers[x] & 0xff) << 8) + (headers[x+1] & 0xff)); x+=2; // Then E(H(docname)) byte[] ehDocname = new byte[E_H_DOCNAME_LENGTH]; System.arraycopy(headers, x, ehDocname, 0, ehDocname.length); x += E_H_DOCNAME_LENGTH; headersOffset = x; // is index to start of encrypted headers x += ENCRYPTED_HEADERS_LENGTH; // Extract the signature if(x+SIG_R_LENGTH+SIG_S_LENGTH > headers.length) throw new SSKVerifyException("Headers too short: "+headers.length+" should be at least "+x+SIG_R_LENGTH+SIG_S_LENGTH); // Compute the hash on the data if(!dontVerify || logMINOR) { // force verify on log minor byte[] bufR = new byte[SIG_R_LENGTH]; byte[] bufS = new byte[SIG_S_LENGTH]; System.arraycopy(headers, x, bufR, 0, SIG_R_LENGTH); x+=SIG_R_LENGTH; System.arraycopy(headers, x, bufS, 0, SIG_S_LENGTH); x+=SIG_S_LENGTH; MessageDigest md = null; byte[] overallHash; try { md = SHA256.getMessageDigest(); md.update(data); byte[] dataHash = md.digest(); // All headers up to and not including the signature md.update(headers, 0, headersOffset + ENCRYPTED_HEADERS_LENGTH); // Then the implicit data hash md.update(dataHash); // Makes the implicit overall hash overallHash = md.digest(); } finally { SHA256.returnMessageDigest(md); } // Now verify it BigInteger r = new BigInteger(1, bufR); BigInteger s = new BigInteger(1, bufS); DSASigner dsa = new DSASigner(); dsa.init(false, new DSAPublicKeyParameters(pubKey.getY(), Global.getDSAgroupBigAParameters())); // We probably don't need to try both here... // but that's what the legacy code was doing... // @see comments in Global before touching it if(!(dsa.verifySignature(Global.truncateHash(overallHash), r, s) || dsa.verifySignature(overallHash, r, s)) ) { if (dontVerify) Logger.error(this, "DSA verification failed with dontVerify!!!!"); throw new SSKVerifyException("Signature verification failed for node-level SSK"); } } // x isn't verified otherwise so no need to += SIG_R_LENGTH + SIG_S_LENGTH if(!Arrays.equals(ehDocname, nodeKey.encryptedHashedDocname)) throw new SSKVerifyException("E(H(docname)) wrong - wrong key?? \nfrom headers: "+HexUtil.bytesToHex(ehDocname)+"\nfrom key: "+HexUtil.bytesToHex(nodeKey.encryptedHashedDocname)); hashCode = Fields.hashCode(data) ^ Fields.hashCode(headers) ^ nodeKey.hashCode() ^ pubKey.hashCode() ^ hashIdentifier; } @Override public NodeSSK getKey() { return nodeKey; } @Override public byte[] getRawHeaders() { return headers; } @Override public byte[] getRawData() { return data; } public DSAPublicKey getPubKey() { return pubKey; } @Override public byte[] getPubkeyBytes() { return pubKey.asBytes(); } @Override public byte[] getFullKey() { return getKey().getFullKey(); } @Override public byte[] getRoutingKey() { return getKey().getRoutingKey(); } }