forked from I2P_Developers/i2p.i2p
Router: Move ElGamalAESEngine from core to router
Client end-to-end crypto removed 13 years ago Not used by any client, app, or plugin.
This commit is contained in:
@ -15,6 +15,7 @@ import net.i2p.data.router.RouterInfo;
|
||||
import net.i2p.data.router.RouterKeyGenerator;
|
||||
import net.i2p.internal.InternalClientManager;
|
||||
import net.i2p.router.client.ClientManagerFacadeImpl;
|
||||
import net.i2p.router.crypto.ElGamalAESEngine;
|
||||
import net.i2p.router.crypto.TransientSessionKeyManager;
|
||||
import net.i2p.router.dummy.*;
|
||||
import net.i2p.router.message.GarlicMessageParser;
|
||||
@ -68,6 +69,7 @@ public class RouterContext extends I2PAppContext {
|
||||
private RouterAppManager _appManager;
|
||||
private RouterKeyGenerator _routingKeyGenerator;
|
||||
private GarlicMessageParser _garlicMessageParser;
|
||||
private ElGamalAESEngine _elGamalAESEngine;
|
||||
private final Set<Runnable> _finalShutdownTasks;
|
||||
// split up big lock on this to avoid deadlocks
|
||||
private volatile boolean _initialized;
|
||||
@ -211,6 +213,7 @@ public class RouterContext extends I2PAppContext {
|
||||
_clientManagerFacade = new DummyClientManagerFacade(this);
|
||||
// internal client manager is null
|
||||
}
|
||||
_elGamalAESEngine = new ElGamalAESEngine(this);
|
||||
_garlicMessageParser = new GarlicMessageParser(this);
|
||||
_clientMessagePool = new ClientMessagePool(this);
|
||||
_jobQueue = new JobQueue(this);
|
||||
@ -667,4 +670,16 @@ public class RouterContext extends I2PAppContext {
|
||||
public GarlicMessageParser garlicMessageParser() {
|
||||
return _garlicMessageParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the ElGamal/AES+SessionTag engine for this context. The algorithm
|
||||
* makes use of the context's sessionKeyManager to coordinate transparent
|
||||
* access to the sessionKeys and sessionTags, as well as the context's elGamal
|
||||
* engine (which in turn keeps stats, etc).
|
||||
*
|
||||
* @since 0.9.38 moved from superclass (app context)
|
||||
*/
|
||||
public ElGamalAESEngine elGamalAESEngine() {
|
||||
return _elGamalAESEngine;
|
||||
}
|
||||
}
|
||||
|
735
router/java/src/net/i2p/router/crypto/ElGamalAESEngine.java
Normal file
735
router/java/src/net/i2p/router/crypto/ElGamalAESEngine.java
Normal file
@ -0,0 +1,735 @@
|
||||
package net.i2p.router.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.crypto.AESEngine;
|
||||
import net.i2p.crypto.SessionKeyManager;
|
||||
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.
|
||||
*
|
||||
* @since 0.9.38 moved from net.i2p.crypto
|
||||
*/
|
||||
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[];
|
||||
boolean wasExisting = false;
|
||||
final boolean shouldDebug = _log.shouldDebug();
|
||||
if (key != null) {
|
||||
//if (_log.shouldLog(Log.DEBUG)) _log.debug("Key is known for tag " + st);
|
||||
if (shouldDebug)
|
||||
_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() && shouldDebug)
|
||||
_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 (shouldDebug) _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() && shouldDebug)
|
||||
_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 (shouldDebug)
|
||||
_log.debug("Found key: " + foundKey.toBase64() + " tags: " + foundTags + " wasExisting? " + wasExisting);
|
||||
keyManager.tagsReceived(foundKey, foundTags);
|
||||
} else if (usedKey.getData() != null) {
|
||||
if (shouldDebug)
|
||||
_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 + AESEngine.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[] = AESEngine.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;
|
||||
}
|
||||
|
||||
|
||||
/****
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
****/
|
||||
}
|
@ -10,7 +10,6 @@ package net.i2p.router.message;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.crypto.SessionKeyManager;
|
||||
import net.i2p.data.Certificate;
|
||||
import net.i2p.data.DataFormatException;
|
||||
@ -18,6 +17,7 @@ import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.PrivateKey;
|
||||
import net.i2p.data.i2np.GarlicClove;
|
||||
import net.i2p.data.i2np.GarlicMessage;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
@ -27,7 +27,7 @@ import net.i2p.util.Log;
|
||||
*/
|
||||
public class GarlicMessageParser {
|
||||
private final Log _log;
|
||||
private final I2PAppContext _context;
|
||||
private final RouterContext _context;
|
||||
|
||||
/**
|
||||
* Huge limit just to reduce chance of trouble. Typ. usage is 3.
|
||||
@ -35,7 +35,7 @@ public class GarlicMessageParser {
|
||||
*/
|
||||
private static final int MAX_CLOVES = 32;
|
||||
|
||||
public GarlicMessageParser(I2PAppContext context) {
|
||||
public GarlicMessageParser(RouterContext context) {
|
||||
_context = context;
|
||||
_log = _context.logManager().getLog(GarlicMessageParser.class);
|
||||
}
|
||||
|
Reference in New Issue
Block a user