Skip to content
Merged
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "internxt-crypto",
"version": "1.6.0",
"version": "1.6.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"module": "dist/index.js",
Expand Down Expand Up @@ -41,18 +41,21 @@
"@noble/hashes": "^2.2.0",
"@noble/post-quantum": "^0.6.1",
"@scure/bip39": "^2.2.0",
"base64-js": "^1.5.1",
"hash-wasm": "^4.12.0",
"husky": "^9.1.7",
"uuid": "^14.0.0"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"react-native": "./dist/react-native/index.mjs",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./*": {
"types": "./dist/*.d.ts",
"react-native": "./dist/react-native/*.mjs",
"import": "./dist/*.mjs",
"require": "./dist/*.js"
}
Expand Down
25 changes: 25 additions & 0 deletions src/asymmetric-crypto/ellipticCurve.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { x25519 } from '@noble/curves/ed25519.js';

/**
* Derives secret key from the other user's public key and own private key
*
* @param aliceSecretKey - The secret key of the user deriving the shared secret key
* @param bobPublicKey - The public key of the other user
* @returns The derived secret key bits
*/
export async function deriveSecretKey(aliceSecretKey: Uint8Array, bobPublicKey: Uint8Array): Promise<Uint8Array> {
try {
return x25519.getSharedSecret(aliceSecretKey, bobPublicKey);
} catch (error) {
throw new Error('Failed to derive elliptic curve secret key', { cause: error });
}
}

/**
* Generates elliptic curve key pair
*
* @returns The generated key pair
*/
export async function generateEccKeys(): Promise<{ secretKey: Uint8Array; publicKey: Uint8Array }> {
return x25519.keygen();
}
15 changes: 13 additions & 2 deletions src/asymmetric-crypto/ellipticCurve.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { x25519 } from '@noble/curves/webcrypto.js';

function asBytes(result: unknown): Uint8Array {
if (result instanceof Uint8Array) return result;
throw new Error('Expected raw key bytes, got JWK');
}

