diff --git a/core/java/src/net/i2p/data/EncryptedLeaseSet.java b/core/java/src/net/i2p/data/EncryptedLeaseSet.java index 8b6ae045d..791478aab 100644 --- a/core/java/src/net/i2p/data/EncryptedLeaseSet.java +++ b/core/java/src/net/i2p/data/EncryptedLeaseSet.java @@ -5,15 +5,19 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Collections; import java.util.List; import net.i2p.I2PAppContext; import net.i2p.crypto.Blinding; import net.i2p.crypto.ChaCha20; import net.i2p.crypto.DSAEngine; +import net.i2p.crypto.EncType; import net.i2p.crypto.HKDF; +import net.i2p.crypto.KeyPair; import net.i2p.crypto.SHA256Generator; import net.i2p.crypto.SigType; +import net.i2p.crypto.x25519.X25519DH; import net.i2p.util.Clock; import net.i2p.util.Log; @@ -46,6 +50,12 @@ public class EncryptedLeaseSet extends LeaseSet2 { private static final byte[] SUBCREDENTIAL = DataHelper.getASCII("subcredential"); private static final String ELS2L1K = "ELS2_L1K"; private static final String ELS2L2K = "ELS2_L2K"; + private static final String ELS2_DH = "ELS2_XCA"; + private static final String ELS2_PSK = "ELS2PSKA"; + private static final int IV_LEN = 12; + private static final int ID_LEN = 8; + private static final int COOKIE_LEN = 32; + private static final int CLIENT_LEN = ID_LEN + COOKIE_LEN; public EncryptedLeaseSet() { super(); @@ -351,6 +361,18 @@ public class EncryptedLeaseSet extends LeaseSet2 { */ @Override public void encrypt(SessionKey skey) { + encrypt(BlindData.AUTH_NONE, null); + } + + /** + * Throws IllegalStateException if not initialized. + * Ref: proposal 123 + * + * @param authType 0, 1, or 3, see BlindData + * @param clientKeys The client's X25519 public or private keys, null if unused + * @throws IllegalStateException + */ + public void encrypt(int authType, List clientKeys) { if (_encryptedData != null) throw new IllegalStateException("already encrypted"); if (_signature == null) @@ -373,32 +395,148 @@ public class EncryptedLeaseSet extends LeaseSet2 { _flags = saveFlags; } - I2PAppContext ctx = I2PAppContext.getGlobalContext(); - byte[] input = getHKDFInput(ctx); - // layer 2 (inner) encryption + I2PAppContext ctx = I2PAppContext.getGlobalContext(); byte[] salt = new byte[SALT_LEN]; ctx.random().nextBytes(salt); HKDF hkdf = new HKDF(ctx); byte[] key = new byte[32]; // use first 12 bytes only byte[] iv = new byte[32]; - hkdf.calculate(salt, input, ELS2L2K, key, iv, 0); + int authLen; + if (authType == BlindData.AUTH_NONE) { + authLen = 1; + } else if (authType == BlindData.AUTH_DH || + authType == BlindData.AUTH_PSK) { + if (clientKeys == null || clientKeys.isEmpty()) + throw new IllegalArgumentException("No client keys provided"); + authLen = 1 + SALT_LEN + 2 + (clientKeys.size() * CLIENT_LEN); + } else { + throw new IllegalArgumentException("Bad auth type " + authType); + } + byte[] authInput; + byte[] authcookie = null; + if (authType == BlindData.AUTH_NONE) { + authInput = getHKDFInput(ctx); + } else { + authcookie = new byte[32]; + ctx.random().nextBytes(authcookie); + if (_log.shouldDebug()) { + _log.debug("Auth Cookie:\n" + + net.i2p.util.HexDump.dump(authcookie)); + } + authInput = getHKDFInput(ctx, authcookie); + } + if (_log.shouldDebug()) { + _log.debug("Inner HKDF salt:\n" + + net.i2p.util.HexDump.dump(salt) + + "Inner HKDF input:\n" + + net.i2p.util.HexDump.dump(authInput)); + } + hkdf.calculate(salt, authInput, ELS2L2K, key, iv, 0); byte[] plaintext = baos.toByteArray(); - byte[] ciphertext = new byte[1 + SALT_LEN + plaintext.length]; - // Middle layer - flag - ciphertext[0] = 0; - System.arraycopy(salt, 0, ciphertext, 1, SALT_LEN); - ChaCha20.encrypt(key, iv, plaintext, 0, ciphertext, 1 + SALT_LEN, plaintext.length); + byte[] ciphertext = new byte[authLen + SALT_LEN + plaintext.length]; + + // Middle layer + if (authType == BlindData.AUTH_NONE) { + // Flag only + ciphertext[0] = BlindData.AUTH_NONE; + } else { + // Flag + ciphertext[0] = (byte) (authType & 0x0f); + if (clientKeys.size() > 1) + Collections.shuffle(clientKeys); + if (authType == BlindData.AUTH_DH) { + // DH + KeyPair encKeys = ctx.keyGenerator().generatePKIKeys(EncType.ECIES_X25519); + PrivateKey esk = encKeys.getPrivate(); + PublicKey epk = encKeys.getPublic(); + // HKDF input is 100 bytes + byte[] clientAuthInput = new byte[32 + authInput.length]; + // we copy over end of authInput from above + // subcredential and timestamp remain unchanged + System.arraycopy(authInput, 32, clientAuthInput, 64, 36); + // pubkey + System.arraycopy(epk.getData(), 0, ciphertext, 1, 32); + DataHelper.toLong(ciphertext, 33, 2, clientKeys.size()); + int off = 35; + byte[] clientKey = new byte[32]; + byte[] clientIVandID = new byte[32]; + for (SimpleDataStructure sds : clientKeys) { + if (!(sds instanceof PublicKey)) + throw new IllegalArgumentException("Bad DH client key type: " + sds); + PublicKey cpk = (PublicKey) sds; + if (cpk.getType() != EncType.ECIES_X25519) + throw new IllegalArgumentException("Bad DH client key type: " + cpk); + SessionKey dh = X25519DH.dh(esk, cpk); + System.arraycopy(dh.getData(), 0, clientAuthInput, 0, 32); + System.arraycopy(cpk.getData(), 0, clientAuthInput, 32, 32); + hkdf.calculate(epk.getData(), clientAuthInput, ELS2_DH, clientKey, clientIVandID, 0); + System.arraycopy(clientIVandID, IV_LEN, ciphertext, off, ID_LEN); + off += ID_LEN; + ChaCha20.encrypt(clientKey, clientIVandID, authcookie, 0, ciphertext, off, authcookie.length); + if (_log.shouldDebug()) { + _log.debug("Added client ID/enc.cookie:\n" + + net.i2p.util.HexDump.dump(clientIVandID, IV_LEN, ID_LEN) + + net.i2p.util.HexDump.dump(ciphertext, off, COOKIE_LEN) + + "for client key:\n" + + net.i2p.util.HexDump.dump(clientKey)); + } + off += COOKIE_LEN; + } + } else { + // PSK + // salt + byte[] authsalt = new byte[32]; + ctx.random().nextBytes(authsalt); + System.arraycopy(authsalt, 0, ciphertext, 1, 32); + DataHelper.toLong(ciphertext, 33, 2, clientKeys.size()); + int off = 35; + byte[] clientKey = new byte[32]; + byte[] clientIVandID = new byte[32]; + for (SimpleDataStructure sds : clientKeys) { + if (!(sds instanceof PrivateKey)) + throw new IllegalArgumentException("Bad DH client key type: " + sds); + PrivateKey csk = (PrivateKey) sds; + if (csk.getType() != EncType.ECIES_X25519) + throw new IllegalArgumentException("Bad PSK client key type: " + csk); + // HKDF input is 68 bytes ` + // we reuse authInput from above, just replace the first 32 bytes. + // subcredential and timestamp remain unchanged + System.arraycopy(csk.getData(), 0, authInput, 0, 32); + hkdf.calculate(authsalt, authInput, ELS2_PSK, clientKey, clientIVandID, 0); + System.arraycopy(clientIVandID, IV_LEN, ciphertext, off, ID_LEN); + off += ID_LEN; + ChaCha20.encrypt(clientKey, clientIVandID, authcookie, 0, ciphertext, off, authcookie.length); + if (_log.shouldDebug()) { + _log.debug("Added client ID/enc.cookie:\n" + + net.i2p.util.HexDump.dump(clientIVandID, IV_LEN, ID_LEN) + + net.i2p.util.HexDump.dump(ciphertext, off, COOKIE_LEN) + + "for client key:\n" + + net.i2p.util.HexDump.dump(clientKey)); + } + off += COOKIE_LEN; + } + } + } + System.arraycopy(salt, 0, ciphertext, authLen, SALT_LEN); + ChaCha20.encrypt(key, iv, plaintext, 0, ciphertext, authLen + SALT_LEN, plaintext.length); if (_log.shouldDebug()) { _log.debug("Encrypt: inner plaintext:\n" + net.i2p.util.HexDump.dump(plaintext)); _log.debug("Encrypt: inner ciphertext:\n" + net.i2p.util.HexDump.dump(ciphertext)); } // layer 1 (outer) encryption - // reuse input (because there's no authcookie), generate new salt/key/iv ctx.random().nextBytes(salt); - hkdf.calculate(salt, input, ELS2L1K, key, iv, 0); + if (authType == BlindData.AUTH_NONE) { + // reuse input (because there's no authcookie), generate new salt/key/iv + hkdf.calculate(salt, authInput, ELS2L1K, key, iv, 0); + } else { + // get just the subcredential and date + byte[] l1Input = new byte[36]; + System.arraycopy(authInput, 32, l1Input, 0, 36); + hkdf.calculate(salt, l1Input, ELS2L1K, key, iv, 0); + } plaintext = ciphertext; ciphertext = new byte[SALT_LEN + plaintext.length]; System.arraycopy(salt, 0, ciphertext, 0, SALT_LEN); @@ -415,16 +553,30 @@ public class EncryptedLeaseSet extends LeaseSet2 { /** * Throws IllegalStateException if not initialized. * - * @param skey ignored + * @param clientKey PrivateKey for DH or PSK, or null if none * @throws IllegalStateException */ - private void decrypt() throws DataFormatException, IOException { + private void decrypt(PrivateKey csk) throws DataFormatException, IOException { + try { + x_decrypt(csk); + } catch (IndexOutOfBoundsException ioobe) { + throw new DataFormatException("ioobe", ioobe); + } + } + + /** + * Throws IllegalStateException if not initialized. + * + * @param clientKey PrivateKey for DH or PSK, or null if none + * @throws IllegalStateException + */ + private void x_decrypt(PrivateKey csk) throws DataFormatException, IOException { if (_encryptedData == null) throw new IllegalStateException("not encrypted"); if (_decryptedLS2 != null) return; I2PAppContext ctx = I2PAppContext.getGlobalContext(); - byte[] input = getHKDFInput(ctx); + byte[] authInput = getHKDFInput(ctx); // layer 1 (outer) decryption HKDF hkdf = new HKDF(ctx); @@ -434,7 +586,7 @@ public class EncryptedLeaseSet extends LeaseSet2 { byte[] ciphertext = _encryptedData; byte[] plaintext = new byte[ciphertext.length - SALT_LEN]; // first 32 bytes of ciphertext are the salt - hkdf.calculate(ciphertext, input, ELS2L1K, key, iv, 0); + hkdf.calculate(ciphertext, authInput, ELS2L1K, key, iv, 0); if (_log.shouldDebug()) { _log.debug("Decrypt: chacha20 key:\n" + net.i2p.util.HexDump.dump(key)); _log.debug("Decrypt: chacha20 IV:\n" + net.i2p.util.HexDump.dump(iv)); @@ -445,21 +597,104 @@ public class EncryptedLeaseSet extends LeaseSet2 { _log.debug("Decrypt: outer plaintext:\n" + net.i2p.util.HexDump.dump(plaintext)); } - boolean perClient = (plaintext[0] & 0x01) != 0; - if (perClient) { - int authScheme = (plaintext[0] & 0x0e) >> 1; - // TODO - throw new DataFormatException("Per client auth unsupported, scheme: " + authScheme); + int authType = plaintext[0] & 0x0f; + int authLen; + if (authType == BlindData.AUTH_NONE) { + authLen = 1; + } else { + if (csk == null) + throw new DataFormatException("Per-client auth but no key"); + if (authType != BlindData.AUTH_DH && authType != BlindData.AUTH_PSK) + throw new DataFormatException("Per-client auth unsupported type: " + authType); + if (csk.getType() != EncType.ECIES_X25519) + throw new DataFormatException("Bad PSK client key type: " + csk); + byte[] seed = new byte[32]; + System.arraycopy(plaintext, 1, seed, 0, 32); + int count = (int) DataHelper.fromLong(plaintext, 33, 2); + if (count == 0) + throw new DataFormatException("No client entries"); + authLen = 1 + SALT_LEN + 2 + (count * CLIENT_LEN); + if (_log.shouldDebug()) { + _log.debug("Found " + count + " client entries, seed is:\n" + + net.i2p.util.HexDump.dump(seed)); + } + byte[] clientKey = new byte[32]; + byte[] clientIVandID = new byte[32]; + if (authType == BlindData.AUTH_DH) { + // seed is public key + PublicKey epk = new PublicKey(EncType.ECIES_X25519, seed); + SessionKey dh = X25519DH.dh(csk, epk); + // HKDF input is 100 bytes + byte[] clientAuthInput = new byte[64 + authInput.length]; + System.arraycopy(dh.getData(), 0, clientAuthInput, 0, 32); + // TODO cache pubkey, either in PrivateKey or use KeyPair + PublicKey cpk = csk.toPublic(); + System.arraycopy(cpk.getData(), 0, clientAuthInput, 32, 32); + // we copy over end of authInput from above + // subcredential and timestamp remain unchanged + System.arraycopy(authInput, 0, clientAuthInput, 64, 36); + hkdf.calculate(seed, clientAuthInput, ELS2_DH, clientKey, clientIVandID, 0); + } else { + // PSK + // HKDF input is 68 bytes ` + // we reuse authInput from above, just replace the first 32 bytes. + // subcredential and timestamp remain unchanged + byte[] clientAuthInput = new byte[32 + authInput.length]; + System.arraycopy(csk.getData(), 0, clientAuthInput, 0, 32); + // we copy over authInput from above + // subcredential and timestamp remain unchanged + System.arraycopy(authInput, 0, clientAuthInput, 32, 36); + hkdf.calculate(seed, clientAuthInput, ELS2_PSK, clientKey, clientIVandID, 0); + } + if (_log.shouldDebug()) { + _log.debug("Looking for client ID:\n" + + net.i2p.util.HexDump.dump(clientIVandID, IV_LEN, ID_LEN) + + "for client key:\n" + + net.i2p.util.HexDump.dump(clientKey)); + } + int off = 35; + byte[] clientCookie = null; + for (int i = 0; i < count; i++) { + if (DataHelper.eq(clientIVandID, IV_LEN, plaintext, off, ID_LEN)) { + clientCookie = new byte[32]; + System.arraycopy(plaintext, off + ID_LEN, clientCookie, 0, 32); + break; + } + off += CLIENT_LEN; + } + if (clientCookie == null) + throw new DataFormatException("Our client auth entry not found"); + if (_log.shouldDebug()) { + _log.debug("Found client cookie:\n" + + net.i2p.util.HexDump.dump(clientCookie)); + } + byte[] clientAuthInput = new byte[32 + authInput.length]; + // we copy over end of authInput from above + // subcredential and timestamp remain unchanged + System.arraycopy(authInput, 0, clientAuthInput, 32, 36); + // decrypt clientCookie to clientAuthInput + ChaCha20.decrypt(clientKey, clientIVandID, clientCookie, 0, clientAuthInput, 0, 32); + if (_log.shouldDebug()) { + _log.debug("Decrypted client cookie:\n" + + net.i2p.util.HexDump.dump(clientAuthInput, 0, 32)); + } + authInput = clientAuthInput; } // layer 2 (inner) decryption // reuse input (because there's no authcookie), get new salt/key/iv ciphertext = plaintext; - plaintext = new byte[ciphertext.length - (1 + SALT_LEN)]; + plaintext = new byte[ciphertext.length - (authLen + SALT_LEN)]; byte[] salt = new byte[SALT_LEN]; - System.arraycopy(ciphertext, 1, salt, 0, SALT_LEN); - hkdf.calculate(salt, input, ELS2L2K, key, iv, 0); - ChaCha20.decrypt(key, iv, ciphertext, 1 + SALT_LEN, plaintext, 0, plaintext.length); + System.arraycopy(ciphertext, authLen, salt, 0, SALT_LEN); + if (_log.shouldDebug()) { + _log.debug("Inner HKDF salt:\n" + + net.i2p.util.HexDump.dump(salt) + + "Inner HKDF input:\n" + + net.i2p.util.HexDump.dump(authInput)); + } + hkdf.calculate(salt, authInput, ELS2L2K, key, iv, 0); + ChaCha20.decrypt(key, iv, ciphertext, authLen + SALT_LEN, plaintext, 0, plaintext.length); if (_log.shouldDebug()) _log.debug("Decrypt: inner plaintext:\n" + net.i2p.util.HexDump.dump(plaintext)); ByteArrayInputStream bais = new ByteArrayInputStream(plaintext); @@ -470,13 +705,13 @@ public class EncryptedLeaseSet extends LeaseSet2 { else if (type == KEY_TYPE_META_LS2) innerLS2 = new MetaLeaseSet(); else - throw new DataFormatException("Unsupported LS type: " + type); + throw new DataFormatException("Bad decryption or unsupported LS type: " + type); innerLS2.readBytes(bais); _decryptedLS2 = innerLS2; } /** - * The HKDF input + * The HKDF input (no per-client auth) * * @return 36 bytes * @since 0.9.39 @@ -489,6 +724,22 @@ public class EncryptedLeaseSet extends LeaseSet2 { return rv; } + /** + * The HKDF input (with per-client auth) + * + * @param authcookie 32 bytes + * @return 68 bytes + * @since 0.9.41 + */ + private byte[] getHKDFInput(I2PAppContext ctx, byte[] authcookie) { + byte[] subcredential = getSubcredential(ctx); + byte[] rv = new byte[authcookie.length + subcredential.length + 4]; + System.arraycopy(authcookie, 0, rv, 0, authcookie.length); + System.arraycopy(subcredential, 0, rv, authcookie.length, subcredential.length); + DataHelper.toLong(rv, authcookie.length + subcredential.length, 4, _published / 1000); + return rv; + } + /** * The subcredential * @@ -537,6 +788,20 @@ public class EncryptedLeaseSet extends LeaseSet2 { */ @Override public void sign(SigningPrivateKey key) throws DataFormatException { + sign(key, BlindData.AUTH_NONE, null); + } + + /** + * Sign the structure using the supplied signing key. + * Overridden because we sign the inner, then blind and encrypt + * and sign the outer. + * + * @param authType 0, 1, or 3, see BlindData + * @param clientKeys X25519 public keys for DH, private keys for PSK + * @throws IllegalStateException if already signed + * @since 0.9.41 + */ + public void sign(SigningPrivateKey key, int authType, List clientKeys) throws DataFormatException { // now sign inner with the unblinded key // inner LS is always unpublished int saveFlags = _flags; @@ -548,7 +813,7 @@ public class EncryptedLeaseSet extends LeaseSet2 { _log.debug("Corresponding pubkey: " + key.toPublic()); _log.debug("Inner sig: " + _signature.getType() + ' ' + _signature.toBase64()); } - encrypt(null); + encrypt(authType, clientKeys); SigningPrivateKey bkey = Blinding.blind(key, _alpha); int len = size(); ByteArrayOutputStream out = new ByteArrayOutputStream(1 + len); @@ -580,6 +845,19 @@ public class EncryptedLeaseSet extends LeaseSet2 { */ @Override public boolean verifySignature() { + return verifySignature((PrivateKey) null); + } + + /** + * Decrypt if possible, and verify inner sig also. + * + * Must call setDestination() prior to this if attempting decryption. + * + * @param clientKey PrivateKey for DH or PSK, or null if none + * @return valid + * @since 0.9.41 + */ + public boolean verifySignature(PrivateKey clientKey) { // TODO use fields in super if (_decryptedLS2 != null) return _decryptedLS2.verifySignature(); @@ -598,7 +876,7 @@ public class EncryptedLeaseSet extends LeaseSet2 { return true; } try { - decrypt(); + decrypt(clientKey); } catch (DataFormatException dfe) { _log.warn("ELS2 decrypt fail", dfe); return false; @@ -684,13 +962,21 @@ public class EncryptedLeaseSet extends LeaseSet2 { pkf.createIfAbsent(SigType.EdDSA_SHA512_Ed25519); System.out.println("Online test"); java.io.File f2 = new java.io.File("online-encls2.dat"); - test(pkf, f2, false); + test(pkf, f2, false, BlindData.AUTH_NONE, null); + List keys = new java.util.ArrayList(4); + for (int i = 0; i < 4; i++) { + keys.add(net.i2p.crypto.KeyGenerator.getInstance().generatePKIKeys(net.i2p.crypto.EncType.ECIES_X25519)); + } + System.out.println("Online test with DH Keys"); + test(pkf, f2, false, BlindData.AUTH_DH, keys); + System.out.println("Online test with PSK Keys"); + test(pkf, f2, false, BlindData.AUTH_PSK, keys); //System.out.println("Offline test"); //f2 = new java.io.File("offline-encls2.dat"); //test(pkf, f2, true); } - private static void test(PrivateKeyFile pkf, java.io.File outfile, boolean offline) throws Exception { + private static void test(PrivateKeyFile pkf, java.io.File outfile, boolean offline, int authType, List clientKeys) throws Exception { net.i2p.util.RandomSource rand = net.i2p.util.RandomSource.getInstance(); long now = System.currentTimeMillis() + 5*60*1000; EncryptedLeaseSet ls2 = new EncryptedLeaseSet(); @@ -726,10 +1012,24 @@ public class EncryptedLeaseSet extends LeaseSet2 { ls2.setOfflineSignature(now, transientPub, sig); ls2.sign(transientPriv); } else { - ls2.sign(spk); + List signkeys = null; + if (authType != BlindData.AUTH_NONE) { + signkeys = new java.util.ArrayList(); + for (KeyPair kp : clientKeys) { + if (authType == BlindData.AUTH_DH) + signkeys.add(kp.getPublic()); + else + signkeys.add(kp.getPrivate()); + } + } + ls2.sign(spk, authType, signkeys); } System.out.println("\nCreated: " + ls2); - if (!ls2.verifySignature()) { + PrivateKey verifyKey = null; + if (authType != BlindData.AUTH_NONE) + verifyKey = clientKeys.get(0).getPrivate(); + if (!ls2.verifySignature(verifyKey)) { + I2PAppContext.getGlobalContext().logManager().flush(); System.out.println("Verify FAILED"); return; } @@ -746,8 +1046,9 @@ public class EncryptedLeaseSet extends LeaseSet2 { System.out.println("\nRead back: " + ls3); // required to decrypt ls3.setDestination(pkf.getDestination()); - if (!ls3.verifySignature()) + if (!ls3.verifySignature(verifyKey)) System.out.println("Verify FAILED"); + I2PAppContext.getGlobalContext().logManager().flush(); } ****/ }