Skip to content

Commit

Permalink
feat initial commit kyber pre creted users
Browse files Browse the repository at this point in the history
  • Loading branch information
apsantiso committed Jan 31, 2025
1 parent 90b5d0d commit bd12b20
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 2 deletions.
118 changes: 118 additions & 0 deletions src/lib/assymetric-encryption/hybrib-encryption.ts
Original file line number Diff line number Diff line change
@@ -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<string>} 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<string> => {
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<string>} 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<string> => {
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;
};
59 changes: 59 additions & 0 deletions src/lib/assymetric-encryption/kyber.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<Awaited<ReturnType<typeof getKemBuilder>>>>;

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<Uint8Array> {
if (!this.kem) await this.init();
const { sharedSecret } = await this.kem.decapsulate(ciphertext, privateKey);
return sharedSecret;
}
}
File renamed without changes.
34 changes: 34 additions & 0 deletions src/lib/assymetric-encryption/utils.ts
Original file line number Diff line number Diff line change
@@ -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<string>} The extended secret of the desired bitlength
*/
export function extendSecret(
secret: Uint8Array,
length: number,
): Promise<string> {
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;
}
7 changes: 7 additions & 0 deletions src/modules/user/pre-created-user.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export class PreCreatedUser implements PreCreatedUserAttributes {
privateKey: string;
revocationKey: string;
encryptVersion: UserKeysEncryptVersions;
publicKyberKey?: string;
privateKyberKey?: string;

constructor({
id,
email,
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/modules/user/pre-created-users.attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
}
8 changes: 8 additions & 0 deletions src/modules/user/pre-created-users.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
4 changes: 4 additions & 0 deletions src/modules/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/user/user.usecase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 14 additions & 1 deletion src/modules/user/user.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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() {
Expand Down Expand Up @@ -559,6 +560,7 @@ export class UserUseCases {
if (preCreatedUser) {
return {
...preCreatedUser.toJSON(),
publicKyberKey: preCreatedUser.publicKey,
publicKey: preCreatedUser.publicKey.toString(),
password: preCreatedUser.password.toString(),
};
Expand All @@ -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(),
Expand All @@ -593,13 +603,16 @@ export class UserUseCases {
mnemonic: encMnemonic,
publicKey: publicKeyArmored,
privateKey: encPrivateKey,
privateKyberKey: encPrivateKyberKey,
publicKyberKey: kyberKeys.publicKey,
revocationKey: revocationCertificate,
encryptVersion: UserKeysEncryptVersions.Ecc,
});

return {
...user.toJSON(),
publicKey: user.publicKey.toString(),
publicKyberKey: user.publicKyberKey,
password: user.password.toString(),
};
}
Expand Down

0 comments on commit bd12b20

Please sign in to comment.