Ratchet (proposal 144):

- Randomize high two bits of Elligator2 encoding (incompatible change)
- Fix NPE in RatchetTagSet.toString()
- Use zeros for padding block
- Add more debug logging
This commit is contained in:
zzz
2020-01-21 17:54:14 +00:00
parent 50c86147b0
commit 064e4046a6
3 changed files with 86 additions and 14 deletions

View File

@ -242,6 +242,8 @@ public final class ECIESAEADEngine {
state.getLocalKeyPair().setPublicKey(targetPrivateKey.toPublic().getData(), 0); state.getLocalKeyPair().setPublicKey(targetPrivateKey.toPublic().getData(), 0);
state.getLocalKeyPair().setPrivateKey(targetPrivateKey.getData(), 0); state.getLocalKeyPair().setPrivateKey(targetPrivateKey.getData(), 0);
state.start(); state.start();
if (_log.shouldDebug())
_log.debug("State before decrypt new session: " + state);
// Elg2 // Elg2
byte[] tmp = new byte[KEYLEN]; byte[] tmp = new byte[KEYLEN];
@ -269,8 +271,10 @@ public final class ECIESAEADEngine {
byte[] bobPK = new byte[KEYLEN]; byte[] bobPK = new byte[KEYLEN];
state.getRemotePublicKey().getPublicKey(bobPK, 0); state.getRemotePublicKey().getPublicKey(bobPK, 0);
if (_log.shouldDebug()) if (_log.shouldDebug()) {
_log.debug("NS decrypt success from PK " + Base64.encode(bobPK)); _log.debug("NS decrypt success from PK " + Base64.encode(bobPK));
_log.debug("State after decrypt new session: " + state);
}
if (Arrays.equals(bobPK, NULLPK)) { if (Arrays.equals(bobPK, NULLPK)) {
// TODO // TODO
if (_log.shouldWarn()) if (_log.shouldWarn())
@ -351,8 +355,12 @@ public final class ECIESAEADEngine {
_log.warn("Elg2 decode fail NSR"); _log.warn("Elg2 decode fail NSR");
return null; return null;
} }
if (_log.shouldDebug())
_log.debug("State before decrypt new session reply: " + state);
System.arraycopy(k.getData(), 0, data, TAGLEN, KEYLEN); System.arraycopy(k.getData(), 0, data, TAGLEN, KEYLEN);
state.mixHash(tag, 0, TAGLEN); state.mixHash(tag, 0, TAGLEN);
if (_log.shouldDebug())
_log.debug("State after mixhash tag before decrypt new session reply: " + state);
try { try {
state.readMessage(data, 8, 48, ZEROLEN, 0); state.readMessage(data, 8, 48, ZEROLEN, 0);
} catch (GeneralSecurityException gse) { } catch (GeneralSecurityException gse) {
@ -363,6 +371,8 @@ public final class ECIESAEADEngine {
} }
return null; return null;
} }
if (_log.shouldDebug())
_log.debug("State after decrypt new session reply: " + state);
// split() // split()
byte[] ck = state.getChainingKey(); byte[] ck = state.getChainingKey();
@ -622,6 +632,8 @@ public final class ECIESAEADEngine {
state.getLocalKeyPair().setPublicKey(priv.toPublic().getData(), 0); state.getLocalKeyPair().setPublicKey(priv.toPublic().getData(), 0);
state.getLocalKeyPair().setPrivateKey(priv.getData(), 0); state.getLocalKeyPair().setPrivateKey(priv.getData(), 0);
state.start(); state.start();
if (_log.shouldDebug())
_log.debug("State before encrypt new session: " + state);
byte[] payload = createPayload(cloves, cloves.getExpiration()); byte[] payload = createPayload(cloves, cloves.getExpiration());
@ -633,6 +645,8 @@ public final class ECIESAEADEngine {
_log.warn("Encrypt fail NS", gse); _log.warn("Encrypt fail NS", gse);
return null; return null;
} }
if (_log.shouldDebug())
_log.debug("State after encrypt new session: " + state);
// overwrite eph. key with encoded key // overwrite eph. key with encoded key
DHState eph = state.getLocalEphemeralKeyPair(); DHState eph = state.getLocalEphemeralKeyPair();
@ -642,6 +656,8 @@ public final class ECIESAEADEngine {
return null; return null;
} }
eph.getEncodedPublicKey(enc, 0); eph.getEncodedPublicKey(enc, 0);
if (_log.shouldDebug())
_log.debug("Elligator2 encoded eph. key: " + Base64.encode(enc, 0, 32));
// tell the SKM // tell the SKM
keyManager.createSession(target, state); keyManager.createSession(target, state);
@ -669,8 +685,12 @@ public final class ECIESAEADEngine {
*/ */
private byte[] encryptNewSessionReply(CloveSet cloves, PublicKey target, HandshakeState state, private byte[] encryptNewSessionReply(CloveSet cloves, PublicKey target, HandshakeState state,
RatchetSessionTag currentTag, RatchetSKM keyManager) { RatchetSessionTag currentTag, RatchetSKM keyManager) {
if (_log.shouldDebug())
_log.debug("State before encrypt new session reply: " + state);
byte[] tag = currentTag.getData(); byte[] tag = currentTag.getData();
state.mixHash(tag, 0, TAGLEN); state.mixHash(tag, 0, TAGLEN);
if (_log.shouldDebug())
_log.debug("State after mixhash tag before encrypt new session reply: " + state);
byte[] payload = createPayload(cloves, 0); byte[] payload = createPayload(cloves, 0);
@ -684,6 +704,8 @@ public final class ECIESAEADEngine {
_log.warn("Encrypt fail NSR part 1", gse); _log.warn("Encrypt fail NSR part 1", gse);
return null; return null;
} }
if (_log.shouldDebug())
_log.debug("State after encrypt new session reply: " + state);
// overwrite eph. key with encoded key // overwrite eph. key with encoded key
DHState eph = state.getLocalEphemeralKeyPair(); DHState eph = state.getLocalEphemeralKeyPair();
@ -847,7 +869,10 @@ public final class ECIESAEADEngine {
len += block.getTotalLength(); len += block.getTotalLength();
} }
int padlen = 1 + _context.random().nextInt(MAXPAD); int padlen = 1 + _context.random().nextInt(MAXPAD);
Block block = new PaddingBlock(_context, padlen); // random data
//Block block = new PaddingBlock(_context, padlen);
// zeros
Block block = new PaddingBlock(padlen);
blocks.add(block); blocks.add(block);
len += block.getTotalLength(); len += block.getTotalLength();
byte[] payload = new byte[len]; byte[] payload = new byte[len];

View File

@ -82,21 +82,25 @@ class Elligator2 {
} }
/** /**
* From javascript version documentation: * Use for on-the-wire. Don't use for unit tests as output will be randomized
* * based on the 'alternative' and the high bits.
* The algorithm can return two different values for a single x coordinate if it's not 0. * There are eight possible encodings for any point.
* Which one to return is determined by y coordinate. * Output will look like 256 random bits.
* Since Curve25519 doesn't use y due to optimizations, you should specify a Boolean value
* as the second argument of the function.
* It should be unpredictable, because it's recoverable from the representative.
* *
* @return "representative", little endian or null on failure * @return "representative", little endian or null on failure
*/ */
public byte[] encode(PublicKey point) { public byte[] encode(PublicKey point) {
return encode(point, _context.random().nextBoolean()); byte[] random = new byte[1];
_context.random().nextBytes(random);
byte rand = random[0];
return encode(point, (rand & 0x01) == 0, rand);
} }
/** /**
* Use for unit tests. Don't use for on-the-wire; use one-arg version.
* Output will look like 254 random bits.
* High two bits of rv[31] will be zero.
*
* From javascript version documentation: * From javascript version documentation:
* *
* The algorithm can return two different values for a single x coordinate if it's not 0. * The algorithm can return two different values for a single x coordinate if it's not 0.
@ -107,7 +111,26 @@ class Elligator2 {
* *
* @return "representative", little endian or null on failure * @return "representative", little endian or null on failure
*/ */
public static byte[] encode(PublicKey point, boolean alternative) { protected static byte[] encode(PublicKey point, boolean alternative) {
return encode(point, alternative, (byte) 0);
}
/**
* Output will look like 254 random bits. High two bits of highBits will be ORed in.
*
* From javascript version documentation:
*
* The algorithm can return two different values for a single x coordinate if it's not 0.
* Which one to return is determined by y coordinate.
* Since Curve25519 doesn't use y due to optimizations, you should specify a Boolean value
* as the second argument of the function.
* It should be unpredictable, because it's recoverable from the representative.
*
* @param highBits High two bits will be ORed into rv[31]
* @return "representative", little endian or null on failure
* @since 0.9.45 to add highBits arg
*/
private static byte[] encode(PublicKey point, boolean alternative, byte highBits) {
if (DISABLE) if (DISABLE)
return point.getData(); return point.getData();
@ -152,6 +175,8 @@ class Elligator2 {
// little endian // little endian
byte[] rv = ENCODING.encode(r); byte[] rv = ENCODING.encode(r);
// randomize two high bits
rv[REPRESENTATIVE_LENGTH - 1] |= highBits & (byte) 0xc0;
return rv; return rv;
} }
@ -162,6 +187,7 @@ class Elligator2 {
* It's also able to return null if the representative is invalid (there are only 10 invalid representatives). * It's also able to return null if the representative is invalid (there are only 10 invalid representatives).
* *
* @param representative the encoded data, little endian, 32 bytes * @param representative the encoded data, little endian, 32 bytes
* WILL BE MODIFIED by masking byte 31
* @return x or null on failure * @return x or null on failure
*/ */
public static PublicKey decode(byte[] representative) { public static PublicKey decode(byte[] representative) {
@ -175,7 +201,8 @@ class Elligator2 {
* It's also able to return null if the representative is invalid (there are only 10 invalid representatives). * It's also able to return null if the representative is invalid (there are only 10 invalid representatives).
* *
* @param alternative out parameter, or null if you don't care * @param alternative out parameter, or null if you don't care
* @param representative the encoded data, little endian, 32 bytes * @param representative the encoded data, little endian, 32 bytes;
* WILL BE MODIFIED by masking byte 31
* @return x or null on failure * @return x or null on failure
*/ */
public static PublicKey decode(AtomicBoolean alternative, byte[] representative) { public static PublicKey decode(AtomicBoolean alternative, byte[] representative) {
@ -185,6 +212,8 @@ class Elligator2 {
return new PublicKey(EncType.ECIES_X25519, representative); return new PublicKey(EncType.ECIES_X25519, representative);
// r // r
// Mask out two high bits, to get valid 254 bits.
representative[REPRESENTATIVE_LENGTH - 1] &= (byte) 0x3f;
BigInteger r = ENCODING.toBigInteger(representative); BigInteger r = ENCODING.toBigInteger(representative);
// If r >= (p - 1) / 2 // If r >= (p - 1) / 2
@ -313,6 +342,7 @@ class Elligator2 {
//00000010 d8 fa ec 68 e5 e6 7e f4 5e bb 82 ee ba 52 60 4f |...h..~.^....R`O| //00000010 d8 fa ec 68 e5 e6 7e f4 5e bb 82 ee ba 52 60 4f |...h..~.^....R`O|
I2PAppContext ctx = I2PAppContext.getGlobalContext(); I2PAppContext ctx = I2PAppContext.getGlobalContext();
Elligator2 elg2 = new Elligator2(ctx);
X25519KeyFactory xkf = new X25519KeyFactory(ctx); X25519KeyFactory xkf = new X25519KeyFactory(ctx);
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
PublicKey pub; PublicKey pub;
@ -322,9 +352,10 @@ class Elligator2 {
System.out.println("Trying encode " + ++j); System.out.println("Trying encode " + ++j);
KeyPair kp = xkf.getKeys(); KeyPair kp = xkf.getKeys();
pub = kp.getPublic(); pub = kp.getPublic();
enc = encode(pub, ctx.random().nextBoolean()); enc = elg2.encode(pub);
} while (enc == null); } while (enc == null);
PublicKey pub2 = decode(null, enc); System.out.println("Encoded:\n" + HexDump.dump(enc));
PublicKey pub2 = decode(enc);
if (pub2 == null) { if (pub2 == null) {
System.out.println("Decode FAIL"); System.out.println("Decode FAIL");
continue; continue;
@ -336,6 +367,20 @@ class Elligator2 {
System.out.println("calc: " + pub2.toBase64()); System.out.println("calc: " + pub2.toBase64());
} }
} }
System.out.println("Random decode test");
byte[] enc = new byte[32];
int fails = 0;
for (int i = 0; i < 1000; i++) {
ctx.random().nextBytes(enc);
pk = decode(enc);
if (pk == null)
fails++;
}
if (fails > 0)
System.out.println("FAIL decode " + fails + " / 1000");
else
System.out.println("PASS");
} }
****/ ****/
} }

View File

@ -439,6 +439,8 @@ class RatchetTagSet implements TagSetHandle {
for (int i = 0; i < sz; i++) { for (int i = 0; i < sz; i++) {
int n = _sessionTags.keyAt(i); int n = _sessionTags.keyAt(i);
RatchetSessionTag tag = _sessionTags.valueAt(i); RatchetSessionTag tag = _sessionTags.valueAt(i);
if (tag == null)
continue;
buf.append("\n " + n + '\t' + Base64.encode(tag.getData())); buf.append("\n " + n + '\t' + Base64.encode(tag.getData()));
if (_sessionKeys != null) { if (_sessionKeys != null) {
byte[] key = _sessionKeys.get(n); byte[] key = _sessionKeys.get(n);