Skip to content

Commit

Permalink
Add KEM abstraction layer
Browse files Browse the repository at this point in the history
Move lower-level code away from entry crypto module
  • Loading branch information
larabr committed Nov 2, 2023
1 parent 388057e commit f2cc5ce
Show file tree
Hide file tree
Showing 12 changed files with 124 additions and 97 deletions.
41 changes: 14 additions & 27 deletions src/crypto/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import util from '../util';
import OID from '../type/oid';
import { UnsupportedError } from '../packet/packet';
import ECDHXSymmetricKey from '../type/ecdh_x_symkey';
import * as aesKW from './aes_kw';

/**
* Encrypts data using specified algorithm and public key parameters.
Expand Down Expand Up @@ -97,14 +96,10 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, pri
const c = await modeInstance.encrypt(data, iv, new Uint8Array());
return { aeadMode: new AEADEnum(aeadMode), iv, c: new ShortByteString(c) };
}
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const { eccPublicKey, mlkemPublicKey } = publicParams;
const { eccKeyShare, eccCipherText } = await publicKey.pqc.kem.ecdhX.encaps(keyAlgo, eccPublicKey);
const { mlkemKeyShare, mlkemCipherText } = await publicKey.pqc.kem.ml.encaps(keyAlgo, mlkemPublicKey);
const fixedInfo = new Uint8Array([keyAlgo]);
const kek = await publicKey.pqc.kem.multiKeyCombine(eccKeyShare, eccCipherText, mlkemKeyShare, mlkemCipherText, fixedInfo, 256);
const C = aesKW.wrap(kek, data);
return { eccCipherText, mlkemCipherText, C: new ShortByteString(C) }; // eccCipherText || mlkemCipherText || len(C) || C
const { eccCipherText, mlkemCipherText, wrappedKey } = await publicKey.postQuantum.kem.encrypt(keyAlgo, eccPublicKey, mlkemPublicKey, data);
return { eccCipherText, mlkemCipherText, C: new ShortByteString(wrappedKey) };
}
default:
return [];
Expand Down Expand Up @@ -169,16 +164,11 @@ export async function publicKeyDecrypt(keyAlgo, publicKeyParams, privateKeyParam
const modeInstance = await mode(algoValue, keyMaterial);
return modeInstance.decrypt(c.data, iv, new Uint8Array());
}
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const { eccSecretKey, mlkemSecretKey } = privateKeyParams;
const { eccPublicKey } = publicKeyParams;
const { eccCipherText, mlkemCipherText, C } = sessionKeyParams;
const eccKeyShare = await publicKey.pqc.kem.ecdhX.decaps(keyAlgo, eccCipherText, eccSecretKey, eccPublicKey);
const mlkemKeyShare = await publicKey.pqc.kem.ml.decaps(keyAlgo, mlkemCipherText, mlkemSecretKey);
const fixedInfo = new Uint8Array([keyAlgo]);
const kek = await publicKey.pqc.kem.multiKeyCombine(eccKeyShare, eccCipherText, mlkemKeyShare, mlkemCipherText, fixedInfo, 256);
const sessionKey = aesKW.unwrap(kek, C.data);
return sessionKey;
return publicKey.postQuantum.kem.decrypt(keyAlgo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, C.data);
}
default:
throw new Error('Unknown public key encryption algorithm.');
Expand Down Expand Up @@ -248,7 +238,7 @@ export function parsePublicKeyParams(algo, bytes) {
const digest = bytes.subarray(read, read + digestLength); read += digestLength;
return { read: read, publicParams: { cipher: algo, digest } };
}
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const eccPublicKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccPublicKey.length;
const mlkemPublicKey = util.readExactSubarray(bytes, read, read + (1184 / 8)); read += mlkemPublicKey.length;
return { read, publicParams: { eccPublicKey, mlkemPublicKey } };
Expand Down Expand Up @@ -321,7 +311,7 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) {
const keyMaterial = bytes.subarray(read, read + keySize); read += keySize;
return { read, privateParams: { hashSeed, keyMaterial } };
}
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const eccSecretKey = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccSecretKey.length;
const mlkemSecretKey = util.readExactSubarray(bytes, read, read + (2400 / 8)); read += mlkemSecretKey.length;
return { read, privateParams: { eccSecretKey, mlkemSecretKey } };
Expand Down Expand Up @@ -389,7 +379,7 @@ export function parseEncSessionKeyParams(algo, bytes) {

return { aeadMode, iv, c };
}
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const eccCipherText = util.readExactSubarray(bytes, read, read + getCurvePayloadSize(enums.publicKey.x25519)); read += eccCipherText.length;
const mlkemCipherText = util.readExactSubarray(bytes, read, read + (1088 / 8)); read += mlkemCipherText.length;
const C = new ShortByteString(); read += C.read(bytes.subarray(read));
Expand All @@ -415,7 +405,7 @@ export function serializeParams(algo, params) {
enums.publicKey.x448,
enums.publicKey.aead,
enums.publicKey.hmac,
enums.publicKey.kem_x25519
enums.publicKey.pqc_mlkem_x25519
]);
const orderedParams = Object.keys(params).map(name => {
const param = params[name];
Expand Down Expand Up @@ -482,14 +472,11 @@ export async function generateParams(algo, bits, oid, symmetric) {
const keyMaterial = generateSessionKey(symmetric);
return createSymmetricParams(keyMaterial, new SymAlgoEnum(symmetric));
}
case enums.publicKey.kem_x25519: {
const eccKeyPair = await publicKey.elliptic.ecdhX.generate(enums.publicKey.x25519); // todo move into kem_echd_x?
const mlkemKeyPair = await publicKey.pqc.kem.ml.generate(algo);
return {
privateParams: { eccSecretKey: eccKeyPair.k, mlkemSecretKey: mlkemKeyPair.decapsulationKey },
publicParams: { eccPublicKey: eccKeyPair.A, mlkemPublicKey: mlkemKeyPair.encapsulationKey }
};
}
case enums.publicKey.pqc_mlkem_x25519:
return publicKey.postQuantum.kem.generate(algo).then(({ eccSecretKey, eccPublicKey, mlkemSecretKey, mlkemPublicKey }) => ({
privateParams: { eccSecretKey, mlkemSecretKey },
publicParams: { eccPublicKey, mlkemPublicKey }
}));
case enums.publicKey.dsa:
case enums.publicKey.elgamal:
throw new Error('Unsupported algorithm for key generation.');
Expand Down
39 changes: 14 additions & 25 deletions src/crypto/public_key/elliptic/ecdh_x.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,27 +87,22 @@ export async function validateParams(algo, A, k) {
* @async
*/
export async function encrypt(algo, data, recipientA) {
const { ephemeralPublicKey } = await generateEphemeralKeyPair(algo);
const sharedSecret = await getSharedSecret(algo, recipientA);
const { ephemeralPublicKey, ephemeralSecretKey } = await generateEphemeralKeyPair(algo);
const sharedSecret = await getSharedSecret(algo, ephemeralSecretKey, recipientA);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
recipientA,
sharedSecret
]);

