From bd12b20a38601e1ab132d38ce52cce2ef046fc4f Mon Sep 17 00:00:00 2001 From: Andres Pinto Date: Thu, 30 Jan 2025 23:08:46 -0400 Subject: [PATCH] feat initial commit kyber pre creted users --- .../hybrib-encryption.ts | 118 ++++++++++++++++++ src/lib/assymetric-encryption/kyber.ts | 59 +++++++++ .../{ => assymetric-encryption}/openpgp.ts | 0 src/lib/assymetric-encryption/utils.ts | 34 +++++ src/modules/user/pre-created-user.domain.ts | 7 ++ .../user/pre-created-users.attributes.ts | 2 + src/modules/user/pre-created-users.model.ts | 8 ++ src/modules/user/user.controller.ts | 4 + src/modules/user/user.usecase.spec.ts | 2 +- src/modules/user/user.usecase.ts | 15 ++- 10 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 src/lib/assymetric-encryption/hybrib-encryption.ts create mode 100644 src/lib/assymetric-encryption/kyber.ts rename src/lib/{ => assymetric-encryption}/openpgp.ts (100%) create mode 100644 src/lib/assymetric-encryption/utils.ts diff --git a/src/lib/assymetric-encryption/hybrib-encryption.ts b/src/lib/assymetric-encryption/hybrib-encryption.ts new file mode 100644 index 000000000..8faa584c1 --- /dev/null +++ b/src/lib/assymetric-encryption/hybrib-encryption.ts @@ -0,0 +1,118 @@ +import { + decryptMessageWithPrivateKey, + encryptMessageWithPublicKey, +} from './openpgp'; +import { Kyber512 } from './kyber'; +import { extendSecret, XORhex } from './utils'; + +const WORDS_HYBRID_MODE_IN_BASE64 = 'SHlicmlkTW9kZQ=='; // 'HybridMode' in BASE64 format + +/** + * Encrypts message using hybrid method (ecc and kyber) if kyber key is given, else uses ecc only + * + * @param {Object} params - The parameters object. + * @param {string} params.message - The message to encrypt. + * @param {string} params.publicKeyInBase64 - The ECC public key in Base64 encoding. + * @param {string} [params.publicKyberKeyBase64] - The Kyber public key in Base64 encoding. Optional. + * @returns {Promise} The encrypted message as a Base64-encoded string. + * @throws {Error} If both ECC and Kyber keys are required but one is missing. + */ +export const hybridEncryptMessageWithPublicKey = async ({ + message, + publicKeyInBase64, + publicKyberKeyBase64, +}: { + message: string; + publicKeyInBase64: string; + publicKyberKeyBase64?: string; +}): Promise => { + let result = ''; + let plaintext = message; + if (publicKyberKeyBase64) { + const kem = new Kyber512(); + + const publicKyberKey = Buffer.from(publicKyberKeyBase64, 'base64'); + const { ciphertext, sharedSecret: secret } = await kem.encapsulate( + new Uint8Array(publicKyberKey), + ); + const kyberCiphertextStr = Buffer.from(ciphertext).toString('base64'); + + const bits = message.length * 8; + const secretHex = await extendSecret(secret, bits); + const messageHex = Buffer.from(message).toString('hex'); + + plaintext = XORhex(messageHex, secretHex); + result = WORDS_HYBRID_MODE_IN_BASE64.concat('$', kyberCiphertextStr, '$'); + } + + const encryptedMessage = await encryptMessageWithPublicKey({ + message: plaintext, + publicKeyInBase64, + }); + const eccCiphertextStr = btoa(encryptedMessage as string); + + result = result.concat(eccCiphertextStr); + + return result; +}; + +/** + * Decrypts ciphertext using hybrid method (ecc and kyber) if kyber key is given, else uses ecc only + * + * @param {Object} params - The parameters object. + * @param {string} params.encryptedMessageInBase64 - The encrypted message as a Base64-encoded string. + * @param {string} params.privateKeyInBase64 - The ECC private key in Base64 encoding. + * @param {string} [params.privateKyberKeyInBase64] - The Kyber private key in Base64 encoding. Optional. + * @returns {Promise} The decrypted message as a plain string. + * @throws {Error} If attempting to decrypt a hybrid message without the required Kyber private key. + */ +export const hybridDecryptMessageWithPrivateKey = async ({ + encryptedMessageInBase64, + privateKeyInBase64, + privateKyberKeyInBase64, +}: { + encryptedMessageInBase64: string; + privateKeyInBase64: string; + privateKyberKeyInBase64?: string; +}): Promise => { + let eccCiphertextStr = encryptedMessageInBase64; + let kyberSecret; + + const ciphertexts = encryptedMessageInBase64.split('$'); + const prefix = ciphertexts[0]; + const isHybridMode = prefix === WORDS_HYBRID_MODE_IN_BASE64; + + if (isHybridMode) { + if (!privateKyberKeyInBase64) { + return Promise.reject( + new Error('Attempted to decrypt hybrid ciphertex without Kyber key'), + ); + } + const kem = new Kyber512(); + + const kyberCiphertextBase64 = ciphertexts[1]; + eccCiphertextStr = ciphertexts[2]; + + const privateKyberKey = Buffer.from(privateKyberKeyInBase64, 'base64'); + const kyberCiphertext = Buffer.from(kyberCiphertextBase64, 'base64'); + const decapsulateSharedSecret = await kem.decapsulate( + new Uint8Array(kyberCiphertext), + new Uint8Array(privateKyberKey), + ); + kyberSecret = decapsulateSharedSecret; + } + + const decryptedMessage = await decryptMessageWithPrivateKey({ + encryptedMessage: atob(eccCiphertextStr), + privateKeyInBase64, + }); + let result = decryptedMessage as string; + if (isHybridMode) { + const bits = result.length * 4; + const secretHex = await extendSecret(kyberSecret, bits); + const xored = XORhex(result, secretHex); + result = Buffer.from(xored, 'hex').toString('utf8'); + } + + return result; +}; diff --git a/src/lib/assymetric-encryption/kyber.ts b/src/lib/assymetric-encryption/kyber.ts new file mode 100644 index 000000000..4e907ea97 --- /dev/null +++ b/src/lib/assymetric-encryption/kyber.ts @@ -0,0 +1,59 @@ +export async function getKemBuilder() { + const kemBuilder = await import('@dashlane/pqc-kem-kyber512-node'); + return kemBuilder.default; +} + +export class Kyber512 { + private kem: Awaited>>>; + + constructor() {} + + async init() { + const kemBuilder = await getKemBuilder(); + this.kem = await kemBuilder(); + } + + /** + * Generates a new Kyber-512 key pair. + */ + async generateKeys(): Promise<{ + publicKey: Uint8Array; + privateKey: Uint8Array; + }> { + if (!this.kem) await this.init(); + return this.kem.keypair(); + } + + async generateKeysInBase64() { + const keys = await this.generateKeys(); + + return { + publicKey: Buffer.from(keys.publicKey).toString('base64'), + privateKey: Buffer.from(keys.privateKey).toString('base64'), + }; + } + + /** + * Encrypts a message using the recipient's public key. + * Kyber encapsulates a shared secret along with a ciphertext. + */ + async encapsulate( + publicKey: Uint8Array, + ): Promise<{ ciphertext: Uint8Array; sharedSecret: Uint8Array }> { + if (!this.kem) await this.init(); + return this.kem.encapsulate(publicKey); + } + + /** + * Decrypts the ciphertext using the recipient's private key. + * Returns the shared secret that matches the one from encryption. + */ + async decapsulate( + ciphertext: Uint8Array, + privateKey: Uint8Array, + ): Promise { + if (!this.kem) await this.init(); + const { sharedSecret } = await this.kem.decapsulate(ciphertext, privateKey); + return sharedSecret; + } +} diff --git a/src/lib/openpgp.ts b/src/lib/assymetric-encryption/openpgp.ts similarity index 100% rename from src/lib/openpgp.ts rename to src/lib/assymetric-encryption/openpgp.ts diff --git a/src/lib/assymetric-encryption/utils.ts b/src/lib/assymetric-encryption/utils.ts new file mode 100644 index 000000000..bade3af60 --- /dev/null +++ b/src/lib/assymetric-encryption/utils.ts @@ -0,0 +1,34 @@ +import { blake3 } from 'hash-wasm'; + +/** + * Extends the given secret to the required number of bits + * @param {string} secret - The original secret + * @param {number} length - The desired bitlength + * @returns {Promise} The extended secret of the desired bitlength + */ +export function extendSecret( + secret: Uint8Array, + length: number, +): Promise { + return blake3(secret, length); +} + +/** + * XORs two strings of the identical length + * @param {string} a - The first string + * @param {string} b - The second string + * @returns {string} The result of XOR of strings a and b. + */ +export function XORhex(a: string, b: string): string { + let res = '', + i = a.length, + j = b.length; + if (i != j) { + throw new Error('Can XOR only strings with identical length'); + } + while (i-- > 0 && j-- > 0) + res = + (parseInt(a.charAt(i), 16) ^ parseInt(b.charAt(j), 16)).toString(16) + + res; + return res; +} diff --git a/src/modules/user/pre-created-user.domain.ts b/src/modules/user/pre-created-user.domain.ts index abe520ea2..8db2ee4f5 100644 --- a/src/modules/user/pre-created-user.domain.ts +++ b/src/modules/user/pre-created-user.domain.ts @@ -13,6 +13,9 @@ export class PreCreatedUser implements PreCreatedUserAttributes { privateKey: string; revocationKey: string; encryptVersion: UserKeysEncryptVersions; + publicKyberKey?: string; + privateKyberKey?: string; + constructor({ id, email, @@ -25,12 +28,16 @@ export class PreCreatedUser implements PreCreatedUserAttributes { privateKey, revocationKey, encryptVersion, + publicKyberKey, + privateKyberKey, }: PreCreatedUserAttributes) { this.id = id; this.uuid = uuid; this.email = email; this.publicKey = publicKey; this.privateKey = privateKey; + this.publicKyberKey = publicKyberKey; + this.privateKyberKey = privateKyberKey; this.revocationKey = revocationKey; this.encryptVersion = encryptVersion; this.username = username; diff --git a/src/modules/user/pre-created-users.attributes.ts b/src/modules/user/pre-created-users.attributes.ts index 7e1f2b9a3..add8eeecb 100644 --- a/src/modules/user/pre-created-users.attributes.ts +++ b/src/modules/user/pre-created-users.attributes.ts @@ -11,6 +11,8 @@ export interface PreCreatedUserAttributes { uuid: UserAttributes['uuid']; publicKey: KeyServerAttributes['publicKey']; privateKey: KeyServerAttributes['privateKey']; + publicKyberKey?: KeyServerAttributes['publicKey']; + privateKyberKey?: KeyServerAttributes['privateKey']; revocationKey: KeyServerAttributes['revocationKey']; encryptVersion: KeyServerAttributes['encryptVersion']; } diff --git a/src/modules/user/pre-created-users.model.ts b/src/modules/user/pre-created-users.model.ts index 626948a11..789827d11 100644 --- a/src/modules/user/pre-created-users.model.ts +++ b/src/modules/user/pre-created-users.model.ts @@ -48,6 +48,14 @@ export class PreCreatedUserModel @Column(DataType.STRING) encryptVersion: KeyServerAttributes['encryptVersion']; + @AllowNull(true) + @Column(DataType.STRING(4000)) + privateKyberKey?: string; + + @AllowNull(true) + @Column(DataType.STRING(2000)) + publicKyberKey?: string; + @Column password: string; diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 0873f2895..44afacc16 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -403,6 +403,10 @@ export class UserController { uuid: user.uuid, }, publicKey: user.publicKey, + keys: { + ecc: user.publicKey, + kyber: user.publicKyberKey, + }, }; } catch (err) { let errorMessage = err.message; diff --git a/src/modules/user/user.usecase.spec.ts b/src/modules/user/user.usecase.spec.ts index ad260ed05..59408e05e 100644 --- a/src/modules/user/user.usecase.spec.ts +++ b/src/modules/user/user.usecase.spec.ts @@ -47,7 +47,7 @@ import { } from '../../../test/fixtures'; import { MailTypes } from '../security/mail-limit/mailTypes'; import { SequelizeWorkspaceRepository } from '../workspaces/repositories/workspaces.repository'; -import * as openpgpUtils from '../../lib/openpgp'; +import * as openpgpUtils from '../../lib/assymetric-encryption/openpgp'; import { SequelizeMailLimitRepository } from '../security/mail-limit/mail-limit.repository'; import { DeviceType, diff --git a/src/modules/user/user.usecase.ts b/src/modules/user/user.usecase.ts index b460b3fc5..afc307984 100644 --- a/src/modules/user/user.usecase.ts +++ b/src/modules/user/user.usecase.ts @@ -54,7 +54,7 @@ import { decryptMessageWithPrivateKey, encryptMessageWithPublicKey, generateNewKeys, -} from '../../lib/openpgp'; +} from '../../lib/assymetric-encryption/openpgp'; import { aes } from '@internxt/lib'; import { PreCreatedUserAttributes } from './pre-created-users.attributes'; import { PreCreatedUser } from './pre-created-user.domain'; @@ -78,6 +78,7 @@ import { UpdateProfileDto } from './dto/update-profile.dto'; import { isUUID } from 'class-validator'; import { KeyServerUseCases } from '../keyserver/key-server.usecase'; import { UserKeysEncryptVersions } from '../keyserver/key-server.domain'; +import { Kyber512 } from '../../lib/assymetric-encryption/kyber'; export class ReferralsNotAvailableError extends Error { constructor() { @@ -559,6 +560,7 @@ export class UserUseCases { if (preCreatedUser) { return { ...preCreatedUser.toJSON(), + publicKyberKey: preCreatedUser.publicKey, publicKey: preCreatedUser.publicKey.toString(), password: preCreatedUser.password.toString(), }; @@ -584,6 +586,14 @@ export class UserUseCases { salt: this.configService.get('secrets.magicSalt'), }); + const Kem = new Kyber512(); + const kyberKeys = await Kem.generateKeysInBase64(); + + const encPrivateKyberKey = aes.encrypt(kyberKeys.privateKey, defaultPass, { + iv: this.configService.get('secrets.magicIv'), + salt: this.configService.get('secrets.magicSalt'), + }); + const user = await this.preCreatedUserRepository.create({ email, uuid: v4(), @@ -593,6 +603,8 @@ export class UserUseCases { mnemonic: encMnemonic, publicKey: publicKeyArmored, privateKey: encPrivateKey, + privateKyberKey: encPrivateKyberKey, + publicKyberKey: kyberKeys.publicKey, revocationKey: revocationCertificate, encryptVersion: UserKeysEncryptVersions.Ecc, }); @@ -600,6 +612,7 @@ export class UserUseCases { return { ...user.toJSON(), publicKey: user.publicKey.toString(), + publicKyberKey: user.publicKyberKey, password: user.password.toString(), }; }