Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"@noble/post-quantum": "^0.5.2",
"@scure/bip39": "^2.0.1",
"axios": "^1.11.0",
"buffer": "^6.0.3",
"flexsearch": "^0.8.205",
"hash-wasm": "^4.12.0",
"husky": "^9.1.7",
Expand Down
23 changes: 12 additions & 11 deletions src/email-crypto/emailKeys.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { generateEccKeys } from '../asymmetric-crypto';
import { generateKyberKeys } from '../post-quantum-crypto';
import { PublicKeys, PrivateKeys } from '../types';
import { EmailKeys } from '../types';

/**
* Generates public and private keys for email service.
*
* @returns The user's private and public keys
*/
export async function generateEmailKeys(): Promise<{ publicKeys: PublicKeys; privateKeys: PrivateKeys }> {
export async function generateEmailKeys(): Promise<EmailKeys> {
try {
const kyberKeys = generateKyberKeys();
const keys = await generateEccKeys();

const privateKeys: PrivateKeys = {
eccPrivateKey: keys.privateKey,
kyberPrivateKey: kyberKeys.secretKey,
const emailKeys: EmailKeys = {
publicKeys: {
eccPublicKey: keys.publicKey,
kyberPublicKey: kyberKeys.publicKey,
},
privateKeys: {
eccPrivateKey: keys.privateKey,
kyberPrivateKey: kyberKeys.secretKey,
},
};

const publicKeys: PublicKeys = {
eccPublicKey: keys.publicKey,
kyberPublicKey: kyberKeys.publicKey,
};

return { publicKeys, privateKeys };
return emailKeys;
} catch (error) {
throw new Error('Failed to generate keys for email service', { cause: error });
}
Expand Down
65 changes: 48 additions & 17 deletions src/keystore-crypto/core.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { encryptSymmetrically, decryptSymmetrically } from '../symmetric-crypto';
import { base64ToUint8Array, uint8ArrayToBase64, UTF8ToUint8, mnemonicToBytes } from '../utils';
import { base64ToUint8Array, uint8ArrayToBase64, UTF8ToUint8, mnemonicToBytes, publicKeyToBase64 } from '../utils';
import { deriveSymmetricCryptoKeyFromContext } from '../derive-key';
import { CONTEXT_ENC_KEYSTORE, AES_KEY_BIT_LENGTH, CONTEXT_RECOVERY } from '../constants';
import { getBytesFromData } from '../hash';
import { EmailKeys, EncryptedKeystore, KeystoreType } from 'types';
import { exportPrivateKey, importPrivateKey, importPublicKey } from '../asymmetric-crypto';

/**
* Encrypts the keystore content using symmetric encryption
Expand All @@ -15,15 +17,30 @@ import { getBytesFromData } from '../hash';
*/
export async function encryptKeystoreContent(
secretKey: CryptoKey,
content: string,
userID: string,
tag: string,
): Promise<Uint8Array> {
keys: EmailKeys,
userEmail: string,
type: KeystoreType,
): Promise<EncryptedKeystore> {
try {
const aux = UTF8ToUint8(userID + tag);
const message = base64ToUint8Array(content);
const result = await encryptSymmetrically(secretKey, message, aux);
return result;
const aux = UTF8ToUint8(userEmail + type);
const publicKeys = await publicKeyToBase64(keys.publicKeys);
const kyberPrivateKeyEnc = await encryptSymmetrically(secretKey, keys.privateKeys.kyberPrivateKey, aux);
const eccPrivateKey = await exportPrivateKey(keys.privateKeys.eccPrivateKey);
const eccPrivateKeyEnc = await encryptSymmetrically(secretKey, eccPrivateKey, aux);
const encryptedKeys = {
publicKeys,
privateKeys: {
kyberPrivateKeyBase64: uint8ArrayToBase64(kyberPrivateKeyEnc),
eccPrivateKeyBase64: uint8ArrayToBase64(eccPrivateKeyEnc),
},
};

const keystore: EncryptedKeystore = {
userEmail,
type,
encryptedKeys,
};
return keystore;
} catch (error) {
throw new Error('Failed to encrypt keystore content', { cause: error });
}
Expand All @@ -40,15 +57,29 @@ export async function encryptKeystoreContent(
*/
export async function decryptKeystoreContent(
secretKey: CryptoKey,
encryptedKeys: Uint8Array,
userEmail: string,
tag: string,
): Promise<string> {
encryptedKeystore: EncryptedKeystore,
): Promise<EmailKeys> {
try {
const aux = UTF8ToUint8(userEmail + tag);
const content = await decryptSymmetrically(secretKey, encryptedKeys, aux);
const result = uint8ArrayToBase64(content);
return result;
const aux = UTF8ToUint8(encryptedKeystore.userEmail + encryptedKeystore.type);
const kyberPublicKey = base64ToUint8Array(encryptedKeystore.encryptedKeys.publicKeys.kyberPublicKeyBase64);
const eccPublicArray = base64ToUint8Array(encryptedKeystore.encryptedKeys.publicKeys.eccPublicKeyBase64);
const eccPublicKey = await importPublicKey(eccPublicArray);
const encKyberPrivateKey = base64ToUint8Array(encryptedKeystore.encryptedKeys.privateKeys.kyberPrivateKeyBase64);
const kyberPrivateKey = await decryptSymmetrically(secretKey, encKyberPrivateKey, aux);
const eccEncArray = base64ToUint8Array(encryptedKeystore.encryptedKeys.privateKeys.eccPrivateKeyBase64);
const eccKey = await decryptSymmetrically(secretKey, eccEncArray, aux);
const eccPrivateKey = await importPrivateKey(eccKey);
const keys = {
publicKeys: {
kyberPublicKey,
eccPublicKey,
},
privateKeys: {
kyberPrivateKey,
eccPrivateKey,
},
};
return keys;
} catch (error) {
throw new Error('Failed to decrypt keystore content', { cause: error });
}
Expand Down
35 changes: 7 additions & 28 deletions src/keystore-crypto/emailEncryptionKey.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EmailKeys, EncryptedKeystore, KeystoreType } from '../types';
import { emailKeysToBase64, base64ToEmailKeys, genMnemonic } from '../utils';
import { genMnemonic } from '../utils';
import { encryptKeystoreContent, decryptKeystoreContent, deriveEncryptionKeystoreKey, deriveRecoveryKey } from './core';
import { generateEmailKeys } from '../email-crypto';

Expand All @@ -20,23 +20,14 @@ export async function createEncryptionAndRecoveryKeystores(
}> {
try {
const keys = await generateEmailKeys();
const content = await emailKeysToBase64(keys);

const secretKey = await deriveEncryptionKeystoreKey(baseKey);
const ciphertext = await encryptKeystoreContent(secretKey, content, userEmail, KeystoreType.ENCRYPTION);
const encryptionKeystore: EncryptedKeystore = {
userEmail,
type: KeystoreType.ENCRYPTION,
encryptedKeys: ciphertext,
};
const encryptionKeystore = await encryptKeystoreContent(secretKey, keys, userEmail, KeystoreType.ENCRYPTION);

const recoveryCodes = genMnemonic();
const recoveryKey = await deriveRecoveryKey(recoveryCodes);
const encKeys = await encryptKeystoreContent(recoveryKey, content, userEmail, KeystoreType.RECOVERY);
const recoveryKeystore: EncryptedKeystore = {
userEmail,
type: KeystoreType.RECOVERY,
encryptedKeys: encKeys,
};
const recoveryKeystore = await encryptKeystoreContent(recoveryKey, keys, userEmail, KeystoreType.RECOVERY);

return { encryptionKeystore, recoveryKeystore, recoveryCodes };
} catch (error) {
throw new Error('Failed to create encryption and recovery keystores', { cause: error });
Expand All @@ -60,13 +51,7 @@ export async function openEncryptionKeystore(
throw new Error('Input is invalid');
}
const secretKey = await deriveEncryptionKeystoreKey(baseKey);
const json = await decryptKeystoreContent(
secretKey,
encryptedKeystore.encryptedKeys,
encryptedKeystore.userEmail,
KeystoreType.ENCRYPTION,
);
const keys: EmailKeys = await base64ToEmailKeys(json);
const keys = await decryptKeystoreContent(secretKey, encryptedKeystore);
return keys;
} catch (error) {
throw new Error('Failed to open encryption keystore', { cause: error });
Expand All @@ -90,13 +75,7 @@ export async function openRecoveryKeystore(
throw new Error('Input is invalid');
}
const recoveryKey = await deriveRecoveryKey(recoveryCodes);
const json = await decryptKeystoreContent(
recoveryKey,
encryptedKeystore.encryptedKeys,
encryptedKeystore.userEmail,
KeystoreType.RECOVERY,
);
const keys: EmailKeys = await base64ToEmailKeys(json);
const keys = await decryptKeystoreContent(recoveryKey, encryptedKeystore);
return keys;
} catch (error) {
throw new Error('Failed to open recovery keystore', { cause: error });
Expand Down
75 changes: 13 additions & 62 deletions src/keystore-service/api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import axios, { AxiosResponse, AxiosError } from 'axios';
import { KeystoreType, EncryptedKeystore, PublicKeys } from '../types';
import { base64ToEncryptedKeystore, encryptedKeystoreToBase64, base64ToPublicKey } from '../utils';
import { KeystoreType, EncryptedKeystore, PublicKeys, PublicKeysBase64 } from '../types';
import { base64ToPublicKey } from '../utils';

export class KeyServiceAPI {
private readonly baseUrl: string;
Expand Down Expand Up @@ -35,10 +35,11 @@ export class KeyServiceAPI {
* @param url - The user specific part of the url
* @returns Server response
*/
async sendEncryptedKeystoreToServer(encryptedKeystore: string, url: string): Promise<AxiosResponse> {
async uploadKeystoreToServer(encryptedKeystore: EncryptedKeystore): Promise<AxiosResponse> {
try {
const url = `${this.baseUrl}/uploadKeystore`;
const response = await axios.post(
this.baseUrl + `${url}`,
url,
{ encryptedKeystore },
{
withCredentials: true,
Expand All @@ -61,10 +62,11 @@ export class KeyServiceAPI {
* @param url - The user-specific part of the url
* @returns The user's encrypted keystore as base64 string
*/
async requestEncryptedKeystore(userID: string, keystoreType: KeystoreType): Promise<string> {
async getKeystoreFromServer(userID: string, keystoreType: KeystoreType): Promise<EncryptedKeystore> {
try {
const url = `/downloadKeystore/${userID}/${keystoreType}`;
const response = await axios.get<string>(this.baseUrl + `${url}`, {
const url = `${this.baseUrl}/downloadKeystore`;
const response = await axios.get<EncryptedKeystore>(url, {
params: { userID, keystoreType },
withCredentials: true,
headers: {
'Content-Type': 'application/json',
Expand All @@ -80,67 +82,16 @@ export class KeyServiceAPI {
}
}

/**
* Uploads encrypted keystore to the server
*
* @param encryptedKeystore - The encrypted keystore
* @returns Server response
*/
async uploadKeystoreToServer(encryptedKeystore: EncryptedKeystore): Promise<AxiosResponse> {
try {
const userID = encryptedKeystore.userEmail;
const keystoreType = encryptedKeystore.type;
const url = `/uploadKeystore/${userID}/${keystoreType}`;
const ciphertextBase64 = encryptedKeystoreToBase64(encryptedKeystore);
const result = await this.sendEncryptedKeystoreToServer(ciphertextBase64, url);
return result;
} catch (error) {
throw new Error('Failed to upload keystore to the server', { cause: error });
}
}

/**
* Gets a user's encrypted keystore containing encryption keys from the server
*
* @returns Encrypted keystore containing encryption keys
*/
async getEncryptionKeystoreFromServer(userEmail: string): Promise<EncryptedKeystore> {
return this.getKeystoreFromServer(userEmail, KeystoreType.ENCRYPTION);
}

/**
* Gets a user's encrypted Recovery Keystore from the server
*
* @returns Encrypted Recovery Keystore
*/
async getRecoveryKeystoreFromServer(userEmail: string): Promise<EncryptedKeystore> {
return this.getKeystoreFromServer(userEmail, KeystoreType.RECOVERY);
}

/**
* Gets a user's encrypted keystore from the server
*
* @param type - The requested keystore's type
* @returns Encrypted Keytore
*/
async getKeystoreFromServer(userEmail: string, type: KeystoreType): Promise<EncryptedKeystore> {
try {
const response = await this.requestEncryptedKeystore(userEmail, type);
const result = base64ToEncryptedKeystore(response);
return result;
} catch (error) {
throw new Error('Failed to retrieve keystore from the server', { cause: error });
}
}
/**
* Obtains recipients public keys from the server
*
* @param emails - The recipients' emails
* @returns The list of recipients' public keys
*/
async getRecipientsPublicKeysFromServer(emails: string[]): Promise<string[]> {
async getRecipientsPublicKeysFromServer(emails: string[]): Promise<PublicKeysBase64[]> {
try {
const response = await axios.get<string[]>('/api/getPublicKeys', {
const url = `${this.baseUrl}/getPublicKeys`;
const response = await axios.get<PublicKeysBase64[]>(url, {
params: {
emails: emails,
},
Expand All @@ -166,7 +117,7 @@ export class KeyServiceAPI {
*/
async getRecipientsPublicKeys(emails: string[]): Promise<PublicKeys[]> {
try {
const publicKeysBase64: string[] = await this.getRecipientsPublicKeysFromServer(emails);
const publicKeysBase64: PublicKeysBase64[] = await this.getRecipientsPublicKeysFromServer(emails);
const result: PublicKeys[] = [];
for (const keyBase64 of publicKeysBase64) {
const publicKeys = await base64ToPublicKey(keyBase64);
Expand Down
21 changes: 18 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export type EncryptedKeystore = {
userEmail: string;
type: KeystoreType;
encryptedKeys: Uint8Array;
encryptedKeys: EmailKeysEncrypted;
};

export type User = {
Expand All @@ -18,16 +18,31 @@ export type PublicKeys = {
kyberPublicKey: Uint8Array;
};

export type PublicKeysBase64 = {
eccPublicKeyBase64: string;
kyberPublicKeyBase64: string;
};

export type PrivateKeys = {
eccPrivateKey: CryptoKey;
kyberPrivateKey: Uint8Array;
};

export type PrivateKeysEncrypted = {
eccPrivateKeyBase64: string;
kyberPrivateKeyBase64: string;
};

export type EmailKeys = {
publicKeys: PublicKeys;
privateKeys: PrivateKeys;
};

export type EmailKeysEncrypted = {
publicKeys: PublicKeysBase64;
privateKeys: PrivateKeysEncrypted;
};

export type HybridEncryptedEmail = {
encryptedKey: HybridEncKey;
enc: Uint8Array;
Expand Down Expand Up @@ -92,6 +107,6 @@ export interface EmailSearchResult {
}

export enum KeystoreType {
ENCRYPTION = 'Encryption keystore',
RECOVERY = 'Recovery keystore',
ENCRYPTION = 'Encryption',
RECOVERY = 'Recovery',
}
Loading