Changeset 06fa817


Ignore:
Timestamp:
May 22, 2019 4:11:36 PM (10 months ago)
Author:
zzz <zzz@…>
Branches:
master
Children:
7489a64
Parents:
cb76235
Message:

Data: Per-client auth for enc. LS2 (proposal 123)

File:
1 edited

Legend:

Unmodified
Added
Removed
  • core/java/src/net/i2p/data/EncryptedLeaseSet.java

    rcb76235 r06fa817  
    66import java.io.InputStream;
    77import java.io.OutputStream;
     8import java.util.Collections;
    89import java.util.List;
    910
     
    1213import net.i2p.crypto.ChaCha20;
    1314import net.i2p.crypto.DSAEngine;
     15import net.i2p.crypto.EncType;
    1416import net.i2p.crypto.HKDF;
     17import net.i2p.crypto.KeyPair;
    1518import net.i2p.crypto.SHA256Generator;
    1619import net.i2p.crypto.SigType;
     20import net.i2p.crypto.x25519.X25519DH;
    1721import net.i2p.util.Clock;
    1822import net.i2p.util.Log;
     
    4751    private static final String ELS2L1K = "ELS2_L1K";
    4852    private static final String ELS2L2K = "ELS2_L2K";
     53    private static final String ELS2_DH = "ELS2_XCA";
     54    private static final String ELS2_PSK = "ELS2PSKA";
     55    private static final int IV_LEN = 12;
     56    private static final int ID_LEN = 8;
     57    private static final int COOKIE_LEN = 32;
     58    private static final int CLIENT_LEN = ID_LEN + COOKIE_LEN;
    4959
    5060    public EncryptedLeaseSet() {
     
    352362    @Override
    353363    public void encrypt(SessionKey skey) {
     364        encrypt(BlindData.AUTH_NONE, null);
     365    }
     366
     367    /**
     368     *  Throws IllegalStateException if not initialized.
     369     *  Ref: proposal 123
     370     *
     371     *  @param authType 0, 1, or 3, see BlindData
     372     *  @param clientKeys The client's X25519 public or private keys, null if unused
     373     *  @throws IllegalStateException
     374     */
     375    public void encrypt(int authType, List<? extends SimpleDataStructure> clientKeys) {
    354376        if (_encryptedData != null)
    355377            throw new IllegalStateException("already encrypted");
     
    374396        }
    375397
     398        // layer 2 (inner) encryption
    376399        I2PAppContext ctx = I2PAppContext.getGlobalContext();
    377         byte[] input = getHKDFInput(ctx);
    378 
    379         // layer 2 (inner) encryption
    380400        byte[] salt = new byte[SALT_LEN];
    381401        ctx.random().nextBytes(salt);
     
    384404        // use first 12 bytes only
    385405        byte[] iv = new byte[32];
    386         hkdf.calculate(salt, input, ELS2L2K, key, iv, 0);
     406        int authLen;
     407        if (authType == BlindData.AUTH_NONE) {
     408            authLen = 1;
     409        } else if (authType == BlindData.AUTH_DH ||
     410                   authType == BlindData.AUTH_PSK) {
     411            if (clientKeys == null || clientKeys.isEmpty())
     412                throw new IllegalArgumentException("No client keys provided");
     413            authLen = 1 + SALT_LEN + 2 + (clientKeys.size() * CLIENT_LEN);
     414        } else {
     415            throw new IllegalArgumentException("Bad auth type " + authType);
     416        }
     417        byte[] authInput;
     418        byte[] authcookie = null;
     419        if (authType == BlindData.AUTH_NONE) {
     420            authInput = getHKDFInput(ctx);
     421        } else {
     422            authcookie = new byte[32];
     423            ctx.random().nextBytes(authcookie);
     424            if (_log.shouldDebug()) {
     425                _log.debug("Auth Cookie:\n" +
     426                           net.i2p.util.HexDump.dump(authcookie));
     427            }
     428            authInput = getHKDFInput(ctx, authcookie);
     429        }
     430        if (_log.shouldDebug()) {
     431            _log.debug("Inner HKDF salt:\n" +
     432                       net.i2p.util.HexDump.dump(salt) +
     433                       "Inner HKDF input:\n" +
     434                       net.i2p.util.HexDump.dump(authInput));
     435        }
     436        hkdf.calculate(salt, authInput, ELS2L2K, key, iv, 0);
    387437        byte[] plaintext = baos.toByteArray();
    388         byte[] ciphertext = new byte[1 + SALT_LEN + plaintext.length];
    389         // Middle layer - flag
    390         ciphertext[0] = 0;
    391         System.arraycopy(salt, 0, ciphertext, 1, SALT_LEN);
    392         ChaCha20.encrypt(key, iv, plaintext, 0, ciphertext, 1 + SALT_LEN, plaintext.length);
     438        byte[] ciphertext = new byte[authLen + SALT_LEN + plaintext.length];
     439
     440        // Middle layer
     441        if (authType == BlindData.AUTH_NONE) {
     442            // Flag only
     443            ciphertext[0] = BlindData.AUTH_NONE;
     444        } else {
     445            // Flag
     446            ciphertext[0] = (byte) (authType & 0x0f);
     447            if (clientKeys.size() > 1)
     448                Collections.shuffle(clientKeys);
     449            if (authType == BlindData.AUTH_DH) {
     450                // DH
     451                KeyPair encKeys = ctx.keyGenerator().generatePKIKeys(EncType.ECIES_X25519);
     452                PrivateKey esk = encKeys.getPrivate();
     453                PublicKey epk = encKeys.getPublic();
     454                // HKDF input is 100 bytes
     455                byte[] clientAuthInput = new byte[32 + authInput.length];
     456                // we copy over end of authInput from above
     457                // subcredential and timestamp remain unchanged
     458                System.arraycopy(authInput, 32, clientAuthInput, 64, 36);
     459                // pubkey
     460                System.arraycopy(epk.getData(), 0, ciphertext, 1, 32);
     461                DataHelper.toLong(ciphertext, 33, 2, clientKeys.size());
     462                int off = 35;
     463                byte[] clientKey = new byte[32];
     464                byte[] clientIVandID = new byte[32];
     465                for (SimpleDataStructure sds : clientKeys) {
     466                    if (!(sds instanceof PublicKey))
     467                        throw new IllegalArgumentException("Bad DH client key type: " + sds);
     468                    PublicKey cpk = (PublicKey) sds;
     469                    if (cpk.getType() != EncType.ECIES_X25519)
     470                        throw new IllegalArgumentException("Bad DH client key type: " + cpk);
     471                    SessionKey dh = X25519DH.dh(esk, cpk);
     472                    System.arraycopy(dh.getData(), 0, clientAuthInput, 0, 32);
     473                    System.arraycopy(cpk.getData(), 0, clientAuthInput, 32, 32);
     474                    hkdf.calculate(epk.getData(), clientAuthInput, ELS2_DH, clientKey, clientIVandID, 0);
     475                    System.arraycopy(clientIVandID, IV_LEN, ciphertext, off, ID_LEN);
     476                    off += ID_LEN;
     477                    ChaCha20.encrypt(clientKey, clientIVandID, authcookie, 0, ciphertext, off, authcookie.length);
     478                    if (_log.shouldDebug()) {
     479                        _log.debug("Added client ID/enc.cookie:\n" +
     480                                   net.i2p.util.HexDump.dump(clientIVandID, IV_LEN, ID_LEN) +
     481                                   net.i2p.util.HexDump.dump(ciphertext, off, COOKIE_LEN) +
     482                                   "for client key:\n" +
     483                                   net.i2p.util.HexDump.dump(clientKey));
     484                    }
     485                    off += COOKIE_LEN;
     486                }
     487            } else {
     488                // PSK
     489                // salt
     490                byte[] authsalt = new byte[32];
     491                ctx.random().nextBytes(authsalt);
     492                System.arraycopy(authsalt, 0, ciphertext, 1, 32);
     493                DataHelper.toLong(ciphertext, 33, 2, clientKeys.size());
     494                int off = 35;
     495                byte[] clientKey = new byte[32];
     496                byte[] clientIVandID = new byte[32];
     497                for (SimpleDataStructure sds : clientKeys) {
     498                    if (!(sds instanceof PrivateKey))
     499                        throw new IllegalArgumentException("Bad DH client key type: " + sds);
     500                    PrivateKey csk = (PrivateKey) sds;
     501                    if (csk.getType() != EncType.ECIES_X25519)
     502                        throw new IllegalArgumentException("Bad PSK client key type: " + csk);
     503                    // HKDF input is 68 bytes                        `
     504                    // we reuse authInput from above, just replace the first 32 bytes.
     505                    // subcredential and timestamp remain unchanged
     506                    System.arraycopy(csk.getData(), 0, authInput, 0, 32);
     507                    hkdf.calculate(authsalt, authInput, ELS2_PSK, clientKey, clientIVandID, 0);
     508                    System.arraycopy(clientIVandID, IV_LEN, ciphertext, off, ID_LEN);
     509                    off += ID_LEN;
     510                    ChaCha20.encrypt(clientKey, clientIVandID, authcookie, 0, ciphertext, off, authcookie.length);
     511                    if (_log.shouldDebug()) {
     512                        _log.debug("Added client ID/enc.cookie:\n" +
     513                                   net.i2p.util.HexDump.dump(clientIVandID, IV_LEN, ID_LEN) +
     514                                   net.i2p.util.HexDump.dump(ciphertext, off, COOKIE_LEN) +
     515                                   "for client key:\n" +
     516                                   net.i2p.util.HexDump.dump(clientKey));
     517                    }
     518                    off += COOKIE_LEN;
     519                }
     520            }
     521        }
     522        System.arraycopy(salt, 0, ciphertext, authLen, SALT_LEN);
     523        ChaCha20.encrypt(key, iv, plaintext, 0, ciphertext, authLen + SALT_LEN, plaintext.length);
    393524        if (_log.shouldDebug()) {
    394525            _log.debug("Encrypt: inner plaintext:\n" + net.i2p.util.HexDump.dump(plaintext));
     
    397528
    398529        // layer 1 (outer) encryption
    399         // reuse input (because there's no authcookie), generate new salt/key/iv
    400530        ctx.random().nextBytes(salt);
    401         hkdf.calculate(salt, input, ELS2L1K, key, iv, 0);
     531        if (authType == BlindData.AUTH_NONE) {
     532            // reuse input (because there's no authcookie), generate new salt/key/iv
     533            hkdf.calculate(salt, authInput, ELS2L1K, key, iv, 0);
     534        } else {
     535            // get just the subcredential and date
     536            byte[] l1Input = new byte[36];
     537            System.arraycopy(authInput, 32, l1Input, 0, 36);
     538            hkdf.calculate(salt, l1Input, ELS2L1K, key, iv, 0);
     539        }
    402540        plaintext = ciphertext;
    403541        ciphertext = new byte[SALT_LEN + plaintext.length];
     
    416554     *  Throws IllegalStateException if not initialized.
    417555     *
    418      *  @param skey ignored
     556     *  @param clientKey PrivateKey for DH or PSK, or null if none
    419557     *  @throws IllegalStateException
    420558     */
    421     private void decrypt() throws DataFormatException, IOException {
     559    private void decrypt(PrivateKey csk) throws DataFormatException, IOException {
     560        try {
     561            x_decrypt(csk);
     562        } catch (IndexOutOfBoundsException ioobe) {
     563            throw new DataFormatException("ioobe", ioobe);
     564        }
     565    }
     566
     567    /**
     568     *  Throws IllegalStateException if not initialized.
     569     *
     570     *  @param clientKey PrivateKey for DH or PSK, or null if none
     571     *  @throws IllegalStateException
     572     */
     573    private void x_decrypt(PrivateKey csk) throws DataFormatException, IOException {
    422574        if (_encryptedData == null)
    423575            throw new IllegalStateException("not encrypted");
     
    425577            return;
    426578        I2PAppContext ctx = I2PAppContext.getGlobalContext();
    427         byte[] input = getHKDFInput(ctx);
     579        byte[] authInput = getHKDFInput(ctx);
    428580
    429581        // layer 1 (outer) decryption
     
    435587        byte[] plaintext = new byte[ciphertext.length - SALT_LEN];
    436588        // first 32 bytes of ciphertext are the salt
    437         hkdf.calculate(ciphertext, input, ELS2L1K, key, iv, 0);
     589        hkdf.calculate(ciphertext, authInput, ELS2L1K, key, iv, 0);
    438590        if (_log.shouldDebug()) {
    439591            _log.debug("Decrypt: chacha20 key:\n" + net.i2p.util.HexDump.dump(key));
     
    446598        }
    447599
    448         boolean perClient = (plaintext[0] & 0x01) != 0;
    449         if (perClient) {
    450             int authScheme = (plaintext[0] & 0x0e) >> 1;
    451             // TODO
    452             throw new DataFormatException("Per client auth unsupported, scheme: " + authScheme);
     600        int authType = plaintext[0] & 0x0f;
     601        int authLen;
     602        if (authType == BlindData.AUTH_NONE) {
     603            authLen = 1;
     604        } else {
     605            if (csk == null)
     606                throw new DataFormatException("Per-client auth but no key");
     607            if (authType != BlindData.AUTH_DH && authType != BlindData.AUTH_PSK)
     608                throw new DataFormatException("Per-client auth unsupported type: " + authType);
     609            if (csk.getType() != EncType.ECIES_X25519)
     610                throw new DataFormatException("Bad PSK client key type: " + csk);
     611            byte[] seed = new byte[32];
     612            System.arraycopy(plaintext, 1, seed, 0, 32);
     613            int count = (int) DataHelper.fromLong(plaintext, 33, 2);
     614            if (count == 0)
     615                throw new DataFormatException("No client entries");
     616            authLen = 1 + SALT_LEN + 2 + (count * CLIENT_LEN);
     617            if (_log.shouldDebug()) {
     618                 _log.debug("Found " + count + " client entries, seed is:\n" +
     619                            net.i2p.util.HexDump.dump(seed));
     620            }
     621            byte[] clientKey = new byte[32];
     622            byte[] clientIVandID = new byte[32];
     623            if (authType == BlindData.AUTH_DH) {
     624                // seed is public key
     625                PublicKey epk = new PublicKey(EncType.ECIES_X25519, seed);
     626                SessionKey dh = X25519DH.dh(csk, epk);
     627                // HKDF input is 100 bytes
     628                byte[] clientAuthInput = new byte[64 + authInput.length];
     629                System.arraycopy(dh.getData(), 0, clientAuthInput, 0, 32);
     630                // TODO cache pubkey, either in PrivateKey or use KeyPair
     631                PublicKey cpk = csk.toPublic();
     632                System.arraycopy(cpk.getData(), 0, clientAuthInput, 32, 32);
     633                // we copy over end of authInput from above
     634                // subcredential and timestamp remain unchanged
     635                System.arraycopy(authInput, 0, clientAuthInput, 64, 36);
     636                hkdf.calculate(seed, clientAuthInput, ELS2_DH, clientKey, clientIVandID, 0);
     637            } else {
     638                // PSK
     639                // HKDF input is 68 bytes                        `
     640                // we reuse authInput from above, just replace the first 32 bytes.
     641                // subcredential and timestamp remain unchanged
     642                byte[] clientAuthInput = new byte[32 + authInput.length];
     643                System.arraycopy(csk.getData(), 0, clientAuthInput, 0, 32);
     644                // we copy over authInput from above
     645                // subcredential and timestamp remain unchanged
     646                System.arraycopy(authInput, 0, clientAuthInput, 32, 36);
     647                hkdf.calculate(seed, clientAuthInput, ELS2_PSK, clientKey, clientIVandID, 0);
     648            }
     649            if (_log.shouldDebug()) {
     650                 _log.debug("Looking for client ID:\n" +
     651                            net.i2p.util.HexDump.dump(clientIVandID, IV_LEN, ID_LEN) +
     652                            "for client key:\n" +
     653                            net.i2p.util.HexDump.dump(clientKey));
     654            }
     655            int off = 35;
     656            byte[] clientCookie = null;
     657            for (int i = 0; i < count; i++) {
     658                if (DataHelper.eq(clientIVandID, IV_LEN, plaintext, off, ID_LEN)) {
     659                    clientCookie = new byte[32];
     660                    System.arraycopy(plaintext, off + ID_LEN, clientCookie, 0, 32);
     661                    break;
     662                }
     663                off += CLIENT_LEN;
     664            }
     665            if (clientCookie == null)
     666                throw new DataFormatException("Our client auth entry not found");
     667            if (_log.shouldDebug()) {
     668                 _log.debug("Found client cookie:\n" +
     669                            net.i2p.util.HexDump.dump(clientCookie));
     670            }
     671            byte[] clientAuthInput = new byte[32 + authInput.length];
     672            // we copy over end of authInput from above
     673            // subcredential and timestamp remain unchanged
     674            System.arraycopy(authInput, 0, clientAuthInput, 32, 36);
     675            // decrypt clientCookie to clientAuthInput
     676            ChaCha20.decrypt(clientKey, clientIVandID, clientCookie, 0, clientAuthInput, 0, 32);
     677            if (_log.shouldDebug()) {
     678                 _log.debug("Decrypted client cookie:\n" +
     679                            net.i2p.util.HexDump.dump(clientAuthInput, 0, 32));
     680            }
     681            authInput = clientAuthInput;
    453682        }
    454683
     
    456685        // reuse input (because there's no authcookie), get new salt/key/iv
    457686        ciphertext = plaintext;
    458         plaintext = new byte[ciphertext.length  - (1 + SALT_LEN)];
     687        plaintext = new byte[ciphertext.length  - (authLen + SALT_LEN)];
    459688        byte[] salt = new byte[SALT_LEN];
    460         System.arraycopy(ciphertext, 1, salt, 0, SALT_LEN);
    461         hkdf.calculate(salt, input, ELS2L2K, key, iv, 0);
    462         ChaCha20.decrypt(key, iv, ciphertext, 1 + SALT_LEN, plaintext, 0, plaintext.length);
     689        System.arraycopy(ciphertext, authLen, salt, 0, SALT_LEN);
     690        if (_log.shouldDebug()) {
     691            _log.debug("Inner HKDF salt:\n" +
     692                       net.i2p.util.HexDump.dump(salt) +
     693                       "Inner HKDF input:\n" +
     694                       net.i2p.util.HexDump.dump(authInput));
     695        }
     696        hkdf.calculate(salt, authInput, ELS2L2K, key, iv, 0);
     697        ChaCha20.decrypt(key, iv, ciphertext, authLen + SALT_LEN, plaintext, 0, plaintext.length);
    463698        if (_log.shouldDebug())
    464699            _log.debug("Decrypt: inner plaintext:\n" + net.i2p.util.HexDump.dump(plaintext));
     
    471706            innerLS2 = new MetaLeaseSet();
    472707        else
    473             throw new DataFormatException("Unsupported LS type: " + type);
     708            throw new DataFormatException("Bad decryption or unsupported LS type: " + type);
    474709        innerLS2.readBytes(bais);
    475710        _decryptedLS2 = innerLS2;
     
    477712
    478713    /**
    479      *  The HKDF input
     714     *  The HKDF input (no per-client auth)
    480715     *
    481716     *  @return 36 bytes
     
    487722        System.arraycopy(subcredential, 0, rv, 0, subcredential.length);
    488723        DataHelper.toLong(rv, subcredential.length, 4, _published / 1000);
     724        return rv;
     725    }
     726
     727    /**
     728     *  The HKDF input (with per-client auth)
     729     *
     730     *  @param authcookie 32 bytes
     731     *  @return 68 bytes
     732     *  @since 0.9.41
     733     */
     734    private byte[] getHKDFInput(I2PAppContext ctx, byte[] authcookie) {
     735        byte[] subcredential = getSubcredential(ctx);
     736        byte[] rv = new byte[authcookie.length + subcredential.length + 4];
     737        System.arraycopy(authcookie, 0, rv, 0, authcookie.length);
     738        System.arraycopy(subcredential, 0, rv, authcookie.length, subcredential.length);
     739        DataHelper.toLong(rv, authcookie.length + subcredential.length, 4, _published / 1000);
    489740        return rv;
    490741    }
     
    538789    @Override
    539790    public void sign(SigningPrivateKey key) throws DataFormatException {
     791        sign(key, BlindData.AUTH_NONE, null);
     792    }
     793
     794    /**
     795     * Sign the structure using the supplied signing key.
     796     * Overridden because we sign the inner, then blind and encrypt
     797     * and sign the outer.
     798     *
     799     * @param authType 0, 1, or 3, see BlindData
     800     * @param clientKeys X25519 public keys for DH, private keys for PSK
     801     * @throws IllegalStateException if already signed
     802     * @since 0.9.41
     803     */
     804    public void sign(SigningPrivateKey key, int authType, List<? extends SimpleDataStructure> clientKeys) throws DataFormatException {
    540805        // now sign inner with the unblinded key
    541806        // inner LS is always unpublished
     
    549814            _log.debug("Inner sig: " + _signature.getType() + ' ' + _signature.toBase64());
    550815        }
    551         encrypt(null);
     816        encrypt(authType, clientKeys);
    552817        SigningPrivateKey bkey = Blinding.blind(key, _alpha);
    553818        int len = size();
     
    581846    @Override
    582847    public boolean verifySignature() {
     848        return verifySignature((PrivateKey) null);
     849    }
     850
     851    /**
     852     * Decrypt if possible, and verify inner sig also.
     853     *
     854     * Must call setDestination() prior to this if attempting decryption.
     855     *
     856     * @param clientKey PrivateKey for DH or PSK, or null if none
     857     * @return valid
     858     * @since 0.9.41
     859     */
     860    public boolean verifySignature(PrivateKey clientKey) {
    583861        // TODO use fields in super
    584862        if (_decryptedLS2 != null)
     
    599877        }
    600878        try {
    601             decrypt();
     879            decrypt(clientKey);
    602880        } catch (DataFormatException dfe) {
    603881            _log.warn("ELS2 decrypt fail", dfe);
     
    685963        System.out.println("Online test");
    686964        java.io.File f2 = new java.io.File("online-encls2.dat");
    687         test(pkf, f2, false);
     965        test(pkf, f2, false, BlindData.AUTH_NONE, null);
     966        List<KeyPair> keys = new java.util.ArrayList<KeyPair>(4);
     967        for (int i = 0; i < 4; i++) {
     968            keys.add(net.i2p.crypto.KeyGenerator.getInstance().generatePKIKeys(net.i2p.crypto.EncType.ECIES_X25519));
     969        }
     970        System.out.println("Online test with DH Keys");
     971        test(pkf, f2, false, BlindData.AUTH_DH, keys);
     972        System.out.println("Online test with PSK Keys");
     973        test(pkf, f2, false, BlindData.AUTH_PSK, keys);
    688974        //System.out.println("Offline test");
    689975        //f2 = new java.io.File("offline-encls2.dat");
     
    691977    }
    692978
    693     private static void test(PrivateKeyFile pkf, java.io.File outfile, boolean offline) throws Exception {
     979    private static void test(PrivateKeyFile pkf, java.io.File outfile, boolean offline, int authType, List<KeyPair> clientKeys) throws Exception {
    694980        net.i2p.util.RandomSource rand = net.i2p.util.RandomSource.getInstance();
    695981        long now = System.currentTimeMillis() + 5*60*1000;
     
    7271013            ls2.sign(transientPriv);
    7281014        } else {
    729             ls2.sign(spk);
     1015            List<SimpleDataStructure> signkeys = null;
     1016            if (authType != BlindData.AUTH_NONE) {
     1017                signkeys = new java.util.ArrayList<SimpleDataStructure>();
     1018                for (KeyPair kp : clientKeys) {
     1019                    if (authType == BlindData.AUTH_DH)
     1020                       signkeys.add(kp.getPublic());
     1021                    else
     1022                       signkeys.add(kp.getPrivate());
     1023                }
     1024            }
     1025            ls2.sign(spk, authType, signkeys);
    7301026        }
    7311027        System.out.println("\nCreated: " + ls2);
    732         if (!ls2.verifySignature()) {
     1028        PrivateKey verifyKey = null;
     1029        if (authType != BlindData.AUTH_NONE)
     1030            verifyKey = clientKeys.get(0).getPrivate();
     1031        if (!ls2.verifySignature(verifyKey)) {
     1032            I2PAppContext.getGlobalContext().logManager().flush();
    7331033            System.out.println("Verify FAILED");
    7341034            return;
     
    7471047        // required to decrypt
    7481048        ls3.setDestination(pkf.getDestination());
    749         if (!ls3.verifySignature())
     1049        if (!ls3.verifySignature(verifyKey))
    7501050            System.out.println("Verify FAILED");
     1051        I2PAppContext.getGlobalContext().logManager().flush();
    7511052    }
    7521053****/
Note: See TracChangeset for help on using the changeset viewer.