/**
* Derives secret key from the other user's public key and own private key
*
Expand All @@ -9,7 +14,9 @@ import { x25519 } from '@noble/curves/webcrypto.js';
*/
export async function deriveSecretKey(aliceSecretKey: Uint8Array, bobPublicKey: Uint8Array): Promise<Uint8Array> {
try {
return await x25519.getSharedSecret(aliceSecretKey, bobPublicKey);
const spkiPublic = asBytes(await x25519.utils.convertPublicKey(bobPublicKey, 'raw', 'spki'));
const pkcs8Secret = asBytes(await x25519.utils.convertSecretKey(aliceSecretKey, 'raw', 'pkcs8'));
return await x25519.getSharedSecret(pkcs8Secret, spkiPublic);
} catch (error) {
throw new Error('Failed to derive elliptic curve secret key', { cause: error });
}
Expand All @@ -21,5 +28,9 @@ export async function deriveSecretKey(aliceSecretKey: Uint8Array, bobPublicKey:
* @returns The generated key pair
*/
export async function generateEccKeys(): Promise<{ secretKey: Uint8Array; publicKey: Uint8Array }> {
return x25519.keygen();
const { secretKey, publicKey } = await x25519.keygen();
return {
secretKey: asBytes(await x25519.utils.convertSecretKey(secretKey, 'pkcs8', 'raw')),
publicKey: asBytes(await x25519.utils.convertPublicKey(publicKey, 'spki', 'raw')),
};
}
34 changes: 34 additions & 0 deletions src/derive-password/core.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { argon2id } from '@noble/hashes/argon2.js';
import {
ARGON2ID_ITERATIONS,
ARGON2ID_MEMORY_SIZE,
ARGON2ID_PARALLELISM,
ARGON2ID_OUTPUT_BYTE_LENGTH,
} from '../constants';

/**
* Calculates hash using the argon2id password-hashing function
*
* @param password - The user's password
* @param salt - The given salt
* @param parallelism - The degree of parallelism
* @param iterations - The number of iterations
* @param memorySize - The memory size in KB
* @param hashLength - The desired hash byte length
* @returns The resulting hash
*/
export async function argon2(
password: string,
salt: Uint8Array,
parallelism: number = ARGON2ID_PARALLELISM,
iterations: number = ARGON2ID_ITERATIONS,
memorySize: number = ARGON2ID_MEMORY_SIZE,
hashLength: number = ARGON2ID_OUTPUT_BYTE_LENGTH,
): Promise<Uint8Array> {
return argon2id(password, salt, {
p: parallelism,
t: iterations,
m: memorySize,
dkLen: hashLength,
});
}
25 changes: 25 additions & 0 deletions src/email-crypto/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
EmailSymmetricEncryptionError,
EmailPasswordOpenError,
EmailPasswordProtectError,
EmailPreviewSymmetricDecryptionError,
} from './errors';

/**
Expand Down Expand Up @@ -73,6 +74,30 @@ export async function encryptEmailWithKey(
}
}

/**
* Decrypts symmetrically encrypted email preview.
*
* @param encEmailPreview - The email preview to decrypt.
* @param encryptionKey - The symmetric key to decrypt the email.
* @param aux - An optional auxilary sting for AEAD (e.g., email ID or timestamp).
* @returns The resulting decrypted email
*/
export async function decryptPreview(
encEmailPreview: string,
encryptionKey: Uint8Array,
aux?: Uint8Array,
): Promise<string> {
try {
const encPreview = base64ToUint8Array(encEmailPreview);
const previewArray = await decryptSymmetrically(encryptionKey, encPreview, aux);
const preview = uint8ToUTF8(previewArray);

return preview;
} catch (error) {
throw new EmailPreviewSymmetricDecryptionError(error instanceof Error ? error.message : String(error));
}
}

/**
* Decrypts symmetrically encrypted email.
*
Expand Down
18 changes: 18 additions & 0 deletions src/email-crypto/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export class FailedToEncryptEmail extends Error {
constructor(errorMsg?: string) {
super('Failed to encrypt email: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, FailedToEncryptEmail.prototype);
}
}
Expand All @@ -10,6 +11,7 @@ export class EmailSymmetricEncryptionError extends Error {
constructor(errorMsg?: string) {
super('Failed to symmetrically encrypt email: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, EmailSymmetricEncryptionError.prototype);
}
}
Expand All @@ -18,14 +20,25 @@ export class EmailSymmetricDecryptionError extends Error {
constructor(errorMsg?: string) {
super('Failed to symmetrically decrypt email: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, EmailSymmetricDecryptionError.prototype);
}
}

export class EmailPreviewSymmetricDecryptionError extends Error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, add the name of the error here:
this.name = 'EmailPreviewSymmetrictDecryptionError' so we can use it in the client if we need to filter errors. Better using the name rather than status codes, messages or whatever.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

constructor(errorMsg?: string) {
super('Failed to symmetrically decrypt email preview: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, EmailPreviewSymmetricDecryptionError.prototype);
}
}

export class EmailHybridEncryptionError extends Error {
constructor(errorMsg?: string) {
super('Failed to hybridly encrypt the key: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, EmailHybridEncryptionError.prototype);
}
}
Expand All @@ -34,6 +47,7 @@ export class EmailHybridDecryptionError extends Error {
constructor(errorMsg?: string) {
super('Failed to hybridly decrypt the key: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, EmailHybridDecryptionError.prototype);
}
}
Expand All @@ -42,6 +56,7 @@ export class EmailPasswordProtectError extends Error {
constructor(errorMsg?: string) {
super('Failed to password-protect the key: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, EmailPasswordProtectError.prototype);
}
}
Expand All @@ -50,6 +65,7 @@ export class EmailPasswordOpenError extends Error {
constructor(errorMsg?: string) {
super('Failed to open password-protected key: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, EmailPasswordOpenError.prototype);
}
}
Expand All @@ -58,6 +74,7 @@ export class InvalidInputEmail extends Error {
constructor() {
super('Invalid input');

this.name = this.constructor.name;
Object.setPrototypeOf(this, InvalidInputEmail.prototype);
}
}
Expand All @@ -66,6 +83,7 @@ export class FailedToDecryptEmail extends Error {
constructor(errorMsg?: string) {
super('Failed to decrypt email: ' + errorMsg);

this.name = this.constructor.name;
Object.setPrototypeOf(this, FailedToDecryptEmail.prototype);
}
}
20 changes: 0 additions & 20 deletions src/email-crypto/hybridEncryptedEmailAndSubject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@ import { RecipientWithPublicKey, EmailAndSubject, EmailAndSubjectEncrypted, Hybr
import { encryptKeysHybrid, decryptKeysHybrid } from './core';
import { encryptEmailAndSubject, decryptEmailAndSubject } from './coreSubject';
import {
FailedToDecryptEmail,
FailedToEncryptEmail,
EmailHybridDecryptionError,
EmailHybridEncryptionError,
InvalidInputEmail,
EmailSymmetricDecryptionError,
EmailSymmetricEncryptionError,
} from './errors';

/**
Expand All @@ -24,7 +18,6 @@ export async function encryptEmailAndSubjectHybridForMultipleRecipients(
recipients: RecipientWithPublicKey[],
aux?: Uint8Array,
): Promise<{ encryptedKeys: HybridEncKey[]; encEmail: EmailAndSubjectEncrypted }> {
try {
if (!recipients || recipients.length === 0) {
throw new InvalidInputEmail();
}
Expand All @@ -33,12 +26,6 @@ export async function encryptEmailAndSubjectHybridForMultipleRecipients(
const encryptedKeys = await Promise.all(recipients.map((recipient) => encryptKeysHybrid(encryptionKey, recipient)));

return { encEmail, encryptedKeys };
} catch (error) {
if (error instanceof InvalidInputEmail) throw error;
if (error instanceof EmailSymmetricEncryptionError) throw error;
if (error instanceof EmailHybridEncryptionError) throw error;
throw new FailedToEncryptEmail(error instanceof Error ? error.message : String(error));
}
}

/**
Expand All @@ -56,13 +43,6 @@ export async function decryptEmailAndSubjectHybrid(
recipientPrivateHybridKeys: Uint8Array,
aux?: Uint8Array,
): Promise<EmailAndSubject> {
try {
const encryptionKey = await decryptKeysHybrid(encryptedKey, recipientPrivateHybridKeys);
return await decryptEmailAndSubject(encEmail, encryptionKey, aux);
} catch (error) {
if (error instanceof InvalidInputEmail) throw error;
if (error instanceof EmailHybridDecryptionError) throw error;
if (error instanceof EmailSymmetricDecryptionError) throw error;
throw new FailedToDecryptEmail(error instanceof Error ? error.message : String(error));
}
}
52 changes: 21 additions & 31 deletions src/email-crypto/hybridEncyptedEmail.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { Email, RecipientWithPublicKey, HybridEncKey, EmailEncrypted } from '../types';
import { decryptEmail, encryptKeysHybrid, decryptKeysHybrid, encryptEmail } from './core';
import {
FailedToDecryptEmail,
FailedToEncryptEmail,
EmailHybridDecryptionError,
EmailHybridEncryptionError,
InvalidInputEmail,
EmailSymmetricDecryptionError,
EmailSymmetricEncryptionError,
} from './errors';
import { decryptEmail, decryptPreview, encryptKeysHybrid, decryptKeysHybrid, encryptEmail } from './core';
import { InvalidInputEmail } from './errors';

/**
* Encrypts the email using hybrid encryption for multiple recipients.
Expand All @@ -23,21 +15,14 @@ export async function encryptEmailHybridForMultipleRecipients(
recipients: RecipientWithPublicKey[],
aux?: Uint8Array,
): Promise<{ encryptedKeys: HybridEncKey[]; encEmail: EmailEncrypted }> {
try {
if (!recipients || recipients.length === 0) {
throw new InvalidInputEmail();
}
const { encryptionKey, encEmail } = await encryptEmail(email, aux);
if (!recipients || recipients.length === 0) {
throw new InvalidInputEmail();
}
const { encryptionKey, encEmail } = await encryptEmail(email, aux);

const encryptedKeys = await Promise.all(recipients.map((recipient) => encryptKeysHybrid(encryptionKey, recipient)));
const encryptedKeys = await Promise.all(recipients.map((recipient) => encryptKeysHybrid(encryptionKey, recipient)));

return { encryptedKeys, encEmail };
} catch (error) {
if (error instanceof InvalidInputEmail) throw error;
if (error instanceof EmailSymmetricEncryptionError) throw error;
if (error instanceof EmailHybridEncryptionError) throw error;
throw new FailedToEncryptEmail(error instanceof Error ? error.message : String(error));
}
return { encryptedKeys, encEmail };
}

/**
Expand All @@ -55,12 +40,17 @@ export async function decryptEmailHybrid(
recipientPrivateHybridKeys: Uint8Array,
aux?: Uint8Array,
): Promise<Email> {
try {
const encryptionKey = await decryptKeysHybrid(encryptedKey, recipientPrivateHybridKeys);
return await decryptEmail(encEmail, encryptionKey, aux);
} catch (error) {
if (error instanceof EmailHybridDecryptionError) throw error;
if (error instanceof EmailSymmetricDecryptionError) throw error;
throw new FailedToDecryptEmail(error instanceof Error ? error.message : String(error));
}
const encryptionKey = await decryptKeysHybrid(encryptedKey, recipientPrivateHybridKeys);
return await decryptEmail(encEmail, encryptionKey, aux);
}

export async function decryptEmailPreviewHybrid(
encPreview: string,
encryptedKey: HybridEncKey,
recipientPrivateHybridKeys: Uint8Array,
aux?: Uint8Array,
): Promise<{ preview: string; encryptionKey: Uint8Array }> {
const encryptionKey = await decryptKeysHybrid(encryptedKey, recipientPrivateHybridKeys);
const preview = await decryptPreview(encPreview, encryptionKey, aux);
return { preview, encryptionKey };
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { getKeyFromPassword, getKeyFromPasswordAndSalt, generateSalt } from './d
export {
encryptEmailHybridForMultipleRecipients,
decryptEmailHybrid,
decryptEmailPreviewHybrid,
encryptEmailAndSubjectHybridForMultipleRecipients,
decryptEmailAndSubjectHybrid,
createPwdProtectedEmail,
Expand Down
Loading
Loading