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

This commit is contained in:
zzz
2019-05-22 16:11:36 +00:00
parent cb762356da
commit 06fa817bde

View File

@ -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<? extends SimpleDataStructure> 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<? extends SimpleDataStructure> 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<KeyPair> keys = new java.util.ArrayList<KeyPair>(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<KeyPair> 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<SimpleDataStructure> signkeys = null;
if (authType != BlindData.AUTH_NONE) {
signkeys = new java.util.ArrayList<SimpleDataStructure>();
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();
}
****/
}