/** * All the functions involved in building and saving [[ApplicationEncryptors]] for a user. */ import { KeyDerivationConfig, KeyDerivationConfigParams } from "encryption/low-level/key-derivation"; import { KeyEncryptionConfig, KeyEncryptionConfigParams } from "encryption/low-level/symmetric/aes/key"; import { DataEncryptor, DataEncryptorI } from "encryption/data/data-encryptor"; import { encryptKeyPair, decryptKeyPair, EncryptedKeyPair, exportKeyPair, importKeyPair, ExportedKeyPair } from "./util/encrypt-key-pair"; export interface UserDoc { kek_derivation_config: KeyDerivationConfigParams; private_key_encryption_config: KeyEncryptionConfigParams; encryption_key_pair: EncryptedKeyPair; signing_key_pair: EncryptedKeyPair; } export interface ResolvedNewUserResponse { encryptor: DataEncryptorI; userDoc: UserDoc; } export interface ImportedKeys { encryptionKeyPair: CryptoKeyPair; signingKeyPair: CryptoKeyPair; } export interface ExportedKeys { encryption_key_pair: ExportedKeyPair; signing_key_pair: ExportedKeyPair; } /** * Build a [[DataEncryptor]] and the user data that should be saved against a user. * This should be called whenever a new user is created. * It will give you a [[DataEncryptor]] that can be used for encrypting, decrypting, * signing, verifying data, and the ability to create and load ApplicationEncryptors. * * @param password The password that will be required to build the [[DataEncryptor]] * from the user data in the future. * * @return [[ResolvedNewUserResponse]] containing [[UserDoc]] - the data that should be saved * against the user to recreate the [[DataEncryptor]] with [[encryptorFromExistingUser]], * and encryptor - the [[DataEncryptor]]. */ export async function newUser( password: string ): Promise { const encryptionKeyPair = await DataEncryptor.generateAsymmetricEncryptionKeyPair(); const signingKeyPair = await DataEncryptor.generateSigningKeyPair(); const encryptor = DataEncryptor(encryptionKeyPair, signingKeyPair); // Generate the configuration for deriving our KEK const keyDerivationConfig = KeyDerivationConfig(); // Generate the configuration for encrypting and decrypting private keys const privateKeyEncryptionConfig = KeyEncryptionConfig(); // Build a KEK deriver from our KEK derivation config, and derive a key which will be used // to encrypt/decrypt our private keys const kekDeriver = keyDerivationConfig.buildDeriver(); const kek = await kekDeriver.deriveKey( password, privateKeyEncryptionConfig.getAlgorithm() ); // Build our key encryptor which we will use to encrypt our private keys const privateKeyEncryptor = privateKeyEncryptionConfig.buildKeyEncryptor( kek ); return { // This can be used to encrypt and decrypt our data encryptor: encryptor, // This contains all of the information required to be stored with the user for us to be // able to encrypt and decrypt any data at a later date. userDoc: { // eslint-disable-next-line @typescript-eslint/camelcase kek_derivation_config: keyDerivationConfig.toJSON(), // eslint-disable-next-line @typescript-eslint/camelcase private_key_encryption_config: privateKeyEncryptionConfig.toJSON(), // eslint-disable-next-line @typescript-eslint/camelcase encryption_key_pair: await encryptKeyPair( privateKeyEncryptor, encryptionKeyPair ), // eslint-disable-next-line @typescript-eslint/camelcase signing_key_pair: await encryptKeyPair( privateKeyEncryptor, signingKeyPair ) } }; } /** * Extract the encryption and signing key pairs from the user's [[UserDoc]]. * * @param useDoc - The encryption data stored against the user. Comes from [[newUser]]. * @param password - The password [[newUser]] was called with. * @param extractable - Should the keys be able to be exported? The answer is probably * no, unless the password is being changed. * * @return The encryption and signing key pairs. */ async function keysForExistingUser( userDoc: UserDoc, password: string, extractable = false ): Promise { // Build our key encryption key (KEK) derivation config and private key encryption // config from what was stored on the user document. This will ensure that we can be backwards // compatible if we ever upgrade our algorithms/key lengths/whatever const keyDerivationConfig = KeyDerivationConfig( userDoc.kek_derivation_config ); const privateKeyEncryptionConfig = KeyEncryptionConfig( userDoc.private_key_encryption_config ); // Derive the KEK from the config in exactly the same way that we do for a new user. This will // give us exactly the same key. const kekDeriver = keyDerivationConfig.buildDeriver(); const kek = await kekDeriver.deriveKey( password, privateKeyEncryptionConfig.getAlgorithm() ); const privateKeyEncryptor = privateKeyEncryptionConfig.buildKeyEncryptor( kek ); async function doDecryptKeyPair( keyPairJSON: EncryptedKeyPair, type: "encryption" | "signing" ): Promise { return await decryptKeyPair( privateKeyEncryptor, keyPairJSON, type, extractable ); } const encryptionKeyPair = await doDecryptKeyPair( userDoc.encryption_key_pair, "encryption" ); const signingKeyPair = await doDecryptKeyPair( userDoc.signing_key_pair, "signing" ); return { encryptionKeyPair, signingKeyPair }; } /** * Given the [[UserDoc]] from [[newUser]], and the password it was created with, build the user's * [[DataEncryptor]]. * * @param userDoc The [[UserDoc]] returned by [[newUser]] * @param password The password provided to [[newUser]] * * @return A [[DataEncryptor]] identical to the one created with [[newUser]] */ export async function encryptorFromExistingUser( userDoc: UserDoc, password: string ): Promise { const { encryptionKeyPair, signingKeyPair } = await keysForExistingUser( userDoc, password, false ); // Build the encryptor that can be used for encrypting and decrypting JSON data return DataEncryptor(encryptionKeyPair, signingKeyPair); } /** * Given already decrypted/decoded keys and a new password, generate * a new [[UserDoc]] that can be passed to [[encryptorFromExistingUser]] with the * new password. * * @param importedKeys The [[ImportedKeys]] * @param newPassword The new password for the user * * @return A [[UserDoc]] for use with the new password */ async function reencryptKeys( importedKeys: ImportedKeys, newPassword: string ): Promise { const { encryptionKeyPair, signingKeyPair } = importedKeys; const keyDerivationConfig = KeyDerivationConfig(); const privateKeyEncryptionConfig = KeyEncryptionConfig(); const kekDeriver = keyDerivationConfig.buildDeriver(); const kek = await kekDeriver.deriveKey( newPassword, privateKeyEncryptionConfig.getAlgorithm() ); const privateKeyEncryptor = privateKeyEncryptionConfig.buildKeyEncryptor( kek ); return { // eslint-disable-next-line @typescript-eslint/camelcase kek_derivation_config: keyDerivationConfig.toJSON(), // eslint-disable-next-line @typescript-eslint/camelcase private_key_encryption_config: privateKeyEncryptionConfig.toJSON(), // eslint-disable-next-line @typescript-eslint/camelcase encryption_key_pair: await encryptKeyPair( privateKeyEncryptor, encryptionKeyPair ), // eslint-disable-next-line @typescript-eslint/camelcase signing_key_pair: await encryptKeyPair( privateKeyEncryptor, signingKeyPair ) }; } /** * Given a [[UserDoc]], the password it was created with, and a new password, generate * a new [[UserDoc]] that can be passed to [[encryptorFromExistingUser]] with the * new password. * * @param userDoc The [[UserDoc]] returned by [[newUser]] * @param password The password provided to [[newUser]] * @param newPassword The new password for the user * * @return A [[UserDoc]] for use with the new password */ export async function changePassword( oldUserDoc: UserDoc, oldPassword: string, newPassword: string ): Promise { const importedKeys = await keysForExistingUser( oldUserDoc, oldPassword, true ); return await reencryptKeys(importedKeys, newPassword); } /** * Given a [[UserDoc]] and the password it was created with, generate a secret JSON * document that can be restored in case of the password being forgotten. * * @param userDoc The [[UserDoc]] returned by [[newUser]] * @param password The password provided to [[newUser]] * * @return [[ExportedKeys]] to be imported one day */ export async function exportKeys( userDoc: UserDoc, password: string ): Promise { const { encryptionKeyPair, signingKeyPair } = await keysForExistingUser( userDoc, password, true ); return { // eslint-disable-next-line @typescript-eslint/camelcase encryption_key_pair: await exportKeyPair(encryptionKeyPair), // eslint-disable-next-line @typescript-eslint/camelcase signing_key_pair: await exportKeyPair(signingKeyPair) }; } /** * Given [[ExportedKeys]] and a password to encrypt them with, generate a new [[UserDoc]]. * * @param exportedKeys The [[ExportedKeys]] returned by [[exportKeys]] * @param newPassword The new password for the user * * @return A [[UserDoc]] for use with the new password */ export async function importKeys( exportedKeys: ExportedKeys, newPassword: string ): Promise { const importedKeys = { encryptionKeyPair: await importKeyPair( exportedKeys.encryption_key_pair, "encryption", true ), signingKeyPair: await importKeyPair( exportedKeys.signing_key_pair, "signing", true ) }; return await reencryptKeys(importedKeys, newPassword); }