Ratchet: Improve muxed decrypt

Try tags for both ratchet and AES before DH for either
Return empty CloveSet for ratchet errors after successful decrypt
Don't corrupt data in ECIESEngine on NS/NSR failure, for subsequent ElG attempt
Log tweaks
This commit is contained in:
zzz
2020-04-14 12:13:00 +00:00
parent 689b26102b
commit e2cc62a21f
4 changed files with 283 additions and 69 deletions

View File

@ -114,7 +114,7 @@ public final class ElGamalAESEngine {
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) */ );
_log.debug("Decrypting existing session with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes ");
decrypted = decryptExistingSession(data, key, targetPrivateKey, foundTags, usedKey, foundKey);
if (decrypted != null) {
@ -128,8 +128,7 @@ public final class ElGamalAESEngine {
_log.warn("ElG decrypt fail: known tag [" + st + "], failed decrypt");
}
}
} else {
if (shouldDebug) _log.debug("Key is NOT known for tag " + st);
} else if (data.length >= ELG_ENCRYPTED_LENGTH) {
decrypted = decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey);
if (decrypted != null) {
_context.statManager().updateFrequency("crypto.elGamalAES.decryptNewSession");
@ -140,6 +139,8 @@ public final class ElGamalAESEngine {
if (_log.shouldLog(Log.WARN))
_log.warn("ElG decrypt fail: unknown tag: " + st);
}
} else {
return null;
}
//if ((key == null) && (decrypted == null)) {
@ -160,6 +161,95 @@ public final class ElGamalAESEngine {
return decrypted;
}
/**
* Tags only. For MuxedEngine use only.
*
* @return decrypted data or null on failure
* @since 0.9.46
*/
public byte[] decryptFast(byte data[], PrivateKey targetPrivateKey,
SessionKeyManager keyManager) throws DataFormatException {
if (data == null)
return null;
if (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);
if (key == null)
return null;
SessionKey foundKey = new SessionKey();
SessionKey usedKey = new SessionKey();
Set<SessionTag> foundTags = new HashSet<SessionTag>();
final boolean shouldDebug = _log.shouldDebug();
if (shouldDebug)
_log.debug("Decrypting existing session with tag: " + st.toString() + ": key: " + key.toBase64() + ": " + data.length + " bytes");
byte[] 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);
if (!foundTags.isEmpty()) {
if (foundKey.getData() != null) {
if (shouldDebug)
_log.debug("Found key: " + foundKey.toBase64() + " tags: " + foundTags + " in existing session");
keyManager.tagsReceived(foundKey, foundTags);
} else if (usedKey.getData() != null) {
if (shouldDebug)
_log.debug("Used key: " + usedKey.toBase64() + " tags: " + foundTags + " in existing session");
keyManager.tagsReceived(usedKey, foundTags);
}
}
} else {
_context.statManager().updateFrequency("crypto.elGamalAES.decryptFailed");
if (_log.shouldLog(Log.WARN)) {
_log.warn("ElG decrypt fail: known tag [" + st + "], failed decrypt");
}
}
return decrypted;
}
/**
* Full ElG only. For MuxedEngine use only.
*
* @return decrypted data or null on failure
* @since 0.9.46
*/
public byte[] decryptSlow(byte data[], PrivateKey targetPrivateKey,
SessionKeyManager keyManager) throws DataFormatException {
if (data == null)
return null;
if (data.length < ELG_ENCRYPTED_LENGTH)
return null;
SessionKey foundKey = new SessionKey();
SessionKey usedKey = new SessionKey();
Set<SessionTag> foundTags = new HashSet<SessionTag>();
byte[] decrypted = decryptNewSession(data, targetPrivateKey, foundTags, usedKey, foundKey);
final boolean shouldDebug = _log.shouldDebug();
if (decrypted != null) {
_context.statManager().updateFrequency("crypto.elGamalAES.decryptNewSession");
} else {
_context.statManager().updateFrequency("crypto.elGamalAES.decryptFailed");
if (_log.shouldLog(Log.WARN))
_log.warn("ElG decrypt fail as new session");
}
if (!foundTags.isEmpty()) {
if (shouldDebug)
_log.debug("ElG decrypt success: found tags: " + foundTags);
if (foundKey.getData() != null) {
if (shouldDebug)
_log.debug("Found key: " + foundKey.toBase64() + " tags: " + foundTags + " in new session");
keyManager.tagsReceived(foundKey, foundTags);
} else if (usedKey.getData() != null) {
if (shouldDebug)
_log.debug("Used key: " + usedKey.toBase64() + " tags: " + foundTags + " in new session");
keyManager.tagsReceived(usedKey, foundTags);
}
}
return decrypted;
}
/**
* scenario 1:
* Begin with 222 bytes, ElG encrypted, containing:
@ -180,13 +270,6 @@ public final class ElGamalAESEngine {
*/
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 < ELG_ENCRYPTED_LENGTH) {
//if (_log.shouldLog(Log.WARN)) _log.warn("Data length is too small (" + data.length + ")");
return null;
}
byte elgEncr[] = new byte[ELG_ENCRYPTED_LENGTH];
if (data.length > ELG_ENCRYPTED_LENGTH) {
System.arraycopy(data, 0, elgEncr, 0, ELG_ENCRYPTED_LENGTH);
@ -423,8 +506,8 @@ public final class ElGamalAESEngine {
throw new IllegalArgumentException("Bad public key type " + type);
}
if (currentTag == null) {
if (_log.shouldLog(Log.INFO))
_log.info("Current tag is null, encrypting as new session");
if (_log.shouldDebug())
_log.debug("Encrypting as new session");
_context.statManager().updateFrequency("crypto.elGamalAES.encryptNewSession");
return encryptNewSession(data, target, key, tagsForDelivery, newKey, paddedSize);
}
@ -535,12 +618,12 @@ public final class ElGamalAESEngine {
//_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();
//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 (_log.shouldDebug()) {
// long after = _context.clock().now();
// _log.debug("elgEngine.encrypt of the session key took " + (after - before) + "ms");
//}
if (elgEncr.length < ELG_ENCRYPTED_LENGTH) {
// ??? ElGamalEngine.encrypt() always returns 514 bytes
byte elg[] = new byte[ELG_ENCRYPTED_LENGTH];

View File

@ -65,6 +65,10 @@ public final class ECIESAEADEngine {
private static final long MAX_NS_FUTURE = 2*60*1000;
// debug, send ACKREQ in every ES
private static final boolean ACKREQ_IN_ES = false;
// return value for a payload failure after a successful decrypt,
// so we don't continue with ElG
private static final GarlicClove[] NO_GARLIC = new GarlicClove[] {};
private static final CloveSet NO_CLOVES = new CloveSet(NO_GARLIC, Certificate.NULL_CERT, 0, 0);
private static final String INFO_0 = "SessionReplyTags";
private static final String INFO_6 = "AttachPayloadKDF";
@ -149,10 +153,10 @@ public final class ECIESAEADEngine {
} catch (DataFormatException dfe) {
if (_log.shouldWarn())
_log.warn("ECIES decrypt error", dfe);
throw dfe;
return NO_CLOVES;
} catch (Exception e) {
_log.error("ECIES decrypt error", e);
return null;
return NO_CLOVES;
}
}
@ -165,8 +169,8 @@ public final class ECIESAEADEngine {
return null;
}
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 + ")");
if (_log.shouldWarn())
_log.warn("Data is less than the minimum size (" + data.length + " < " + MIN_ENCRYPTED_SIZE + ")");
return null;
}
@ -177,31 +181,128 @@ public final class ECIESAEADEngine {
CloveSet decrypted;
final boolean shouldDebug = _log.shouldDebug();
if (key != null) {
HandshakeState state = key.getHandshakeState();
if (state == null) {
if (shouldDebug)
_log.debug("Decrypting ES with tag: " + st.toBase64() + " key: " + key.toBase64() + ": " + data.length + " bytes");
decrypted = decryptExistingSession(tag, data, key, targetPrivateKey, keyManager);
} else if (data.length >= MIN_NSR_SIZE) {
if (shouldDebug)
_log.debug("Decrypting NSR with tag: " + st.toBase64() + " key: " + key.toBase64() + ": " + data.length + " bytes");
decrypted = decryptNewSessionReply(tag, data, state, keyManager);
} else {
decrypted = null;
if (_log.shouldWarn())
_log.warn("ECIES decrypt fail, tag found but no state and too small for NSR: " + data.length + " bytes");
decrypted = xx_decryptFast(tag, st, key, data, targetPrivateKey, keyManager);
// we do NOT retry as NS
} else {
decrypted = x_decryptSlow(data, targetPrivateKey, keyManager);
}
return decrypted;
}
/**
* NSR/ES only. For MuxedEngine use only.
*
* @return decrypted data or null on failure
* @since 0.9.46
*/
CloveSet decryptFast(byte data[], PrivateKey targetPrivateKey,
RatchetSKM keyManager) throws DataFormatException {
try {
return x_decryptFast(data, targetPrivateKey, keyManager);
} catch (DataFormatException dfe) {
if (_log.shouldWarn())
_log.warn("ECIES decrypt error", dfe);
return NO_CLOVES;
} catch (Exception e) {
_log.error("ECIES decrypt error", e);
return NO_CLOVES;
}
}
/**
* NSR/ES only.
*
* @return decrypted data or null on failure
* @since 0.9.46
*/
private CloveSet x_decryptFast(byte data[], PrivateKey targetPrivateKey,
RatchetSKM keyManager) throws DataFormatException {
if (data.length < MIN_ENCRYPTED_SIZE) {
if (_log.shouldWarn())
_log.warn("Data is less than the minimum size (" + data.length + " < " + MIN_ENCRYPTED_SIZE + ")");
return null;
}
byte tag[] = new byte[TAGLEN];
System.arraycopy(data, 0, tag, 0, TAGLEN);
RatchetSessionTag st = new RatchetSessionTag(tag);
SessionKeyAndNonce key = keyManager.consumeTag(st);
CloveSet decrypted;
if (key != null) {
decrypted = xx_decryptFast(tag, st, key, data, targetPrivateKey, keyManager);
} else {
decrypted = null;
}
return decrypted;
}
/**
* NSR/ES only.
*
* @param key non-null
* @param data non-null
* @return decrypted data or null on failure
* @since 0.9.46
*/
private CloveSet xx_decryptFast(byte[] tag, RatchetSessionTag st, SessionKeyAndNonce key,
byte data[], PrivateKey targetPrivateKey,
RatchetSKM keyManager) throws DataFormatException {
CloveSet decrypted;
final boolean shouldDebug = _log.shouldDebug();
HandshakeState state = key.getHandshakeState();
if (state == null) {
if (shouldDebug)
_log.debug("Decrypting ES with tag: " + st.toBase64() + " key: " + key + ": " + data.length + " bytes");
decrypted = decryptExistingSession(tag, data, key, targetPrivateKey, keyManager);
} else if (data.length >= MIN_NSR_SIZE) {
if (shouldDebug)
_log.debug("Decrypting NSR with tag: " + st.toBase64() + " key: " + key + ": " + data.length + " bytes");
decrypted = decryptNewSessionReply(tag, data, state, keyManager);
} else {
decrypted = null;
if (_log.shouldWarn())
_log.warn("ECIES decrypt fail, tag found but no state and too small for NSR: " + data.length + " bytes");
}
if (decrypted != null) {
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptExistingSession");
} else {
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed");
if (_log.shouldWarn()) {
_log.warn("ECIES decrypt fail: known tag [" + st + "], failed decrypt with key " + key);
}
if (decrypted != null) {
///
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptExistingSession");
} else {
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptFailed");
if (_log.shouldWarn()) {
_log.warn("ECIES decrypt fail: known tag [" + st + "], failed decrypt");
}
}
} else if (data.length >= MIN_NS_SIZE) {
if (shouldDebug) _log.debug("IB Tag " + st + " not found, trying NS decrypt");
}
return decrypted;
}
/**
* NS only. For MuxedEngine use only.
*
* @return decrypted data or null on failure
* @since 0.9.46
*/
CloveSet decryptSlow(byte data[], PrivateKey targetPrivateKey,
RatchetSKM keyManager) throws DataFormatException {
try {
return x_decryptSlow(data, targetPrivateKey, keyManager);
} catch (DataFormatException dfe) {
if (_log.shouldWarn())
_log.warn("ECIES decrypt error", dfe);
return NO_CLOVES;
} catch (Exception e) {
_log.error("ECIES decrypt error", e);
return NO_CLOVES;
}
}
/**
* NS only.
*
* @return decrypted data or null on failure
* @since 0.9.46
*/
private CloveSet x_decryptSlow(byte data[], PrivateKey targetPrivateKey,
RatchetSKM keyManager) throws DataFormatException {
CloveSet decrypted;
if (data.length >= MIN_NS_SIZE) {
decrypted = decryptNewSession(data, targetPrivateKey, keyManager);
if (decrypted != null) {
_context.statManager().updateFrequency("crypto.eciesAEAD.decryptNewSession");
@ -213,9 +314,8 @@ public final class ECIESAEADEngine {
} else {
decrypted = null;
if (_log.shouldWarn())
_log.warn("ECIES decrypt fail, tag not found and too small for NS: " + data.length + " bytes");
_log.warn("ECIES decrypt fail, too small for NS: " + data.length + " bytes");
}
return decrypted;
}
@ -260,6 +360,7 @@ public final class ECIESAEADEngine {
_log.warn("Elg2 decode fail NS");
return null;
}
// rewrite in place, must restore below on failure
System.arraycopy(pk.getData(), 0, data, 0, KEYLEN);
int payloadlen = data.length - (KEYLEN + KEYLEN + MACLEN + MACLEN);
@ -272,6 +373,8 @@ public final class ECIESAEADEngine {
if (_log.shouldDebug())
_log.debug("State at failure: " + state);
}
// restore original data for subsequent ElG attempt
System.arraycopy(tmp, 0, data, 0, KEYLEN);
return null;
}
// bloom filter here based on ephemeral key
@ -281,7 +384,7 @@ public final class ECIESAEADEngine {
if (keyManager.isDuplicate(pk)) {
if (_log.shouldWarn())
_log.warn("Dup eph. key in IB NS: " + pk);
return null;
return NO_CLOVES;
}
byte[] bobPK = new byte[KEYLEN];
@ -294,14 +397,14 @@ public final class ECIESAEADEngine {
// TODO
if (_log.shouldWarn())
_log.warn("Zero static key in IB NS");
return null;
return NO_CLOVES;
}
// payload
if (payloadlen == 0) {
if (_log.shouldWarn())
_log.warn("Zero length payload in NS");
return null;
return NO_CLOVES;
}
PLCallback pc = new PLCallback();
try {
@ -317,7 +420,7 @@ public final class ECIESAEADEngine {
if (pc.datetime == 0) {
if (_log.shouldWarn())
_log.warn("No datetime block in IB NS");
return null;
return NO_CLOVES;
}
// tell the SKM
@ -378,6 +481,7 @@ public final class ECIESAEADEngine {
}
if (_log.shouldDebug())
_log.debug("State before decrypt new session reply: " + state);
// rewrite in place, must restore below on failure
System.arraycopy(k.getData(), 0, data, TAGLEN, KEYLEN);
state.mixHash(tag, 0, TAGLEN);
if (_log.shouldDebug())
@ -390,6 +494,9 @@ public final class ECIESAEADEngine {
if (_log.shouldDebug())
_log.debug("State at failure: " + state);
}
// restore original data for subsequent ElG attempt
// unlikely since we already matched the tag
System.arraycopy(yy, 0, data, TAGLEN, KEYLEN);
return null;
}
if (_log.shouldDebug())
@ -417,12 +524,12 @@ public final class ECIESAEADEngine {
if (_log.shouldDebug())
_log.debug("State at failure: " + state);
}
return null;
return NO_CLOVES;
}
if (payload.length == 0) {
if (_log.shouldWarn())
_log.warn("Zero length payload in NSR");
return null;
return NO_CLOVES;
}
PLCallback pc = new PLCallback();
try {
@ -443,7 +550,7 @@ public final class ECIESAEADEngine {
// TODO
if (_log.shouldWarn())
_log.warn("NSR reply to zero static key NS");
return null;
return NO_CLOVES;
}
// tell the SKM

View File

@ -34,30 +34,53 @@ final class MuxedEngine {
ecKey.getType() != EncType.ECIES_X25519)
throw new IllegalArgumentException();
CloveSet rv = null;
boolean tryElg = false;
// See proposal 144
if (data.length >= 128) {
int mod = data.length % 16;
if (mod == 0 || mod == 2)
tryElg = true;
}
// Always try ElG first, for now
if (tryElg) {
byte[] dec = _context.elGamalAESEngine().decrypt(data, elgKey, keyManager.getElgSKM());
// Try in-order from fastest to slowest
// Ratchet Tag
rv = _context.eciesEngine().decryptFast(data, ecKey, keyManager.getECSKM());
if (rv != null)
return rv;
if (_log.shouldDebug())
_log.debug("Ratchet tag not found");
// AES Tag
if (data.length >= 128 && (data.length & 0x0f) == 0) {
byte[] dec = _context.elGamalAESEngine().decryptFast(data, elgKey, keyManager.getElgSKM());
if (dec != null) {
try {
rv = _context.garlicMessageParser().readCloveSet(dec, 0);
if (rv == null && _log.shouldInfo())
_log.info("AES cloveset error");
} catch (DataFormatException dfe) {
if (_log.shouldInfo())
_log.info("ElG decrypt failed, trying ECIES", dfe);
_log.info("AES cloveset error", dfe);
}
return rv;
} else {
//if (_log.shouldDebug())
// _log.debug("ElG decrypt failed, trying ECIES");
if (_log.shouldDebug())
_log.debug("AES tag not found");
}
}
if (rv == null) {
rv = _context.eciesEngine().decrypt(data, ecKey, keyManager.getECSKM());
// Ratchet DH
rv = _context.eciesEngine().decryptSlow(data, ecKey, keyManager.getECSKM());
if (rv != null)
return rv;
if (_log.shouldDebug())
_log.debug("Ratchet NS decrypt failed");
// ElG DH
if (data.length >= 514 && (data.length & 0x0f) == 2) {
byte[] dec = _context.elGamalAESEngine().decryptSlow(data, elgKey, keyManager.getElgSKM());
if (dec != null) {
try {
rv = _context.garlicMessageParser().readCloveSet(dec, 0);
if (rv == null && _log.shouldInfo())
_log.info("ElG cloveset error");
} catch (DataFormatException dfe) {
if (_log.shouldInfo())
_log.info("ElG cloveset error", dfe);
}
} else {
if (_log.shouldInfo())
_log.info("ElG decrypt failed");
}
}
return rv;
}

View File

@ -86,6 +86,7 @@ class SessionKeyAndNonce extends SessionKey {
StringBuilder buf = new StringBuilder(64);
buf.append("[SessionKeyAndNonce: ");
buf.append(toBase64());
buf.append(_state != null ? " NSR" : " ES");
buf.append(" nonce: ").append(_nonce);
buf.append(']');
return buf.toString();