package net.i2p.crypto; /* * free (adj.): unencumbered; not under the control of others * Written by jrandom in 2003 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.util.Log; import net.i2p.util.SimpleByteCache; /** * Handles the actual ElGamal+AES encryption and decryption scenarios using the * supplied keys and data. * * No, this does not extend AESEngine or CryptixAESEngine. */ public final class ElGamalAESEngine { private final Log _log; private final static int MIN_ENCRYPTED_SIZE = 80; // smallest possible resulting size private final I2PAppContext _context; /** enforced since release 0.6 */ public static final int MAX_TAGS_RECEIVED = 200; public ElGamalAESEngine(I2PAppContext ctx) { _context = ctx; _log = _context.logManager().getLog(ElGamalAESEngine.class); _context.statManager().createFrequencyStat("crypto.elGamalAES.encryptNewSession", "how frequently we encrypt to a new ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*60*1000l}); _context.statManager().createFrequencyStat("crypto.elGamalAES.encryptExistingSession", "how frequently we encrypt to an existing ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*60*1000l}); _context.statManager().createFrequencyStat("crypto.elGamalAES.decryptNewSession", "how frequently we decrypt with a new ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*60*1000l}); _context.statManager().createFrequencyStat("crypto.elGamalAES.decryptExistingSession", "how frequently we decrypt with an existing ElGamal/AES+SessionTag session?", "Encryption", new long[] { 60*60*1000l}); _context.statManager().createFrequencyStat("crypto.elGamalAES.decryptFailed", "how frequently we fail to decrypt with ElGamal/AES+SessionTag?", "Encryption", new long[] { 60*60*1000l}); } /** * Decrypt the message using the given private key using tags from the default key manager, * which is the router's key manager. Use extreme care if you aren't the router. * * @deprecated specify the key manager! */ public byte[] decrypt(byte data[], PrivateKey targetPrivateKey) throws DataFormatException { return decrypt(data, targetPrivateKey, _context.sessionKeyManager()); } /** * Decrypt the message using the given private key * and using tags from the specified key manager. * This works according to the * ElGamal+AES algorithm in the data structure spec. * * Warning - use the correct SessionKeyManager. Clients should instantiate their own. * Clients using I2PAppContext.sessionKeyManager() may be correlated with the router, * unless you are careful to use different keys. * * @return decrypted data or null on failure */ public byte[] decrypt(byte data[], PrivateKey targetPrivateKey, SessionKeyManager keyManager) throws DataFormatException { if (data == null) { if (_log.shouldLog(Log.ERROR)) _log.error("Null data being decrypted?"); return null; } else if (data.length < MIN_ENCRYPTED_SIZE) { if (_log.shouldLog(Log.ERROR)) _log.error("Data is less than the minimum size (" + data.length + " < " + MIN_ENCRYPTED_SIZE + ")"); return null; } byte tag[] = new byte[32]; System.arraycopy(data, 0, tag, 0, 32); SessionTag st = new SessionTag(tag); SessionKey key = keyManager.consumeTag(st); SessionKey foundKey = new SessionKey(); SessionKey usedKey = new SessionKey(); Set<SessionTag> foundTags = new HashSet<SessionTag>(); byte decrypted[] = null; boolean wasExisting = false; if (key != null) { //if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is known for tag " + st); if (_log.shouldLog(Log.DEBUG)) _log.debug("Decrypting existing session encrypted with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes " /* + Base64.encode(data, 0, 64) */ ); decrypted = decryptExistingSession(data, key, targetPrivateKey, foundTags, usedKey, foundKey); if (decrypted != null) { _context.statManager().updateFrequency("crypto.elGamalAES.decryptExistingSession"); if ( (!foundTags.isEmpty()) && (_log.shouldLog(Log.DEBUG)) ) _log.debug("ElG/AES decrypt success with " + st + ": found tags: " + foundTags); wasExisting = true; } else { _context.statManager().updateFrequency("crypto.elGamalAES.decryptFailed"); if (_log.shouldLog(Log.WARN)) { _log.warn("ElG decrypt fail: known tag [" + st + "], failed decrypt"); } } } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is NOT known for tag " + st); decrypted = decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey); if (decrypted != null) { _context.statManager().updateFrequency("crypto.elGamalAES.decryptNewSession"); if ( (!foundTags.isEmpty()) && (_log.shouldLog(Log.DEBUG)) ) _log.debug("ElG decrypt success: found tags: " + foundTags); } else { _context.statManager().updateFrequency("crypto.elGamalAES.decryptFailed"); if (_log.shouldLog(Log.WARN)) _log.warn("ElG decrypt fail: unknown tag: " + st); } } if ((key == null) && (decrypted == null)) { //_log.debug("Unable to decrypt the data starting with tag [" + st + "] - did the tag expire recently?", new Exception("Decrypt failure")); } if (!foundTags.isEmpty()) { if (foundKey.getData() != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Found key: " + foundKey.toBase64() + " tags: " + foundTags + " wasExisting? " + wasExisting); keyManager.tagsReceived(foundKey, foundTags); } else if (usedKey.getData() != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Used key: " + usedKey.toBase64() + " tags: " + foundTags + " wasExisting? " + wasExisting); keyManager.tagsReceived(usedKey, foundTags); } } return decrypted; } /** * scenario 1: * Begin with 222 bytes, ElG encrypted, containing: * <pre> * - 32 byte SessionKey * - 32 byte pre-IV for the AES * - 158 bytes of random padding * </pre> * After encryption, the ElG section is 514 bytes long. * Then encrypt with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV, using * the decryptAESBlock method & structure. * * @param foundTags set which is filled with any sessionTags found during decryption * @param foundKey out parameter. Data must be unset when called; may be filled with a new sessionKey found during decryption * @param usedKey out parameter. Data must be unset when called; usedKey.setData() will be called by this method on success. * * @return null if decryption fails */ private byte[] decryptNewSession(byte data[], PrivateKey targetPrivateKey, Set<SessionTag> foundTags, SessionKey usedKey, SessionKey foundKey) throws DataFormatException { if (data == null) { //if (_log.shouldLog(Log.WARN)) _log.warn("Data is null, unable to decrypt new session"); return null; } else if (data.length < 514) { //if (_log.shouldLog(Log.WARN)) _log.warn("Data length is too small (" + data.length + ")"); return null; } byte elgEncr[] = new byte[514]; if (data.length > 514) { System.arraycopy(data, 0, elgEncr, 0, 514); } else { System.arraycopy(data, 0, elgEncr, 514 - data.length, data.length); } byte elgDecr[] = _context.elGamalEngine().decrypt(elgEncr, targetPrivateKey); if (elgDecr == null) { //if (_log.shouldLog(Log.WARN)) // _log.warn("decrypt returned null", new Exception("decrypt failed")); return null; } int offset = 0; byte key[] = new byte[SessionKey.KEYSIZE_BYTES]; System.arraycopy(elgDecr, offset, key, 0, SessionKey.KEYSIZE_BYTES); offset += SessionKey.KEYSIZE_BYTES; usedKey.setData(key); byte[] preIV = SimpleByteCache.acquire(32); System.arraycopy(elgDecr, offset, preIV, 0, 32); offset += 32; //_log.debug("Pre IV for decryptNewSession: " + DataHelper.toString(preIV, 32)); //_log.debug("SessionKey for decryptNewSession: " + DataHelper.toString(key.getData(), 32)); // use alternate calculateHash() method to avoid object churn and caching //Hash ivHash = _context.sha().calculateHash(preIV); //byte iv[] = new byte[16]; //System.arraycopy(ivHash.getData(), 0, iv, 0, 16); byte[] iv = halfHash(preIV); SimpleByteCache.release(preIV); // feed the extra bytes into the PRNG _context.random().harvester().feedEntropy("ElG/AES", elgDecr, offset, elgDecr.length - offset); byte aesDecr[] = decryptAESBlock(data, 514, data.length-514, usedKey, iv, null, foundTags, foundKey); SimpleByteCache.release(iv); //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Decrypt with a NEW session successfull: # tags read = " + foundTags.size(), // new Exception("Decrypted by")); return aesDecr; } /** * scenario 2: * The data begins with 32 byte session tag, which also serves as the preIV. * Then decrypt with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV: * <pre> * - 2 byte integer specifying the # of session tags * - that many 32 byte session tags * - 4 byte integer specifying data.length * - SHA256 of data * - 1 byte flag that, if == 1, is followed by a new SessionKey * - data * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 * </pre> * * If anything doesn't match up in decryption, it falls back to decryptNewSession * * @param foundTags set which is filled with any sessionTags found during decryption * @param foundKey out parameter. Data must be unset when called; may be filled with a new sessionKey found during decryption * @param usedKey out parameter. Data must be unset when called; usedKey.setData() will be called by this method on success. * * @return decrypted data or null on failure * */ private byte[] decryptExistingSession(byte data[], SessionKey key, PrivateKey targetPrivateKey, Set<SessionTag> foundTags, SessionKey usedKey, SessionKey foundKey) throws DataFormatException { byte preIV[] = SimpleByteCache.acquire(32); System.arraycopy(data, 0, preIV, 0, 32); // use alternate calculateHash() method to avoid object churn and caching //Hash ivHash = _context.sha().calculateHash(preIV); //byte iv[] = new byte[16]; //System.arraycopy(ivHash.getData(), 0, iv, 0, 16); byte[] iv = halfHash(preIV); SimpleByteCache.release(preIV); //_log.debug("Pre IV for decryptExistingSession: " + DataHelper.toString(preIV, 32)); //_log.debug("SessionKey for decryptNewSession: " + DataHelper.toString(key.getData(), 32)); byte decrypted[] = decryptAESBlock(data, 32, data.length-32, key, iv, preIV, foundTags, foundKey); SimpleByteCache.release(iv); if (decrypted == null) { // it begins with a valid session tag, but thats just a coincidence. //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Decrypt with a non session tag, but tags read: " + foundTags.size()); if (_log.shouldLog(Log.WARN)) _log.warn("Decrypting looks negative... existing key fails with existing tag, lets try as a new one"); byte rv[] = decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey); if (_log.shouldLog(Log.WARN)) { if (rv == null) _log.warn("Decrypting failed with a known existing tag as either an existing message or a new session"); else _log.warn("Decrypting suceeded as a new session, even though it used an existing tag!"); } return rv; } // existing session decrypted successfully! //if (_log.shouldLog(Log.DEBUG)) // _log.debug("Decrypt with an EXISTING session tag successfull, # tags read: " + foundTags.size(), // new Exception("Decrypted by")); usedKey.setData(key.getData()); return decrypted; } /** * Decrypt the AES data with the session key and IV. The result should be: * <pre> * - 2 byte integer specifying the # of session tags * - that many 32 byte session tags * - 4 byte integer specifying data.length * - SHA256 of data * - 1 byte flag that, if == 1, is followed by a new SessionKey * - data * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 * </pre> * * If anything doesn't match up in decryption, return null. Otherwise, return * the decrypted data and update the session as necessary. If the sentTag is not null, * consume it, but if it is null, record the keys, etc as part of a new session. * * @param foundTags set which is filled with any sessionTags found during decryption * @param foundKey out parameter. Data must be unset when called; may be filled with a new sessionKey found during decryption * @return decrypted data or null on failure */ /**** private byte[] decryptAESBlock(byte encrypted[], SessionKey key, byte iv[], byte sentTag[], Set foundTags, SessionKey foundKey) throws DataFormatException { return decryptAESBlock(encrypted, 0, encrypted.length, key, iv, sentTag, foundTags, foundKey); } ****/ /* * Note: package private for ElGamalTest.testAES() */ byte[] decryptAESBlock(byte encrypted[], int offset, int encryptedLen, SessionKey key, byte iv[], byte sentTag[], Set<SessionTag> foundTags, SessionKey foundKey) throws DataFormatException { //_log.debug("iv for decryption: " + DataHelper.toString(iv, 16)); //_log.debug("decrypting AES block. encr.length = " + (encrypted == null? -1 : encrypted.length) + " sentTag: " + DataHelper.toString(sentTag, 32)); byte decrypted[] = new byte[encryptedLen]; _context.aes().decrypt(encrypted, offset, decrypted, 0, key, iv, encryptedLen); //Hash h = _context.sha().calculateHash(decrypted); //_log.debug("Hash of entire aes block after decryption: \n" + DataHelper.toString(h.getData(), 32)); try { SessionKey newKey = null; List<SessionTag> tags = null; //ByteArrayInputStream bais = new ByteArrayInputStream(decrypted); int cur = 0; long numTags = DataHelper.fromLong(decrypted, cur, 2); if ((numTags < 0) || (numTags > MAX_TAGS_RECEIVED)) throw new IllegalArgumentException("Invalid number of session tags"); if (numTags > 0) tags = new ArrayList<SessionTag>((int)numTags); cur += 2; //_log.debug("# tags: " + numTags); if (numTags * SessionTag.BYTE_LENGTH > decrypted.length - 2) { throw new IllegalArgumentException("# tags: " + numTags + " is too many for " + (decrypted.length - 2)); } for (int i = 0; i < numTags; i++) { byte tag[] = new byte[SessionTag.BYTE_LENGTH]; System.arraycopy(decrypted, cur, tag, 0, SessionTag.BYTE_LENGTH); cur += SessionTag.BYTE_LENGTH; tags.add(new SessionTag(tag)); } long len = DataHelper.fromLong(decrypted, cur, 4); cur += 4; //_log.debug("len: " + len); if ((len < 0) || (len > decrypted.length - cur - Hash.HASH_LENGTH - 1)) throw new IllegalArgumentException("Invalid size of payload (" + len + ", remaining " + (decrypted.length-cur) +")"); //byte hashval[] = new byte[Hash.HASH_LENGTH]; //System.arraycopy(decrypted, cur, hashval, 0, Hash.HASH_LENGTH); //readHash = new Hash(); //readHash.setData(hashval); //readHash = Hash.create(decrypted, cur); int hashIndex = cur; cur += Hash.HASH_LENGTH; byte flag = decrypted[cur++]; if (flag == 0x01) { byte rekeyVal[] = new byte[SessionKey.KEYSIZE_BYTES]; System.arraycopy(decrypted, cur, rekeyVal, 0, SessionKey.KEYSIZE_BYTES); cur += SessionKey.KEYSIZE_BYTES; newKey = new SessionKey(); newKey.setData(rekeyVal); } byte unencrData[] = new byte[(int) len]; System.arraycopy(decrypted, cur, unencrData, 0, (int)len); cur += (int) len; // use alternate calculateHash() method to avoid object churn and caching //Hash calcHash = _context.sha().calculateHash(unencrData); //boolean eq = calcHash.equals(readHash); byte[] calcHash = SimpleByteCache.acquire(32); _context.sha().calculateHash(unencrData, 0, (int) len, calcHash, 0); boolean eq = DataHelper.eq(decrypted, hashIndex, calcHash, 0, 32); SimpleByteCache.release(calcHash); if (eq) { // everything matches. w00t. if (tags != null) foundTags.addAll(tags); if (newKey != null) foundKey.setData(newKey.getData()); return unencrData; } throw new RuntimeException("Hash does not match"); } catch (RuntimeException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to decrypt AES block", e); return null; } } /** * Encrypt the unencrypted data to the target. The total size returned will be * no less than the paddedSize parameter, but may be more. This method uses the * ElGamal+AES algorithm in the data structure spec. * * @param target public key to which the data should be encrypted. * @param key session key to use during encryption * @param tagsForDelivery session tags to be associated with the key (or newKey if specified), or null; * 200 max enforced at receiver * @param currentTag sessionTag to use, or null if it should use ElG (i.e. new session) * @param newKey key to be delivered to the target, with which the tagsForDelivery should be associated, or null * @param paddedSize minimum size in bytes of the body after padding it (if less than the * body's real size, no bytes are appended but the body is not truncated) * * Unused externally, only called by below (i.e. newKey is always null) */ public byte[] encrypt(byte data[], PublicKey target, SessionKey key, Set<SessionTag> tagsForDelivery, SessionTag currentTag, SessionKey newKey, long paddedSize) { if (currentTag == null) { if (_log.shouldLog(Log.INFO)) _log.info("Current tag is null, encrypting as new session"); _context.statManager().updateFrequency("crypto.elGamalAES.encryptNewSession"); return encryptNewSession(data, target, key, tagsForDelivery, newKey, paddedSize); } //if (_log.shouldLog(Log.INFO)) // _log.info("Current tag is NOT null, encrypting as existing session"); _context.statManager().updateFrequency("crypto.elGamalAES.encryptExistingSession"); byte rv[] = encryptExistingSession(data, target, key, tagsForDelivery, currentTag, newKey, paddedSize); if (_log.shouldLog(Log.DEBUG)) _log.debug("Existing session encrypted with tag: " + currentTag.toString() + ": " + rv.length + " bytes and key: " + key.toBase64() /* + ": " + Base64.encode(rv, 0, 64) */); return rv; } /** * Encrypt the data to the target using the given key and deliver the specified tags * No new session key * This is the one called from GarlicMessageBuilder and is the primary entry point. * * Re: padded size: The AES block adds at least 39 bytes of overhead to the data, and * that is included in the minimum size calculation. * * In the router, we always use garlic messages. A garlic message with a single * clove and zero data is about 84 bytes, so that's 123 bytes minimum. So any paddingSize * <= 128 is a no-op as every message will be at least 128 bytes * (Streaming, if used, adds more overhead). * * Outside the router, with a client using its own message format, the minimum size * is 48, so any paddingSize <= 48 is a no-op. * * Not included in the minimum is a 32-byte session tag for an existing session, * or a 514-byte ElGamal block and several 32-byte session tags for a new session. * So the returned encrypted data will be at least 32 bytes larger than paddedSize. * * @param target public key to which the data should be encrypted. * @param key session key to use during encryption * @param tagsForDelivery session tags to be associated with the key or null; * 200 max enforced at receiver * @param currentTag sessionTag to use, or null if it should use ElG (i.e. new session) * @param paddedSize minimum size in bytes of the body after padding it (if less than the * body's real size, no bytes are appended but the body is not truncated) * */ public byte[] encrypt(byte data[], PublicKey target, SessionKey key, Set<SessionTag> tagsForDelivery, SessionTag currentTag, long paddedSize) { return encrypt(data, target, key, tagsForDelivery, currentTag, null, paddedSize); } /** * Encrypt the data to the target using the given key and deliver the specified tags * No new session key * No current tag (encrypt as new session) * * @param tagsForDelivery session tags to be associated with the key or null; * 200 max enforced at receiver * @deprecated unused */ public byte[] encrypt(byte data[], PublicKey target, SessionKey key, Set<SessionTag> tagsForDelivery, long paddedSize) { return encrypt(data, target, key, tagsForDelivery, null, null, paddedSize); } /** * Encrypt the data to the target using the given key delivering no tags * No new session key * No current tag (encrypt as new session) * * @deprecated unused */ public byte[] encrypt(byte data[], PublicKey target, SessionKey key, long paddedSize) { return encrypt(data, target, key, null, null, null, paddedSize); } /** * scenario 1: * Begin with 222 bytes, ElG encrypted, containing: * <pre> * - 32 byte SessionKey * - 32 byte pre-IV for the AES * - 158 bytes of random padding * </pre> * After encryption, the ElG section is 514 bytes long. * Then encrypt the following with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV: * <pre> * - 2 byte integer specifying the # of session tags * - that many 32 byte session tags * - 4 byte integer specifying data.length * - SHA256 of data * - 1 byte flag that, if == 1, is followed by a new SessionKey * - data * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 * </pre> * * @param tagsForDelivery session tags to be associated with the key or null; * 200 max enforced at receiver */ private byte[] encryptNewSession(byte data[], PublicKey target, SessionKey key, Set<SessionTag> tagsForDelivery, SessionKey newKey, long paddedSize) { //_log.debug("Encrypting to a NEW session"); byte elgSrcData[] = new byte[SessionKey.KEYSIZE_BYTES+32+158]; System.arraycopy(key.getData(), 0, elgSrcData, 0, SessionKey.KEYSIZE_BYTES); // get both the preIV and the padding at once, then copy to the preIV array _context.random().nextBytes(elgSrcData, SessionKey.KEYSIZE_BYTES, 32 + 158); byte preIV[] = SimpleByteCache.acquire(32); System.arraycopy(elgSrcData, SessionKey.KEYSIZE_BYTES, preIV, 0, 32); //_log.debug("Pre IV for encryptNewSession: " + DataHelper.toString(preIV, 32)); //_log.debug("SessionKey for encryptNewSession: " + DataHelper.toString(key.getData(), 32)); long before = _context.clock().now(); byte elgEncr[] = _context.elGamalEngine().encrypt(elgSrcData, target); if (_log.shouldLog(Log.INFO)) { long after = _context.clock().now(); _log.info("elgEngine.encrypt of the session key took " + (after - before) + "ms"); } if (elgEncr.length < 514) { // ??? ElGamalEngine.encrypt() always returns 514 bytes byte elg[] = new byte[514]; int diff = elg.length - elgEncr.length; //if (_log.shouldLog(Log.DEBUG)) _log.debug("Difference in size: " + diff); System.arraycopy(elgEncr, 0, elg, diff, elgEncr.length); elgEncr = elg; } //_log.debug("ElGamal encrypted length: " + elgEncr.length + " elGamal source length: " + elgSrc.toByteArray().length); // should we also feed the encrypted elG block into the harvester? // use alternate calculateHash() method to avoid object churn and caching //Hash ivHash = _context.sha().calculateHash(preIV); //byte iv[] = new byte[16]; //System.arraycopy(ivHash.getData(), 0, iv, 0, 16); byte[] iv = halfHash(preIV); SimpleByteCache.release(preIV); byte aesEncr[] = encryptAESBlock(data, key, iv, tagsForDelivery, newKey, paddedSize); SimpleByteCache.release(iv); //_log.debug("AES encrypted length: " + aesEncr.length); byte rv[] = new byte[elgEncr.length + aesEncr.length]; System.arraycopy(elgEncr, 0, rv, 0, elgEncr.length); System.arraycopy(aesEncr, 0, rv, elgEncr.length, aesEncr.length); //_log.debug("Return length: " + rv.length); //long finish = _context.clock().now(); //if (_log.shouldLog(Log.DEBUG)) // _log.debug("after the elgEngine.encrypt took a total of " + (finish - after) + "ms"); return rv; } /** * scenario 2: * Begin with 32 byte session tag, which also serves as the preIV. * Then encrypt with AES using that session key and the first 16 bytes of the SHA256 of the pre-IV: * <pre> * - 2 byte integer specifying the # of session tags * - that many 32 byte session tags * - 4 byte integer specifying data.length * - SHA256 of data * - 1 byte flag that, if == 1, is followed by a new SessionKey * - data * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 * </pre> * * @param target unused, this is AES encrypt only using the session key and tag * @param tagsForDelivery session tags to be associated with the key or null; * 200 max enforced at receiver */ private byte[] encryptExistingSession(byte data[], PublicKey target, SessionKey key, Set<SessionTag> tagsForDelivery, SessionTag currentTag, SessionKey newKey, long paddedSize) { //_log.debug("Encrypting to an EXISTING session"); byte rawTag[] = currentTag.getData(); //_log.debug("Pre IV for encryptExistingSession (aka tag): " + currentTag.toString()); //_log.debug("SessionKey for encryptNewSession: " + DataHelper.toString(key.getData(), 32)); // use alternate calculateHash() method to avoid object churn and caching //Hash ivHash = _context.sha().calculateHash(rawTag); //byte iv[] = new byte[16]; //System.arraycopy(ivHash.getData(), 0, iv, 0, 16); byte[] iv = halfHash(rawTag); byte aesEncr[] = encryptAESBlock(data, key, iv, tagsForDelivery, newKey, paddedSize, SessionTag.BYTE_LENGTH); SimpleByteCache.release(iv); // that prepended SessionTag.BYTE_LENGTH bytes at the beginning of the buffer System.arraycopy(rawTag, 0, aesEncr, 0, rawTag.length); return aesEncr; } /** * Generate the first 16 bytes of the SHA-256 hash of the data. * * Here we are careful to use the SHA256Generator method that does not * generate a Hash object or cache the result. * * @param preIV the 32 byte pre-IV. Caller should call SimpleByteCache.release(data) after use. * @return a 16 byte array. Caller should call SimpleByteCache.release(rv) after use. * @since 0.8.9 */ private byte[] halfHash(byte[] preIV) { byte[] ivHash = SimpleByteCache.acquire(32); _context.sha().calculateHash(preIV, 0, 32, ivHash, 0); byte iv[] = SimpleByteCache.acquire(16); System.arraycopy(ivHash, 0, iv, 0, 16); SimpleByteCache.release(ivHash); return iv; } /** * For both scenarios, this method encrypts the AES area using the given key, iv * and making sure the resulting data is at least as long as the paddedSize and * also mod 16 bytes. The contents of the encrypted data is: * <pre> * - 2 byte integer specifying the # of session tags * - that many 32 byte session tags * - 4 byte integer specifying data.length * - SHA256 of data * - 1 byte flag that, if == 1, is followed by a new SessionKey * - data * - random bytes, padding the total size to greater than paddedSize with a mod 16 = 0 * </pre> * * Note: package private for ElGamalTest.testAES() * * @param tagsForDelivery session tags to be associated with the key or null; * 200 max enforced at receiver */ final byte[] encryptAESBlock(byte data[], SessionKey key, byte[] iv, Set<SessionTag> tagsForDelivery, SessionKey newKey, long paddedSize) { return encryptAESBlock(data, key, iv, tagsForDelivery, newKey, paddedSize, 0); } /** * * @param tagsForDelivery session tags to be associated with the key or null; * 200 max enforced at receiver */ private final byte[] encryptAESBlock(byte data[], SessionKey key, byte[] iv, Set<SessionTag> tagsForDelivery, SessionKey newKey, long paddedSize, int prefixBytes) { //_log.debug("iv for encryption: " + DataHelper.toString(iv, 16)); //_log.debug("Encrypting AES"); if (tagsForDelivery == null) tagsForDelivery = Collections.emptySet(); int size = 2 // sizeof(tags) + SessionTag.BYTE_LENGTH*tagsForDelivery.size() + 4 // payload length + Hash.HASH_LENGTH + (newKey == null ? 1 : 1 + SessionKey.KEYSIZE_BYTES) + data.length; int totalSize = size + getPaddingSize(size, paddedSize); byte aesData[] = new byte[totalSize + prefixBytes]; int cur = prefixBytes; DataHelper.toLong(aesData, cur, 2, tagsForDelivery.size()); cur += 2; for (SessionTag tag : tagsForDelivery) { System.arraycopy(tag.getData(), 0, aesData, cur, SessionTag.BYTE_LENGTH); cur += SessionTag.BYTE_LENGTH; } //_log.debug("# tags created, registered, and written: " + tagsForDelivery.size()); DataHelper.toLong(aesData, cur, 4, data.length); cur += 4; //_log.debug("data length: " + data.length); // use alternate calculateHash() method to avoid object churn and caching //Hash hash = _context.sha().calculateHash(data); //System.arraycopy(hash.getData(), 0, aesData, cur, Hash.HASH_LENGTH); _context.sha().calculateHash(data, 0, data.length, aesData, cur); cur += Hash.HASH_LENGTH; //_log.debug("hash of data: " + DataHelper.toString(hash.getData(), 32)); if (newKey == null) { aesData[cur++] = 0x00; // don't rekey //_log.debug("flag written"); } else { aesData[cur++] = 0x01; // rekey System.arraycopy(newKey.getData(), 0, aesData, cur, SessionKey.KEYSIZE_BYTES); cur += SessionKey.KEYSIZE_BYTES; } System.arraycopy(data, 0, aesData, cur, data.length); cur += data.length; //_log.debug("raw data written: " + len); byte padding[] = getPadding(_context, size, paddedSize); //_log.debug("padding length: " + padding.length); System.arraycopy(padding, 0, aesData, cur, padding.length); cur += padding.length; //Hash h = _context.sha().calculateHash(data); //_log.debug("Hash of entire aes block before encryption: (len=" + data.length + ")\n" + DataHelper.toString(h.getData(), 32)); _context.aes().encrypt(aesData, prefixBytes, aesData, prefixBytes, key, iv, aesData.length - prefixBytes); //_log.debug("Encrypted length: " + aesEncr.length); //return aesEncr; return aesData; } /** * Return random bytes for padding the data to a mod 16 size so that it is * at least minPaddedSize * */ final static byte[] getPadding(I2PAppContext context, int curSize, long minPaddedSize) { int size = getPaddingSize(curSize, minPaddedSize); byte rv[] = new byte[size]; context.random().nextBytes(rv); return rv; } final static int getPaddingSize(int curSize, long minPaddedSize) { int diff = 0; if (curSize < minPaddedSize) { diff = (int) minPaddedSize - curSize; } int numPadding = diff; if (((curSize + diff) % 16) != 0) numPadding += (16 - ((curSize + diff) % 16)); return numPadding; } /**** public static void main(String args[]) { I2PAppContext ctx = new I2PAppContext(); ElGamalAESEngine e = new ElGamalAESEngine(ctx); Object kp[] = ctx.keyGenerator().generatePKIKeypair(); PublicKey pubKey = (PublicKey)kp[0]; PrivateKey privKey = (PrivateKey)kp[1]; SessionKey sessionKey = ctx.keyGenerator().generateSessionKey(); for (int i = 0; i < 10; i++) { try { Set tags = new HashSet(5); if (i == 0) { for (int j = 0; j < 5; j++) tags.add(new SessionTag(true)); } byte encrypted[] = e.encrypt("blah".getBytes(), pubKey, sessionKey, tags, 1024); byte decrypted[] = e.decrypt(encrypted, privKey); if ("blah".equals(new String(decrypted))) { System.out.println("equal on " + i); } else { System.out.println("NOT equal on " + i + ": " + new String(decrypted)); break; } ctx.sessionKeyManager().tagsDelivered(pubKey, sessionKey, tags); } catch (Exception ee) { ee.printStackTrace(); break; } } } ****/ }