import { RSAKeyEncryptor, RSAKeyEncryptorI } from "encryption/low-level/asymmetric/rsa/key"; import { AESEncryptor } from "encryption/low-level/symmetric/aes/data"; import { HalfCryptoKeyPair } from "encryption/types"; import { JSONValue, JSONObject } from "encoding/json/types"; import { SymmetricDataEncryptor, EncryptedData as InternalEncryptedData } from "./symmetric-data-encryptor"; import { KeyEncoder } from "encoding/key"; import { b64Encode, b64Decode } from "encoding/base64"; import { DecryptionError } from "error"; export interface EncryptedKey { encrypted_key: string; } export interface EncryptedData { encrypted_key: EncryptedKey; encrypted_data: InternalEncryptedData; } interface EncryptInternalDataReturn { key: CryptoKey; encryptedData: InternalEncryptedData; } export interface AsymmetricDataEncryptorI { encrypt(jsonableInput: JSONValue): Promise; decrypt( encryptedObject: EncryptedData, encryptedKeyOverride?: string ): Promise; update( newData: JSONValue, existingEncryptedObject: EncryptedData, encryptedKeyOverride?: string ): Promise; share( encryptedObject: EncryptedData, publicKeyObj: JsonWebKey, encryptedKeyOverride?: string ): Promise; exportPublicKey(): Promise; encryptPrivateKey( publicKeyObjToShareWith: JsonWebKey ): Promise; } export function AsymmetricDataEncryptor( encryptionKeyPair: CryptoKeyPair | HalfCryptoKeyPair ): AsymmetricDataEncryptorI { const { publicKey, privateKey } = encryptionKeyPair as CryptoKeyPair; const keyEncryptor = RSAKeyEncryptor(encryptionKeyPair); function encryptInternalKey( internalDEK: CryptoKey, keyEncryptor: RSAKeyEncryptorI ): Promise { return keyEncryptor.encryptKey(internalDEK).then(b64Encode); } async function decryptInternalKey( encryptedKey: string, keyEncryptor: RSAKeyEncryptorI ): Promise { try { return await keyEncryptor.decryptKey( b64Decode(encryptedKey), AESEncryptor.getDefaultAlgorithm() ); } catch (error) { throw new DecryptionError( "Failed to decrypt WDEK (Wrapped Data Encryption Key).", "The WDEK probably belongs to another user or application." ); } } async function encryptInternalData( jsonableInput: JSONValue ): Promise { // Generate an AES key, encrypt the data with it, then encrypt the AES key with the // provided publicKey. const key = await AESEncryptor.generateKey(); const symmetricEncryptor = SymmetricDataEncryptor(key); const encryptedData = await symmetricEncryptor.encrypt(jsonableInput); return { key, encryptedData }; } async function decryptInternalData( encryptedData: InternalEncryptedData, key: CryptoKey ): Promise { const symmetricEncryptor = SymmetricDataEncryptor(key); return await symmetricEncryptor.decrypt(encryptedData); } return { async encrypt(jsonableInput: JSONValue): Promise { const { key, encryptedData } = await encryptInternalData( jsonableInput ); const encryptedKey = await encryptInternalKey(key, keyEncryptor); return { // This structure allows us to put other information with the key, // should we choose to. // eslint-disable-next-line @typescript-eslint/camelcase encrypted_key: { // eslint-disable-next-line @typescript-eslint/camelcase encrypted_key: encryptedKey }, // eslint-disable-next-line @typescript-eslint/camelcase encrypted_data: encryptedData }; }, async decrypt( encryptedObject: EncryptedData, encryptedKeyOverride?: undefined | string ): Promise { const { encrypted_key: encryptedKey, encrypted_data: encryptedData } = encryptedObject; const key = await decryptInternalKey( encryptedKeyOverride || encryptedKey.encrypted_key, keyEncryptor ); try { return await decryptInternalData(encryptedData, key); } catch (e) { throw new DecryptionError( "The WD (Wrapped Data) supplied cannot be decrypted by the WDEK (Wrapped Data Key Encryption Key) supplied.", "The key is probably for different WD." ); } }, async update( newData: JSONValue, existingEncryptedObject: EncryptedData, encryptedKeyOverride?: string ): Promise { const { encrypted_key: encryptedKey } = existingEncryptedObject; const key = await decryptInternalKey( encryptedKeyOverride || encryptedKey.encrypted_key, keyEncryptor ); const symmetricEncryptor = SymmetricDataEncryptor(key); const encryptedData = await symmetricEncryptor.encrypt(newData); return { // eslint-disable-next-line @typescript-eslint/camelcase encrypted_key: encryptedKey, // eslint-disable-next-line @typescript-eslint/camelcase encrypted_data: encryptedData }; }, async share( encryptedObject: EncryptedData, publicKeyObj: JsonWebKey, encryptedKeyOverride?: string ): Promise { const { encrypted_key: encryptedKey } = encryptedObject; const publicKey = await AsymmetricDataEncryptor.decodePublicKey( publicKeyObj ); const decryptedKey = await decryptInternalKey( encryptedKeyOverride || encryptedKey.encrypted_key, keyEncryptor ); return await encryptInternalKey( decryptedKey, RSAKeyEncryptor({ publicKey }) ); }, async exportPublicKey(): Promise { if (!publicKey) { throw Error( "Cannot export publicKey as it has not been supplied." ); } return await KeyEncoder().encode(publicKey); }, async encryptPrivateKey( publicKeyObjToShareWith: JsonWebKey ): Promise { if (!privateKey) { throw Error( "Cannot encrypt privateKey as it has not been supplied." ); } // Wrap the key manually, because it's far simpler than rebuilding // everything for key encryption (and the private key is too large // to just be encrypted by the public key) const encodedKey = await KeyEncoder().encode(privateKey); // Just encrypt the key for the other user like we would with any other data const publicKey = await AsymmetricDataEncryptor.decodePublicKey( publicKeyObjToShareWith ); const internalAsymmetricDataEncryptor = AsymmetricDataEncryptor({ publicKey }); return await internalAsymmetricDataEncryptor.encrypt( encodedKey as JSONObject ); } }; } AsymmetricDataEncryptor.decodePublicKey = async function( publicKeyObj: JsonWebKey ): Promise { return await KeyEncoder().decode( publicKeyObj, AsymmetricDataEncryptor.getDefaultAlgorithm(), ["wrapKey"], true ); }; AsymmetricDataEncryptor.getDefaultAlgorithm = RSAKeyEncryptor.getDefaultAlgorithm; AsymmetricDataEncryptor.generateKeyPair = RSAKeyEncryptor.generateKeyPair;