switch (algo) {
case enums.publicKey.x25519: {
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
recipientA,
sharedSecret
]);
const { keySize } = getCipher(enums.symmetric.aes128);
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
const wrappedKey = aesKW.wrap(encryptionKey, data);
return { ephemeralPublicKey, wrappedKey };
}
case enums.publicKey.x448: {
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
recipientA,
sharedSecret
]);
const { keySize } = getCipher(enums.symmetric.aes256);
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
const wrappedKey = aesKW.wrap(encryptionKey, data);
Expand All @@ -131,26 +126,20 @@ export async function encrypt(algo, data, recipientA) {
* @async
*/
export async function decrypt(algo, ephemeralPublicKey, wrappedKey, A, k) {
const sharedSecret = await getSharedSecret(algo, k, ephemeralPublicKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
A,
sharedSecret
]);

switch (algo) {
case enums.publicKey.x25519: {
const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
A,
sharedSecret
]);
const { keySize } = getCipher(enums.symmetric.aes128);
const encryptionKey = await computeHKDF(enums.hash.sha256, hkdfInput, new Uint8Array(), HKDF_INFO.x25519, keySize);
return aesKW.unwrap(encryptionKey, wrappedKey);
}
case enums.publicKey.x448: {
const x448 = await util.getNobleCurve(enums.publicKey.x448);
const sharedSecret = x448.getSharedSecret(k, ephemeralPublicKey);
const hkdfInput = util.concatUint8Array([
ephemeralPublicKey,
A,
sharedSecret
]);
const { keySize } = getCipher(enums.symmetric.aes256);
const encryptionKey = await computeHKDF(enums.hash.sha512, hkdfInput, new Uint8Array(), HKDF_INFO.x448, keySize);
return aesKW.unwrap(encryptionKey, wrappedKey);
Expand Down
5 changes: 3 additions & 2 deletions src/crypto/public_key/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as elgamal from './elgamal';
import * as elliptic from './elliptic';
import * as dsa from './dsa';
import * as hmac from './hmac';
import * as pqc from './pqc';
import * as postQuantum from './post_quantum';

export default {
/** @see module:crypto/public_key/rsa */
Expand All @@ -21,5 +21,6 @@ export default {
dsa: dsa,
/** @see module:crypto/public_key/hmac */
hmac: hmac,
pqc
/** @see module:crypto/public_key/post_quantum */
postQuantum
};
5 changes: 5 additions & 0 deletions src/crypto/public_key/post_quantum/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as kem from './kem/index';

export {
kem
};
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import * as ecdhX from '../elliptic/ecdh_x';
import hash from '../../hash';
import util from '../../../util';
import enums from '../../../enums';
import * as ecdhX from '../../elliptic/ecdh_x';
import hash from '../../../hash';
import util from '../../../../util';
import enums from '../../../../enums';

export async function encaps(eccAlgo, eccRecipientPublicKey) {
switch (eccAlgo) {
case enums.publicKey.kem_x25519: {
export async function generate(algo) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { A, k } = await ecdhX.generate(enums.publicKey.x25519);
return {
eccPublicKey: A,
eccSecretKey: k
};
}
default:
throw new Error('Unsupported KEM algorithm');
}
}

export async function encaps(algo, eccRecipientPublicKey) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const { ephemeralPublicKey: eccCipherText, ephemeralSecretKey } = await ecdhX.generateEphemeralKeyPair(enums.publicKey.x25519);
const X = await ecdhX.getSharedSecret(enums.publicKey.x25519, ephemeralSecretKey, eccRecipientPublicKey);
const eccKeyShare = await hash.sha3_256(util.concatUint8Array([
Expand All @@ -23,9 +37,9 @@ export async function encaps(eccAlgo, eccRecipientPublicKey) {
}
}

export async function decaps(eccAlgo, eccCipherText, eccSecretKey, eccPublicKey) {
switch (eccAlgo) {
case enums.publicKey.kem_x25519: {
export async function decaps(algo, eccCipherText, eccSecretKey, eccPublicKey) {
switch (algo) {
case enums.publicKey.pqc_mlkem_x25519: {
const X = await ecdhX.getSharedSecret(enums.publicKey.x25519, eccSecretKey, eccCipherText);
const eccKeyShare = await hash.sha3_256(util.concatUint8Array([
X,
Expand Down
1 change: 1 addition & 0 deletions src/crypto/public_key/post_quantum/kem/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { generate, encrypt, decrypt } from './kem';
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import util from '../../../util';
import * as ecdhX from './kem_ecdh_x';
import * as ml from './kem_ml';
import * as eccKem from './ecc_kem';
import * as mlKem from './ml_kem';
import * as aesKW from '../../../aes_kw';
import util from '../../../../util';

const kem = { ecdhX, ml, multiKeyCombine };
export {
kem
};
export async function generate(algo) {
const { eccPublicKey, eccSecretKey } = await eccKem.generate(algo);
const { mlkemPublicKey, mlkemSecretKey } = await mlKem.generate(algo);

return { eccPublicKey, eccSecretKey, mlkemPublicKey, mlkemSecretKey };
}

export async function encrypt(algo, eccPublicKey, mlkemPublicKey, sessioneKeyData) {
const { eccKeyShare, eccCipherText } = await eccKem.encaps(algo, eccPublicKey);
const { mlkemKeyShare, mlkemCipherText } = await mlKem.encaps(algo, mlkemPublicKey);
const fixedInfo = new Uint8Array([algo]);
const kek = await multiKeyCombine(eccKeyShare, eccCipherText, mlkemKeyShare, mlkemCipherText, fixedInfo, 256);
const wrappedKey = aesKW.wrap(kek, sessioneKeyData); // C
return { eccCipherText, mlkemCipherText, wrappedKey };
}

export async function decrypt(algo, eccCipherText, mlkemCipherText, eccSecretKey, eccPublicKey, mlkemSecretKey, encryptedSessionKeyData) {
const eccKeyShare = await eccKem.decaps(algo, eccCipherText, eccSecretKey, eccPublicKey);
const mlkemKeyShare = await mlKem.decaps(algo, mlkemCipherText, mlkemSecretKey);
const fixedInfo = new Uint8Array([algo]);
const kek = await multiKeyCombine(eccKeyShare, eccCipherText, mlkemKeyShare, mlkemCipherText, fixedInfo, 256);
const sessionKey = aesKW.unwrap(kek, encryptedSessionKeyData);
return sessionKey;
}

async function multiKeyCombine(eccKeyShare, eccCipherText, mlkemKeyShare, mlkemCipherText, fixedInfo, outputBits) {
// multiKeyCombine(eccKeyShare, eccCipherText,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
import enums from '../../../enums';
import enums from '../../../../enums';

export async function generate(algo) {
switch (algo) {
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const { Kyber768 } = await import('crystals-kyber-js');
const kyberInstance = new Kyber768();
const [encapsulationKey, decapsulationKey] = await kyberInstance.generateKeyPair();

return { encapsulationKey, decapsulationKey };
return { mlkemPublicKey: encapsulationKey, mlkemSecretKey: decapsulationKey };
}
default:
throw new Error('Unsupported KEM algorithm');
}
}

export async function encaps(algo, mlkemRecipientPublicKey) {
switch (algo) {
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const { Kyber768 } = await import('crystals-kyber-js');
const kyberInstance = new Kyber768();
const [mlkemCipherText, mlkemKeyShare] = await kyberInstance.encap(mlkemRecipientPublicKey);

return { mlkemCipherText, mlkemKeyShare };
}
default:
throw new Error('Unsupported KEM algorithm');
}
}

export async function decaps(algo, mlkemCipherText, mlkemSecretKey) {
switch (algo) {
case enums.publicKey.kem_x25519: {
case enums.publicKey.pqc_mlkem_x25519: {
const { Kyber768 } = await import('crystals-kyber-js');
const kyberInstance = new Kyber768();
const mlkemKeyShare = await kyberInstance.decap(mlkemCipherText, mlkemSecretKey);

return mlkemKeyShare;
}
default:
throw new Error('Unsupported KEM algorithm');
}
}
3 changes: 3 additions & 0 deletions src/enums.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ export default {
ed25519: 27,
/** Ed448 (Sign only) */
ed448: 28,
/** Post-quantum ML-KEM-768 + X25519 */
pqc_mlkem_x25519: 29,

/** Persistent symmetric keys: encryption algorithm */
aead: 100,
/** Persistent symmetric keys: authentication algorithm */
Expand Down
30 changes: 15 additions & 15 deletions test/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ import testEAX from './eax';
import testOCB from './ocb';
import testRSA from './rsa';
import testValidate from './validate';
import testPQC from './pqc';
import testPQC from './postQuantum';

export default () => describe('Crypto', function () {
// testCipher();
// testHash();
// testCrypto();
// testElliptic();
// testECDH();
// testPKCS5();
// testAESKW();
// testHKDF();
// testHMAC();
// testGCM();
// testEAX();
// testOCB();
// testRSA();
// testValidate();
testCipher();
testHash();
testCrypto();
testElliptic();
testECDH();
testPKCS5();
testAESKW();
testHKDF();
testHMAC();
testGCM();
testEAX();
testOCB();
testRSA();
testValidate();
testPQC();
});
6 changes: 3 additions & 3 deletions test/crypto/pqc.js → test/crypto/postQuantum.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export default () => describe('PQC - Kyber + X25519', function () {
it('Generate/encaps/decaps', async function () {
const sessionKey = { data: new Uint8Array(16).fill(1), algorithm: 'aes128' };

const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.kem_x25519);
const encryptedSessionKeyParams = await publicKeyEncrypt(openpgp.enums.publicKey.kem_x25519, undefined, publicParams, null, sessionKey.data);
const decryptedSessionKey = await publicKeyDecrypt(openpgp.enums.publicKey.kem_x25519, publicParams, privateParams, encryptedSessionKeyParams);
const { privateParams, publicParams } = await generateParams(openpgp.enums.publicKey.pqc_mlkem_x25519);
const encryptedSessionKeyParams = await publicKeyEncrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, undefined, publicParams, null, sessionKey.data);
const decryptedSessionKey = await publicKeyDecrypt(openpgp.enums.publicKey.pqc_mlkem_x25519, publicParams, privateParams, encryptedSessionKeyParams);
expect(decryptedSessionKey).to.deep.equal(sessionKey.data);
});
});
Loading

0 comments on commit f2cc5ce

Please sign in to comment.