From 7fbe1ced5a652262db320d4aaf6db396638de23a Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Wed, 20 Feb 2019 22:49:14 +0000
Subject: [PATCH] Crypto: Sign/verify/encrypt/decrypt for Encrypted LS2
 generateAlpha() method for arbitrary date

---
 core/java/src/net/i2p/crypto/Blinding.java    |  18 +
 .../src/net/i2p/data/EncryptedLeaseSet.java   | 330 ++++++++++++++++--
 2 files changed, 319 insertions(+), 29 deletions(-)

diff --git a/core/java/src/net/i2p/crypto/Blinding.java b/core/java/src/net/i2p/crypto/Blinding.java
index 6625083052..d9a423cbf9 100644
--- a/core/java/src/net/i2p/crypto/Blinding.java
+++ b/core/java/src/net/i2p/crypto/Blinding.java
@@ -105,6 +105,7 @@ public final class Blinding {
     }
 
     /**
+     *  Generate alpha for current time.
      *  Only for SigType EdDSA_SHA512_Ed25519.
      *
      *  @param dest spk must be SigType EdDSA_SHA512_Ed25519
@@ -116,6 +117,23 @@ public final class Blinding {
      */
     public static SigningPrivateKey generateAlpha(I2PAppContext ctx, Destination dest, String secret) {
         long now = ctx.clock().now();
+        return generateAlpha(ctx, dest, secret, now);
+    }
+
+    /**
+     *  Generate alpha for the given time.
+     *  Only for SigType EdDSA_SHA512_Ed25519.
+     *
+     *  @param dest spk must be SigType EdDSA_SHA512_Ed25519
+     *  @param secret may be null or zero-length
+     *  @param now for what time?
+     *  @return SigType RedDSA_SHA512_Ed25519
+     *  @throws UnsupportedOperationException unless supported SigTypes
+     *  @throws IllegalArgumentException on bad inputs
+     *  @since 0.9.39
+     */
+    public static SigningPrivateKey generateAlpha(I2PAppContext ctx, Destination dest,
+                                                  String secret, long now) {
         String modVal;
         synchronized(_fmt) {
             modVal = _fmt.format(now);
diff --git a/core/java/src/net/i2p/data/EncryptedLeaseSet.java b/core/java/src/net/i2p/data/EncryptedLeaseSet.java
index 3f531179d0..f83f9c85b7 100644
--- a/core/java/src/net/i2p/data/EncryptedLeaseSet.java
+++ b/core/java/src/net/i2p/data/EncryptedLeaseSet.java
@@ -1,13 +1,20 @@
 package net.i2p.data;
 
+import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 
+import net.i2p.I2PAppContext;
+import net.i2p.crypto.Blinding;
+import net.i2p.crypto.ChaCha20;
+import net.i2p.crypto.DSAEngine;
+import net.i2p.crypto.HKDF;
 import net.i2p.crypto.SHA256Generator;
 import net.i2p.crypto.SigType;
 import net.i2p.util.Clock;
+import net.i2p.util.Log;
 
 /**
  * Use getSigningKey() / setSigningKey() (revocation key in super) for the blinded key.
@@ -18,23 +25,40 @@ import net.i2p.util.Clock;
  */
 public class EncryptedLeaseSet extends LeaseSet2 {
 
-    // includes IV and MAC
+    // includes salt
     private byte[] _encryptedData;
     private LeaseSet2 _decryptedLS2;
     private Hash __calculatedHash;
+    private SigningPrivateKey _alpha;
 
     private static final int MIN_ENCRYPTED_SIZE = 8 + 16;
     private static final int MAX_ENCRYPTED_SIZE = 4096;
 
+    private static final int SALT_LEN = 32;
+    private static final byte[] CREDENTIAL = DataHelper.getASCII("credential");
+    private static final byte[] SUBCREDENTIAL = DataHelper.getASCII("subcredential");
+    private static final String ELS2L1K = "ELS2_L1K";
+    private static final String ELS2L2K = "ELS2_L2K";
+
     public EncryptedLeaseSet() {
         super();
     }
 
+    /**
+     *  @return leaseset or null if not decrypted.
+     *          Also returns null if we created and encrypted it.
+     *  @since 0.9.39
+     */
+    public LeaseSet2 getDecryptedLeaseSet() {
+        return _decryptedLS2;
+    }
+
     ///// overrides below here
 
     @Override
     public int getType() {
-        return KEY_TYPE_ENCRYPTED_LS2;
+        // return type 3 before signing so inner signing works
+        return (_signature != null) ? KEY_TYPE_ENCRYPTED_LS2 : KEY_TYPE_LS2;
     }
 
     /**
@@ -68,13 +92,27 @@ public class EncryptedLeaseSet extends LeaseSet2 {
         SigningPublicKey spk = dest.getSigningPublicKey();
         if (spk.getType() != SigType.EdDSA_SHA512_Ed25519)
             throw new IllegalArgumentException();
-        // TODO generate blinded key
-        _signingKey = blind(spk, null);
+        SigningPublicKey bpk = blind();
+        if (_signingKey == null)
+            _signingKey = bpk;
+        else if (!_signingKey.equals(bpk))
+            throw new IllegalArgumentException("blinded pubkey mismatch");
     }
 
-    private static SigningPublicKey blind(SigningPublicKey spk, SigningPrivateKey priv) {
-        // TODO generate blinded key
-        return spk;
+    /**
+     * Generate blinded pubkey from the unblinded pubkey in the destination,
+     * which must have been previously set.
+     *
+     * @since 0.9.39
+     */
+    private SigningPublicKey blind() {
+        SigningPublicKey spk = _destination.getSigningPublicKey();
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        if (_published <= 0)
+            _alpha = Blinding.generateAlpha(ctx, _destination, null);
+        else
+            _alpha = Blinding.generateAlpha(ctx, _destination, null, _published);
+        return Blinding.blind(spk, _alpha);
     }
 
     /**
@@ -112,21 +150,25 @@ public class EncryptedLeaseSet extends LeaseSet2 {
     }
 
     /**
+     *  Before encrypt() is called, the inner leaseset.
+     *  After encrypt() is called, the encrypted data.
      *  Without sig. This does NOT validate the signature
      */
     @Override
     protected void writeBytesWithoutSig(OutputStream out) throws DataFormatException, IOException {
         if (_signingKey == null)
             throw new DataFormatException("Not enough data to write out a LeaseSet");
-        // LS2 header
-        writeHeader(out);
-        // Encrypted LS2 part
         if (_encryptedData == null) {
-            // TODO
-            encrypt(null);
+            super.writeHeader(out);
+            writeBody(out);
+        } else {
+            // for signing the inner part
+            writeHeader(out);
+            // After signing
+            // Encrypted LS2 part
+            DataHelper.writeLong(out, 2, _encryptedData.length);
+            out.write(_encryptedData);
         }
-        DataHelper.writeLong(out, 2, _encryptedData.length);
-        out.write(_encryptedData);
     }
     
     /**
@@ -241,35 +283,249 @@ public class EncryptedLeaseSet extends LeaseSet2 {
     /**
      *  Throws IllegalStateException if not initialized.
      *
-     *  @param key ignored, to be fixed
+     *  @param skey ignored
      *  @throws IllegalStateException
      */
     @Override
-    public void encrypt(SessionKey key) {
+    public void encrypt(SessionKey skey) {
         if (_encryptedData != null)
-            throw new IllegalStateException();
+            throw new IllegalStateException("already encrypted");
+        if (_signature == null)
+            throw new IllegalStateException("not signed");
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         try {
-            // Middle layer - flag
-            baos.write(0);
             // Inner layer - type - data covered by sig
             baos.write(KEY_TYPE_LS2);
             super.writeHeader(baos);
             writeBody(baos);
+            _signature.writeBytes(baos);
         } catch (DataFormatException dfe) {
             throw new IllegalStateException("Error encrypting LS2", dfe);
         } catch (IOException ioe) {
             throw new IllegalStateException("Error encrypting LS2", ioe);
         }
 
-        // TODO sign and add signature
-        // TODO encrypt - TESTING ONLY
-        _encryptedData = baos.toByteArray();
-        for (int i = 0; i < _encryptedData.length; i++) {
-             _encryptedData[i] ^= 0x5a;
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        byte[] input = getHKDFInput(ctx);
+
+        // layer 2 (inner) encryption
+        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);
+        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);
+        System.out.println("Encrypt: inner plaintext:\n" + net.i2p.util.HexDump.dump(plaintext));
+        System.out.println("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);
+        plaintext = ciphertext;
+        ciphertext = new byte[SALT_LEN + plaintext.length];
+        System.arraycopy(salt, 0, ciphertext, 0, SALT_LEN);
+        ChaCha20.encrypt(key, iv, plaintext, 0, ciphertext, SALT_LEN, plaintext.length);
+        System.out.println("Encrypt: outer ciphertext:\n" + net.i2p.util.HexDump.dump(ciphertext));
+        _encryptedData = ciphertext;
+    }
+
+    /**
+     *  Throws IllegalStateException if not initialized.
+     *
+     *  @param skey ignored
+     *  @throws IllegalStateException
+     */
+    private void decrypt() throws DataFormatException, IOException {
+        if (_encryptedData == null)
+            throw new IllegalStateException("not encrypted");
+        if (_decryptedLS2 != null)
+            return;
+        I2PAppContext ctx = I2PAppContext.getGlobalContext();
+        byte[] input = getHKDFInput(ctx);
+
+        // layer 1 (outer) decryption
+        HKDF hkdf = new HKDF(ctx);
+        byte[] key = new byte[32];
+        // use first 12 bytes only
+        byte[] iv = new byte[32];
+        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);
+        ChaCha20.decrypt(key, iv, ciphertext, SALT_LEN, plaintext, 0, plaintext.length);
+        System.out.println("Decrypt: outer ciphertext:\n" + net.i2p.util.HexDump.dump(ciphertext));
+        System.out.println("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);
+        }
+
+        // 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)];
+        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.out.println("Decrypt: inner plaintext:\n" + net.i2p.util.HexDump.dump(plaintext));
+        ByteArrayInputStream bais = new ByteArrayInputStream(plaintext);
+        int type = bais.read();
+        LeaseSet2 innerLS2;
+        if (type == KEY_TYPE_LS2)
+            innerLS2 = new LeaseSet2();
+        else if (type == KEY_TYPE_META_LS2)
+            innerLS2 = new MetaLeaseSet();
+        else
+            throw new DataFormatException("Unsupported LS type: " + type);
+        innerLS2.readBytes(bais);
+        _decryptedLS2 = innerLS2;
+    }
+
+    /**
+     *  The HKDF input
+     *
+     *  @return 36 bytes
+     *  @since 0.9.39
+     */
+    private byte[] getHKDFInput(I2PAppContext ctx) {
+        byte[] subcredential = getSubcredential(ctx);
+        byte[] rv = new byte[subcredential.length + 4];
+        System.arraycopy(subcredential, 0, rv, 0, subcredential.length);
+        DataHelper.toLong(rv, subcredential.length, 4, _published / 1000);
+        return rv;
+    }
+
+    /**
+     *  The subcredential
+     *
+     *  @return 32 bytes
+     *  @throws IllegalStateException if we don't have it
+     *  @since 0.9.39
+     */
+    private byte[] getSubcredential(I2PAppContext ctx) {
+        if (_destination == null)
+            throw new IllegalStateException("no known destination to decrypt with");
+        byte[] credential = hash(ctx, CREDENTIAL, _destination.toByteArray());
+        byte[] spk = _signingKey.getData();
+        byte[] tmp = new byte[credential.length + spk.length];
+        System.arraycopy(credential, 0, tmp, 0, credential.length);
+        System.arraycopy(spk, 0, tmp, credential.length, spk.length);
+        return hash(ctx, SUBCREDENTIAL, tmp);
+    }
+
+    /**
+     *  Hash with a personalization string
+     *
+     *  @return 32 bytes
+     *  @since 0.9.39
+     */
+    private static byte[] hash(I2PAppContext ctx, byte[] p, byte[] d) {
+        byte[] data = new byte[p.length + d.length];
+        System.arraycopy(p, 0, data, 0, p.length);
+        System.arraycopy(d, 0, data, p.length, d.length);
+        byte[] rv = new byte[32];
+        ctx.sha().calculateHash(data, 0, data.length, rv, 0);
+        return rv;
+    }
+
+    /**
+     * Sign the structure using the supplied signing key.
+     * Overridden because we sign the inner, then blind and encrypt
+     * and sign the outer.
+     *
+     * @throws IllegalStateException if already signed
+     */
+    @Override
+    public void sign(SigningPrivateKey key) throws DataFormatException {
+        Log log = I2PAppContext.getGlobalContext().logManager().getLog(EncryptedLeaseSet.class);
+        // now sign inner with the unblinded key 
+        super.sign(key);
+        if (log.shouldDebug()) {
+            log.debug("Sign inner with key: " + key.getType() + ' ' + key.toBase64());
+            log.debug("Corresponding pubkey: " + key.toPublic().toBase64());
+            log.debug("Sign inner: " + _signature.getType() + ' ' + _signature.toBase64());
+        }
+        encrypt(null);
+        SigningPrivateKey bkey = Blinding.blind(key, _alpha);
+        int len = size();
+        ByteArrayOutputStream out = new ByteArrayOutputStream(1 + len);
+        try {
+            // unlike LS1, sig covers type
+            out.write(getType());
+            writeBytesWithoutSig(out);
+        } catch (IOException ioe) {
+            throw new DataFormatException("Signature failed", ioe);
+        }
+        byte data[] = out.toByteArray();
+        // now sign outer with the blinded key 
+        _signature = DSAEngine.getInstance().sign(data, bkey);
+        if (_signature == null)
+            throw new DataFormatException("Signature failed with " + key.getType() + " key");
+        if (log.shouldDebug()) {
+            log.debug("Sign outer with key: " + bkey.getType() + ' ' + bkey.toBase64());
+            log.debug("Corresponding pubkey: " + bkey.toPublic().toBase64());
+            log.debug("Sign outer: " + _signature.getType() + ' ' + _signature.toBase64());
+        }
+    }
+
+    /**
+     * Overridden to decrypt if possible, and verify inner sig also.
+     *
+     * Must call setDestination() prior to this if attempting decryption.
+     *
+     * @return valid
+     */
+    @Override
+    public boolean verifySignature() {
+        Log log = I2PAppContext.getGlobalContext().logManager().getLog(EncryptedLeaseSet.class);
+        if (log.shouldDebug()) {
+            log.debug("Sig verify outer with key: " + _signingKey.getType() + ' ' + _signingKey.toBase64());
+            log.debug("Sig verify outer: " + _signature.getType() + ' ' + _signature.toBase64());
+        }
+        if (!super.verifySignature()) {
+            log.error("ELS2 outer sig verify fail");
+            return false;
+        }
+        log.error("ELS2 outer sig verify success");
+        if (_destination == null) {
+            log.warn("ELS2 no dest to decrypt with");
+            return true;
+        }
+        try {
+            decrypt();
+        } catch (DataFormatException dfe) {
+            log.error("ELS2 decrypt fail", dfe);
+            return false;
+        } catch (IOException ioe) {
+            log.error("ELS2 decrypt fail", ioe);
+            return false;
         }
+        if (log.shouldDebug()) {
+            log.debug("Decrypted inner LS2:\n" + _decryptedLS2);
+            log.debug("Sig verify inner with key: " + _decryptedLS2.getDestination().getSigningPublicKey().getType() + ' ' + _decryptedLS2.getDestination().getSigningPublicKey().toBase64());
+            log.debug("Sig verify inner: " + _decryptedLS2.getSignature().getType() + ' ' + _decryptedLS2.getSignature().toBase64());
+        }
+        boolean rv = _decryptedLS2.verifySignature();
+        if (!rv)
+            log.error("ELS2 inner sig verify fail");
+        else
+            log.debug("ELS2 inner sig verify success");
+        return rv;
     }
 
+
     @Override
     public boolean equals(Object object) {
         if (object == this) return true;
@@ -299,9 +555,21 @@ public class EncryptedLeaseSet extends LeaseSet2 {
             buf.append("\n\tOffline Signature: ").append(_offlineSignature);
         }
         buf.append("\n\tUnpublished? ").append(isUnpublished());
+        buf.append("\n\tLength: ").append(_encryptedData.length);
         buf.append("\n\tSignature: ").append(_signature);
         buf.append("\n\tPublished: ").append(new java.util.Date(_published));
         buf.append("\n\tExpires: ").append(new java.util.Date(_expires));
+        if (_decryptedLS2 != null) {
+            buf.append("\n\tDecrypted LS:\n").append(_decryptedLS2);
+        } else if (_destination != null) {
+            buf.append("\n\tDestination: ").append(_destination);
+            buf.append("\n\tLeases: #").append(getLeaseCount());
+            for (int i = 0; i < getLeaseCount(); i++) {
+                buf.append("\n\t\t").append(getLease(i));
+            }
+        } else {
+            buf.append("\n\tNot decrypted");
+        }
         buf.append("]");
         return buf.toString();
     }
@@ -309,7 +577,7 @@ public class EncryptedLeaseSet extends LeaseSet2 {
 /****
     public static void main(String args[]) throws Exception {
         if (args.length != 1) {
-            System.out.println("Usage: LeaseSet2 privatekeyfile.dat");
+            System.out.println("Usage: EncryptedLeaseSet privatekeyfile.dat");
             System.exit(1);
         }
         java.io.File f = new java.io.File(args[0]);
@@ -318,9 +586,9 @@ public class EncryptedLeaseSet extends LeaseSet2 {
         System.out.println("Online test");
         java.io.File f2 = new java.io.File("online-encls2.dat");
         test(pkf, f2, false);
-        System.out.println("Offline test");
-        f2 = new java.io.File("offline-encls2.dat");
-        test(pkf, f2, true);
+        //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 {
@@ -361,8 +629,10 @@ public class EncryptedLeaseSet extends LeaseSet2 {
             ls2.sign(spk);
         }
         System.out.println("Created: " + ls2);
-        if (!ls2.verifySignature())
+        if (!ls2.verifySignature()) {
             System.out.println("Verify FAILED");
+            return;
+        }
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         ls2.writeBytes(out);
         java.io.OutputStream out2 = new java.io.FileOutputStream(outfile);
@@ -374,6 +644,8 @@ public class EncryptedLeaseSet extends LeaseSet2 {
         EncryptedLeaseSet ls3 = new EncryptedLeaseSet();
         ls3.readBytes(in);
         System.out.println("Read back: " + ls3);
+        // required to decrypt
+        ls3.setDestination(pkf.getDestination());
         if (!ls3.verifySignature())
             System.out.println("Verify FAILED");
     }
-- 
GitLab