diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index 5eac35fc49..fdf0e6c3a4 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -64,8 +64,6 @@ export const TRANSACTION_EXPIRATION_LENGTH: number export const TRANSACTION_FEE_LENGTH: number export const LATEST_TRANSACTION_VERSION: number export declare function verifyTransactions(serializedTransactions: Array): boolean -export declare function encrypt(plaintext: Buffer, passphrase: string): Buffer -export declare function decrypt(encryptedBlob: Buffer, passphrase: string): Buffer export const enum LanguageCode { English = 0, ChineseSimplified = 1, diff --git a/ironfish-rust-nodejs/index.js b/ironfish-rust-nodejs/index.js index e746cf16e7..e4d1006a92 100644 --- a/ironfish-rust-nodejs/index.js +++ b/ironfish-rust-nodejs/index.js @@ -252,7 +252,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { FishHashContext, deserializePublicPackage, deserializeRound2CombinedPublicPackage, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, encrypt, decrypt, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig, xchacha20poly1305 } = nativeBinding +const { FishHashContext, deserializePublicPackage, deserializeRound2CombinedPublicPackage, KEY_LENGTH, NONCE_LENGTH, BoxKeyPair, randomBytes, boxMessage, unboxMessage, RollingFilter, initSignalHandler, triggerSegfault, ASSET_ID_LENGTH, ASSET_METADATA_LENGTH, ASSET_NAME_LENGTH, ASSET_LENGTH, Asset, NOTE_ENCRYPTION_KEY_LENGTH, MAC_LENGTH, ENCRYPTED_NOTE_PLAINTEXT_LENGTH, ENCRYPTED_NOTE_LENGTH, NoteEncrypted, PUBLIC_ADDRESS_LENGTH, RANDOMNESS_LENGTH, MEMO_LENGTH, AMOUNT_VALUE_LENGTH, DECRYPTED_NOTE_LENGTH, Note, PROOF_LENGTH, TRANSACTION_SIGNATURE_LENGTH, TRANSACTION_PUBLIC_KEY_RANDOMNESS_LENGTH, TRANSACTION_EXPIRATION_LENGTH, TRANSACTION_FEE_LENGTH, LATEST_TRANSACTION_VERSION, TransactionPosted, Transaction, verifyTransactions, UnsignedTransaction, LanguageCode, generateKey, spendingKeyToWords, wordsToSpendingKey, generatePublicAddressFromIncomingViewKey, generateKeyFromPrivateKey, initializeSapling, FoundBlockResult, ThreadPoolHandler, isValidPublicAddress, CpuCount, getCpuCount, generateRandomizedPublicKey, multisig, xchacha20poly1305 } = nativeBinding module.exports.FishHashContext = FishHashContext module.exports.deserializePublicPackage = deserializePublicPackage @@ -292,8 +292,6 @@ module.exports.TransactionPosted = TransactionPosted module.exports.Transaction = Transaction module.exports.verifyTransactions = verifyTransactions module.exports.UnsignedTransaction = UnsignedTransaction -module.exports.encrypt = encrypt -module.exports.decrypt = decrypt module.exports.LanguageCode = LanguageCode module.exports.generateKey = generateKey module.exports.spendingKeyToWords = spendingKeyToWords diff --git a/ironfish-rust-nodejs/src/xchacha20poly1305.rs b/ironfish-rust-nodejs/src/xchacha20poly1305.rs index ec23677c65..da3a5f6820 100644 --- a/ironfish-rust-nodejs/src/xchacha20poly1305.rs +++ b/ironfish-rust-nodejs/src/xchacha20poly1305.rs @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use ironfish::xchacha20poly1305::{ - self, EncryptOutput, XChaCha20Poly1305Key, KEY_LENGTH as KEY_SIZE, SALT_LENGTH as SALT_SIZE, + XChaCha20Poly1305Key, KEY_LENGTH as KEY_SIZE, SALT_LENGTH as SALT_SIZE, XNONCE_LENGTH as XNONCE_SIZE, }; use napi::{bindgen_prelude::*, JsBuffer}; @@ -139,26 +139,3 @@ impl NativeXChaCha20Poly1305Key { Ok(Buffer::from(&result[..])) } } - -#[napi] -pub fn encrypt(plaintext: JsBuffer, passphrase: String) -> Result { - let plaintext_bytes = plaintext.into_value()?; - let result = xchacha20poly1305::encrypt(plaintext_bytes.as_ref(), passphrase.as_bytes()) - .map_err(to_napi_err)?; - - let mut vec: Vec = vec![]; - result.write(&mut vec).map_err(to_napi_err)?; - - Ok(Buffer::from(&vec[..])) -} - -#[napi] -pub fn decrypt(encrypted_blob: JsBuffer, passphrase: String) -> Result { - let encrypted_bytes = encrypted_blob.into_value()?; - - let encrypted_output = EncryptOutput::read(encrypted_bytes.as_ref()).map_err(to_napi_err)?; - let result = - xchacha20poly1305::decrypt(encrypted_output, passphrase.as_bytes()).map_err(to_napi_err)?; - - Ok(Buffer::from(&result[..])) -} diff --git a/ironfish-rust/src/xchacha20poly1305.rs b/ironfish-rust/src/xchacha20poly1305.rs index e8af9077ab..cefb0fb2c3 100644 --- a/ironfish-rust/src/xchacha20poly1305.rs +++ b/ironfish-rust/src/xchacha20poly1305.rs @@ -4,8 +4,8 @@ use std::io; +use argon2::Argon2; use argon2::RECOMMENDED_SALT_LEN; -use argon2::{password_hash::SaltString, Argon2}; use chacha20poly1305::aead::Aead; use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce}; use hkdf::Hkdf; @@ -133,109 +133,6 @@ impl XChaCha20Poly1305Key { } } -#[derive(Debug)] -pub struct EncryptOutput { - pub salt: Vec, - - pub nonce: [u8; XNONCE_LENGTH], - - pub ciphertext: Vec, -} - -impl EncryptOutput { - pub fn write(&self, mut writer: W) -> Result<(), IronfishError> { - let salt_len = u32::try_from(self.salt.len())?.to_le_bytes(); - writer.write_all(&salt_len)?; - writer.write_all(&self.salt)?; - - writer.write_all(&self.nonce)?; - - let ciphertext_len = u32::try_from(self.ciphertext.len())?.to_le_bytes(); - writer.write_all(&ciphertext_len)?; - writer.write_all(&self.ciphertext)?; - - Ok(()) - } - - pub fn read(mut reader: R) -> Result { - let mut salt_len = [0u8; 4]; - reader.read_exact(&mut salt_len)?; - let salt_len = u32::from_le_bytes(salt_len) as usize; - - let mut salt = vec![0u8; salt_len]; - reader.read_exact(&mut salt)?; - - let mut nonce = [0u8; XNONCE_LENGTH]; - reader.read_exact(&mut nonce)?; - - let mut ciphertext_len = [0u8; 4]; - reader.read_exact(&mut ciphertext_len)?; - let ciphertext_len = u32::from_le_bytes(ciphertext_len) as usize; - - let mut ciphertext = vec![0u8; ciphertext_len]; - reader.read_exact(&mut ciphertext)?; - - Ok(EncryptOutput { - salt, - nonce, - ciphertext, - }) - } -} - -impl PartialEq for EncryptOutput { - fn eq(&self, other: &EncryptOutput) -> bool { - self.salt == other.salt && self.nonce == other.nonce && self.ciphertext == other.ciphertext - } -} - -fn derive_key(passphrase: &[u8], salt: &[u8]) -> Result { - let mut key = [0u8; KEY_LENGTH]; - let argon2 = Argon2::default(); - - argon2 - .hash_password_into(passphrase, salt, &mut key) - .map_err(|_| IronfishError::new(IronfishErrorKind::FailedArgon2Hash))?; - - Ok(Key::from(key)) -} - -pub fn encrypt(plaintext: &[u8], passphrase: &[u8]) -> Result { - let salt = SaltString::generate(&mut thread_rng()); - let salt_str = salt.to_string(); - let salt_bytes = salt_str.as_bytes(); - let key = derive_key(passphrase, salt_bytes)?; - - let cipher = XChaCha20Poly1305::new(&key); - let mut nonce_bytes = [0u8; XNONCE_LENGTH]; - thread_rng().fill_bytes(&mut nonce_bytes); - let nonce = XNonce::from_slice(&nonce_bytes); - - let ciphertext = cipher - .encrypt(nonce, plaintext) - .map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Encryption))?; - - Ok(EncryptOutput { - salt: salt_bytes.to_vec(), - nonce: nonce_bytes, - ciphertext, - }) -} - -pub fn decrypt( - encrypted_output: EncryptOutput, - passphrase: &[u8], -) -> Result, IronfishError> { - let nonce = XNonce::from_slice(&encrypted_output.nonce); - - let key = derive_key(passphrase, &encrypted_output.salt[..])?; - let cipher = XChaCha20Poly1305::new(&key); - - cipher - .decrypt(nonce, encrypted_output.ciphertext.as_ref()) - .map_err(|_| IronfishError::new(IronfishErrorKind::FailedXChaCha20Poly1305Decryption)) -} - #[cfg(test)] mod test { use crate::xchacha20poly1305::XChaCha20Poly1305Key; diff --git a/ironfish/src/rpc/routes/wallet/create.ts b/ironfish/src/rpc/routes/wallet/create.ts index bba982d93b..18738ebd4f 100644 --- a/ironfish/src/rpc/routes/wallet/create.ts +++ b/ironfish/src/rpc/routes/wallet/create.ts @@ -30,9 +30,7 @@ routes.register( ) } - const account = await context.wallet.createAccount(name, { - passphrase: request.data.passphrase, - }) + const account = await context.wallet.createAccount(name) if (context.wallet.nodeClient) { void context.wallet.scan() } diff --git a/ironfish/src/rpc/routes/wallet/createAccount.ts b/ironfish/src/rpc/routes/wallet/createAccount.ts index 8e1566d066..4a9fb5e32a 100644 --- a/ironfish/src/rpc/routes/wallet/createAccount.ts +++ b/ironfish/src/rpc/routes/wallet/createAccount.ts @@ -18,7 +18,7 @@ import { AssertHasRpcContext } from '../rpcContext' * Hence, we're adding a new createAccount endpoint and will eventually sunset the create endpoint. */ -export type CreateAccountRequest = { name: string; default?: boolean; passphrase?: string } +export type CreateAccountRequest = { name: string; default?: boolean } export type CreateAccountResponse = { name: string publicAddress: string @@ -29,7 +29,6 @@ export const CreateAccountRequestSchema: yup.ObjectSchema .object({ name: yup.string().defined(), default: yup.boolean().optional(), - passphrase: yup.string().optional(), }) .defined() @@ -49,9 +48,7 @@ routes.register( let account try { - account = await context.wallet.createAccount(request.data.name, { - passphrase: request.data.passphrase, - }) + account = await context.wallet.createAccount(request.data.name) } catch (e) { if (e instanceof DuplicateAccountNameError) { throw new RpcValidationError(e.message, 400, RPC_ERROR_CODES.DUPLICATE_ACCOUNT_NAME) diff --git a/ironfish/src/rpc/routes/wallet/rename.ts b/ironfish/src/rpc/routes/wallet/rename.ts index f0461f4bef..1d07190a8c 100644 --- a/ironfish/src/rpc/routes/wallet/rename.ts +++ b/ironfish/src/rpc/routes/wallet/rename.ts @@ -21,7 +21,7 @@ routes.register( AssertHasRpcContext(request, context, 'wallet') const account = getAccount(context.wallet, request.data.account) - await account.setName(request.data.newName, { passphrase: request.data.passphrase }) + await context.wallet.setName(account, request.data.newName) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/renameAccount.ts b/ironfish/src/rpc/routes/wallet/renameAccount.ts index 05f4761020..6c5de737ef 100644 --- a/ironfish/src/rpc/routes/wallet/renameAccount.ts +++ b/ironfish/src/rpc/routes/wallet/renameAccount.ts @@ -7,14 +7,13 @@ import { routes } from '../router' import { AssertHasRpcContext } from '../rpcContext' import { getAccount } from './utils' -export type RenameAccountRequest = { account: string; newName: string; passphrase?: string } +export type RenameAccountRequest = { account: string; newName: string } export type RenameAccountResponse = undefined export const RenameAccountRequestSchema: yup.ObjectSchema = yup .object({ account: yup.string().defined(), newName: yup.string().defined(), - passphrase: yup.string().optional(), }) .defined() @@ -29,7 +28,7 @@ routes.register( AssertHasRpcContext(request, context, 'wallet') const account = getAccount(context.wallet, request.data.account) - await account.setName(request.data.newName, { passphrase: request.data.passphrase }) + await context.wallet.setName(account, request.data.newName) request.end() }, ) diff --git a/ironfish/src/rpc/routes/wallet/resetAccount.ts b/ironfish/src/rpc/routes/wallet/resetAccount.ts index 56ec609bb4..22e6d86467 100644 --- a/ironfish/src/rpc/routes/wallet/resetAccount.ts +++ b/ironfish/src/rpc/routes/wallet/resetAccount.ts @@ -11,7 +11,6 @@ export type ResetAccountRequest = { account: string resetCreatedAt?: boolean resetScanningEnabled?: boolean - passphrase?: string } export type ResetAccountResponse = undefined @@ -20,7 +19,6 @@ export const ResetAccountRequestSchema: yup.ObjectSchema = account: yup.string().defined(), resetCreatedAt: yup.boolean(), resetScanningEnabled: yup.boolean(), - passphrase: yup.string().optional(), }) .defined() @@ -39,7 +37,6 @@ routes.register( await context.wallet.resetAccount(account, { resetCreatedAt: request.data.resetCreatedAt, resetScanningEnabled: request.data.resetScanningEnabled, - passphrase: request.data.passphrase, }) request.end() diff --git a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture index 351defc450..b0e82282cf 100644 --- a/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture +++ b/ironfish/src/wallet/account/__fixtures__/account.test.ts.fixture @@ -5909,13 +5909,13 @@ "value": { "encrypted": false, "version": 4, - "id": "6f0698b4-a99c-46aa-9391-59f0c55cd755", + "id": "aea047be-8c73-448e-8e8d-22844f49736f", "name": "accountA", - "spendingKey": "89001fcdef6bff7e9fd76d4ae6275bf6786afc2797eded9df094ae4a6894782d", - "viewKey": "389bc77ae499f3edc0dc445a732add6f36c275b260efe2c791cc515f6c2c0cd75f5b31e9b1f82edb0ee57b9304ece90d48f43c36d78660b04960b9692485d058", - "incomingViewKey": "fdd70ff012b4e48576bdd71207ebe1e3747811c77fa1a25862c3b869123ce007", - "outgoingViewKey": "9664b763a0418a476d072c715fcbbba58bcaf60cc951ba017195d75453131f11", - "publicAddress": "14a9bfb247dcf632f85ff79ebef222cc9ccf364f9b3e3e0ee39b75d68f80782a", + "spendingKey": "431a4e45c614fd41d7cee2de809e4464e92a125752d6f6839c0af7c706a01f67", + "viewKey": "4be28bbdca174c4e7499d913ea02642d74136eea9058f4e6eb7586dd5c864e905128a5aa5d25356934e5f4cf36575445b8dc60bfa672e1c135521cb5610e9b4c", + "incomingViewKey": "78b6e7887d55853b84d5d0b3b80d787379750b8839ddbc738882124453a5c004", + "outgoingViewKey": "3ce3ee26d7ab6c76838b6920eb6b5cf8fa3aa1ca54940d3b3cbf20532be34e41", + "publicAddress": "09d7f58ee5ae406b19e714b7fab882b83334e5b566a64c9d9707fcc513426163", "createdAt": { "hash": { "type": "Buffer", @@ -5924,7 +5924,7 @@ "sequence": 1 }, "scanningEnabled": true, - "proofAuthorizingKey": "8c11ae2523136f4b11bada56d9bcab2b6591cc99cf7aede8a238c5fefd7fce0d" + "proofAuthorizingKey": "c95606e98895f390f247f0c9e4beb22a039adfdab5cd11dac28263870dfe4802" }, "head": { "hash": { @@ -5935,18 +5935,18 @@ } } ], - "Accounts setName should throw an error if the passphrase is incorrect and the wallet is encrypted": [ + "Accounts setName should throw an error if there is no master key and the wallet is encrypted": [ { "value": { "encrypted": false, "version": 4, - "id": "92b122e5-9f14-453b-a364-e20a7d107305", + "id": "df4836f9-2889-471c-b9c3-c1aaa38fc595", "name": "accountA", - "spendingKey": "3d68ebfd3d600792fc94d583bbab97ab4d02b9f41aa2a6655e151e41d4a33d8d", - "viewKey": "51e699920432cf221351568ade21fb8af4110c7ad57ad90cc2664340d50d6f207a19eb846feb76588f467e4dce99d0da074ca680aca11d3d8c91bd47ae5d9081", - "incomingViewKey": "15f8cb20e3a7b494474f4c4737bc0403dbb5de6e532e2c83a4469cd7f95f5b02", - "outgoingViewKey": "fc1b2e0e85cca6ddaf657baf2c69c76b403afe1adadd73b794c5cb81724d240d", - "publicAddress": "899ea1e9be7202aedab0a41f6b9cf661ce20e41ba11c1ee9d15ac64d8c96a391", + "spendingKey": "433f92654fe57940d18e87481c52588fec36fa552f64dca434b1726e022722cd", + "viewKey": "1d6e8a7faea61bdfd77d6e3c3b63951b3e613a8907bd6786fe0466e3490a8558eec35c9e08b3abbbaa0b596f427801bfc306297d0c3bf1b50ad02aa62cae702f", + "incomingViewKey": "88328774271cf9b9437e0b76ffc05c5697742d2004b9430a257590cceccbb000", + "outgoingViewKey": "5cb8a083108e4f3aa7d5db9a6ddce584d9b3a941cf707ee691b016ddf6c0fc87", + "publicAddress": "534ba54bbf1721cb16e4c8a702f25d96a0c99c2f02c38431be8870a28fc04914", "createdAt": { "hash": { "type": "Buffer", @@ -5955,7 +5955,7 @@ "sequence": 1 }, "scanningEnabled": true, - "proofAuthorizingKey": "d0d08a05953a50a0ce8b35e0c410b7dde77e3bf6099aaa4fc413de2096f7ef0a" + "proofAuthorizingKey": "25079aaa8548a644fc43ba77584bb0cb53adb60e3b2a033865df05569ee44302" }, "head": { "hash": { @@ -5971,13 +5971,13 @@ "value": { "encrypted": false, "version": 4, - "id": "1dd7e196-ffed-4fe0-8dbd-c49f82bf44b7", + "id": "de02e4f1-ad01-464d-9e5b-9b4e83c57f0c", "name": "accountA", - "spendingKey": "71ec323628c95bd56df353ec444b8ceb3d463603e82a89d4ac3336bdf630993a", - "viewKey": "5219abc719ecb8954e77d89e60f4fc82588e8f619ba4da38b53ed0471aeccb200cc7cec29ca533c769c96213417f78ccfaf2ba3f12a4b948a0da242805eb044c", - "incomingViewKey": "d9d70e59490b44c078300a23376e4fc516b47a31231c77538d17740f399e3d00", - "outgoingViewKey": "e3eca4b31e0d4b48e27458db2eff35b4d713d84b0b07505fdc8d49b2e40ab69d", - "publicAddress": "fbc47fe75ef9534b28b95fd17288488b89a4b752191a323a3703520095e8c24b", + "spendingKey": "4c946561f929bfc55faf90cda2f3c8bc6881b0cf96f0a422acb4c2240d5b995b", + "viewKey": "3120c5db86dd17d6b11f14df5f652878306aba202c74db71aad778e45ae4888843eaa4e8fa18ee78a762c2ed3b024c29d0ccd4163b0e49dba1e9c0b2055fd5b5", + "incomingViewKey": "2b1bcb2a5821e8f0485740aaec3157d43af552e853c97fa09dedb86442095003", + "outgoingViewKey": "dff7ee410247d8d2b63cd0da40509bd8fc3e96a4d26c04ec9fdb016da2fbb2ea", + "publicAddress": "9a3f5703d2cf40fc2899bf25793249b44e187acbd5c1f6f4f20ad4143408e42e", "createdAt": { "hash": { "type": "Buffer", @@ -5986,7 +5986,7 @@ "sequence": 1 }, "scanningEnabled": true, - "proofAuthorizingKey": "f335356ed8f77c4facebc2ad9377ab05e6d71ec83aa772df0c23e2733458a10c" + "proofAuthorizingKey": "8726a9a636670be8e1fd12a0b4b42a1d0e30a526b28cae014afc7b241ad1fb0d" }, "head": { "hash": { diff --git a/ironfish/src/wallet/account/account.test.ts b/ironfish/src/wallet/account/account.test.ts index 9701a0b410..47cb5b61e0 100644 --- a/ironfish/src/wallet/account/account.test.ts +++ b/ironfish/src/wallet/account/account.test.ts @@ -17,6 +17,7 @@ import { useTxFixture, } from '../../testUtilities' import { AsyncUtils } from '../../utils/async' +import { MasterKey } from '../masterKey' import { BalanceValue } from '../walletdb/balanceValue' import { Account } from './account' import { EncryptedAccount } from './encryptedAccount' @@ -198,14 +199,14 @@ describe('Accounts', () => { await expect(account.setName('B')).rejects.toThrow() }) - it('should throw an error if the passphrase is incorrect and the wallet is encrypted', async () => { + it('should throw an error if there is no master key and the wallet is encrypted', async () => { const { node } = nodeTest const passphrase = 'foo' const account = await useAccountFixture(node.wallet, 'accountA') await node.wallet.encrypt(passphrase) - await expect(account.setName('B', { passphrase: 'incorrect ' })).rejects.toThrow() + await expect(account.setName('B')).rejects.toThrow() }) it('should save the encrypted account if the passphrase is correct and the wallet is encrypted', async () => { @@ -216,17 +217,23 @@ describe('Accounts', () => { const account = await useAccountFixture(node.wallet, 'accountA') await node.wallet.encrypt(passphrase) - await account.setName(newName, { passphrase }) + await node.wallet.unlock(passphrase) + await node.wallet.setName(account, newName) + await node.wallet.lock() const accountValue = await node.wallet.walletDb.accounts.get(account.id) Assert.isNotUndefined(accountValue) Assert.isTrue(accountValue.encrypted) const encryptedAccount = new EncryptedAccount({ - data: accountValue.data, + accountValue, walletDb: node.wallet.walletDb, }) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) expect(decryptedAccount.name).toEqual(newName) }) @@ -2668,8 +2675,11 @@ describe('Accounts', () => { const account = await useAccountFixture(node.wallet) const passphrase = 'foo' - const encryptedAccount = account.encrypt(passphrase) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + const masterKey = MasterKey.generate(passphrase) + const key = await masterKey.unlock(passphrase) + const encryptedAccount = account.encrypt(masterKey) + + const decryptedAccount = encryptedAccount.decrypt(key) expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) }) diff --git a/ironfish/src/wallet/account/account.ts b/ironfish/src/wallet/account/account.ts index 0c47c5160b..9f880e0558 100644 --- a/ironfish/src/wallet/account/account.ts +++ b/ironfish/src/wallet/account/account.ts @@ -1,8 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { encrypt, multisig } from '@ironfish/rust-nodejs' -import { Asset } from '@ironfish/rust-nodejs' +import { Asset, multisig } from '@ironfish/rust-nodejs' import { BufferMap, BufferSet } from 'buffer-map' import MurmurHash3 from 'imurmurhash' import { Assert } from '../../assert' @@ -15,6 +14,7 @@ import { WithNonNull, WithRequired } from '../../utils' import { DecryptedNote } from '../../workerPool/tasks/decryptNotes' import { AssetBalances } from '../assetBalances' import { MultisigKeys, MultisigSigner } from '../interfaces/multisigKeys' +import { MasterKey } from '../masterKey' import { AccountValueEncoding, DecryptedAccountValue } from '../walletdb/accountValue' import { AssetValue } from '../walletdb/assetValue' import { BalanceValue } from '../walletdb/balanceValue' @@ -129,7 +129,7 @@ export class Account { async setName( name: string, - options?: { passphrase?: string }, + options?: { masterKey: MasterKey | null }, tx?: IDatabaseTransaction, ): Promise { if (!name.trim()) { @@ -141,8 +141,9 @@ export class Account { this.name = name if (walletEncrypted) { - Assert.isNotUndefined(options?.passphrase) - await this.walletDb.setEncryptedAccount(this, options.passphrase, tx) + Assert.isNotUndefined(options) + Assert.isNotNull(options?.masterKey) + await this.walletDb.setEncryptedAccount(this, options.masterKey, tx) } else { await this.walletDb.setAccount(this, tx) } @@ -1330,13 +1331,19 @@ export class Account { return publicKeyPackage.identities() } - encrypt(passphrase: string): EncryptedAccount { + encrypt(masterKey: MasterKey): EncryptedAccount { const encoder = new AccountValueEncoding() const serialized = encoder.serialize(this.serialize()) - const data = encrypt(serialized, passphrase) + const derivedKey = masterKey.deriveNewKey() + const data = derivedKey.encrypt(serialized) return new EncryptedAccount({ - data, + accountValue: { + encrypted: true, + data, + salt: derivedKey.salt(), + nonce: derivedKey.nonce(), + }, walletDb: this.walletDb, }) } diff --git a/ironfish/src/wallet/account/encryptedAccount.test.ts b/ironfish/src/wallet/account/encryptedAccount.test.ts index 9b91821a1c..6dac099c3b 100644 --- a/ironfish/src/wallet/account/encryptedAccount.test.ts +++ b/ironfish/src/wallet/account/encryptedAccount.test.ts @@ -4,6 +4,7 @@ import { useAccountFixture } from '../../testUtilities/fixtures/account' import { createNodeTest } from '../../testUtilities/nodeTest' import { AccountDecryptionFailedError } from '../errors' +import { MasterKey } from '../masterKey' describe('EncryptedAccount', () => { const nodeTest = createNodeTest() @@ -13,8 +14,11 @@ describe('EncryptedAccount', () => { const { node } = nodeTest const account = await useAccountFixture(node.wallet) - const encryptedAccount = account.encrypt(passphrase) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + const masterKey = MasterKey.generate(passphrase) + const key = await masterKey.unlock(passphrase) + + const encryptedAccount = account.encrypt(masterKey) + const decryptedAccount = encryptedAccount.decrypt(key) expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) }) @@ -25,10 +29,13 @@ describe('EncryptedAccount', () => { const { node } = nodeTest const account = await useAccountFixture(node.wallet) - const encryptedAccount = account.encrypt(passphrase) + const masterKey = MasterKey.generate(passphrase) + const invalidMasterKey = MasterKey.generate(invalidPassphrase) + const invalidKey = await invalidMasterKey.unlock(passphrase) + + await masterKey.unlock(passphrase) + const encryptedAccount = account.encrypt(masterKey) - expect(() => encryptedAccount.decrypt(invalidPassphrase)).toThrow( - AccountDecryptionFailedError, - ) + expect(() => encryptedAccount.decrypt(invalidKey)).toThrow(AccountDecryptionFailedError) }) }) diff --git a/ironfish/src/wallet/account/encryptedAccount.ts b/ironfish/src/wallet/account/encryptedAccount.ts index 580cceca98..44f321d4d7 100644 --- a/ironfish/src/wallet/account/encryptedAccount.ts +++ b/ironfish/src/wallet/account/encryptedAccount.ts @@ -1,7 +1,7 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { decrypt, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { xchacha20poly1305 } from '@ironfish/rust-nodejs' import { AccountDecryptionFailedError } from '../errors' import { AccountValueEncoding, EncryptedAccountValue } from '../walletdb/accountValue' import { WalletDB } from '../walletdb/walletdb' @@ -13,16 +13,25 @@ export class EncryptedAccount { readonly nonce: Buffer readonly data: Buffer - constructor({ data, walletDb }: { data: Buffer; walletDb: WalletDB }) { - this.salt = Buffer.alloc(xchacha20poly1305.XSALT_LENGTH) - this.nonce = Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH) - this.data = data + constructor({ + accountValue, + walletDb, + }: { + accountValue: EncryptedAccountValue + walletDb: WalletDB + }) { + this.salt = accountValue.salt + this.nonce = accountValue.nonce + this.data = accountValue.data this.walletDb = walletDb } - decrypt(passphrase: string): Account { + decrypt(masterKey: xchacha20poly1305.XChaCha20Poly1305Key): Account { try { - const decryptedAccountValue = decrypt(this.data, passphrase) + const key = masterKey.deriveKey(this.salt, this.nonce) + const decryptedAccountValue = key.decrypt(this.data) + key.destroy() + const encoder = new AccountValueEncoding() const accountValue = encoder.deserializeDecrypted(decryptedAccountValue) diff --git a/ironfish/src/wallet/masterKey.ts b/ironfish/src/wallet/masterKey.ts index b2dab80726..4b3ba40cc8 100644 --- a/ironfish/src/wallet/masterKey.ts +++ b/ironfish/src/wallet/masterKey.ts @@ -90,4 +90,10 @@ export class MasterKey { return this.masterKey.deriveKey(salt, nonce) } + + async destroy(): Promise { + await this.lock() + this.nonce.fill(0) + this.salt.fill(0) + } } diff --git a/ironfish/src/wallet/wallet.test.ts b/ironfish/src/wallet/wallet.test.ts index 44ebfcd7ca..f6b1213e59 100644 --- a/ironfish/src/wallet/wallet.test.ts +++ b/ironfish/src/wallet/wallet.test.ts @@ -675,29 +675,6 @@ describe('Wallet', () => { await expect(node.wallet.importAccount(accountValue)).rejects.toThrow() }) - it('should throw an error when the wallet is encrypted and the passphrase is incorrect', async () => { - const { node } = await nodeTest.createSetup() - const passphrase = 'foo' - - await useAccountFixture(node.wallet, 'A') - await node.wallet.encrypt(passphrase) - - const key = generateKey() - const accountValue: DecryptedAccountValue = { - encrypted: false, - id: '0', - name: 'new-account', - version: 1, - createdAt: null, - scanningEnabled: false, - ...key, - } - - await expect( - node.wallet.importAccount(accountValue, { passphrase: 'incorrect' }), - ).rejects.toThrow('Your passphrase is incorrect') - }) - it('should encrypt and store the account if the wallet is encrypted', async () => { const { node } = await nodeTest.createSetup() const passphrase = 'foo' @@ -716,7 +693,10 @@ describe('Wallet', () => { ...key, } - const account = await node.wallet.importAccount(accountValue, { passphrase }) + await node.wallet.unlock(passphrase) + const account = await node.wallet.importAccount(accountValue) + await node.wallet.lock() + expect(account.name).toEqual(accountValue.name) expect(account.viewKey).toEqual(key.viewKey) expect(account.incomingViewKey).toEqual(key.incomingViewKey) @@ -1065,18 +1045,6 @@ describe('Wallet', () => { await expect(node.wallet.createAccount('B')).rejects.toThrow() }) - it('should throw an error if the wallet is encrypted and an incorrect passphrase is provided', async () => { - const { node } = await nodeTest.createSetup() - const passphrase = 'foo' - - await useAccountFixture(node.wallet, 'A') - await node.wallet.encrypt(passphrase) - - await expect( - node.wallet.createAccount('B', { passphrase: 'incorrect ' }), - ).rejects.toThrow() - }) - it('should save a new encrypted account with the correct passphrase', async () => { const { node } = await nodeTest.createSetup() const passphrase = 'foo' @@ -1084,17 +1052,23 @@ describe('Wallet', () => { await useAccountFixture(node.wallet, 'A') await node.wallet.encrypt(passphrase) - const account = await node.wallet.createAccount('B', { passphrase }) + await node.wallet.unlock(passphrase) + const account = await node.wallet.createAccount('B') const accountValue = await node.wallet.walletDb.accounts.get(account.id) Assert.isNotUndefined(accountValue) Assert.isTrue(accountValue.encrypted) const encryptedAccount = new EncryptedAccount({ - data: accountValue.data, + accountValue, walletDb: node.wallet.walletDb, }) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) + await node.wallet.lock() expect(decryptedAccount.spendingKey).toEqual(account.spendingKey) expect(decryptedAccount.name).toEqual(account.name) @@ -2478,18 +2452,6 @@ describe('Wallet', () => { await expect(node.wallet.resetAccount(account)).rejects.toThrow() }) - it('should throw an error if the wallet is encrypted and the passphrase is incorrect', async () => { - const { node } = await nodeTest.createSetup() - const passphrase = 'foo' - - const account = await useAccountFixture(node.wallet, 'A') - await node.wallet.encrypt(passphrase) - - await expect( - node.wallet.resetAccount(account, { passphrase: 'incorrect' }), - ).rejects.toThrow() - }) - it('save the encrypted account when the wallet is encrypted and passphrase is valid', async () => { const { node } = await nodeTest.createSetup() const passphrase = 'foo' @@ -2497,7 +2459,8 @@ describe('Wallet', () => { const account = await useAccountFixture(node.wallet, 'A') await node.wallet.encrypt(passphrase) - await node.wallet.resetAccount(account, { passphrase }) + await node.wallet.unlock(passphrase) + await node.wallet.resetAccount(account) const newAccount = node.wallet.getAccountByName(account.name) Assert.isNotNull(newAccount) @@ -2505,7 +2468,12 @@ describe('Wallet', () => { const encryptedAccount = node.wallet.encryptedAccountById.get(newAccount.id) Assert.isNotUndefined(encryptedAccount) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) + await node.wallet.lock() + expect(decryptedAccount.name).toEqual(account.name) expect(decryptedAccount.spendingKey).toEqual(account.spendingKey) }) @@ -2724,14 +2692,18 @@ describe('Wallet', () => { expect(node.wallet.accounts).toHaveLength(0) expect(node.wallet.encryptedAccounts).toHaveLength(2) + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + const encryptedAccountA = node.wallet.encryptedAccountById.get(accountA.id) Assert.isNotUndefined(encryptedAccountA) - const decryptedAccountA = encryptedAccountA.decrypt(passphrase) + const decryptedAccountA = encryptedAccountA.decrypt(key) expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) const encryptedAccountB = node.wallet.encryptedAccountById.get(accountB.id) Assert.isNotUndefined(encryptedAccountB) - const decryptedAccountB = encryptedAccountB.decrypt(passphrase) + const decryptedAccountB = encryptedAccountB.decrypt(key) expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) }) }) @@ -2866,10 +2838,14 @@ describe('Wallet', () => { expect(node.wallet.encryptedAccounts).toHaveLength(2) expect(node.wallet.locked).toBe(false) + const masterKey = node.wallet['masterKey'] + Assert.isNotNull(masterKey) + const key = await masterKey.unlock(passphrase) + for (const [id, account] of node.wallet.accountById.entries()) { const encryptedAccount = node.wallet.encryptedAccountById.get(id) Assert.isNotUndefined(encryptedAccount) - const decryptedAccount = encryptedAccount.decrypt(passphrase) + const decryptedAccount = encryptedAccount.decrypt(key) expect(account.serialize()).toMatchObject(decryptedAccount.serialize()) } diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 94585bd740..bf7d738667 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -59,6 +59,7 @@ import { isMultisigSignerTrustedDealerImport, } from './exporter/multisig' import { MintAssetOptions } from './interfaces/mintAssetOptions' +import { MasterKey } from './masterKey' import { ScanState } from './scanner/scanState' import { WalletScanner } from './scanner/walletScanner' import { AssetValue } from './walletdb/assetValue' @@ -95,7 +96,7 @@ export type TransactionOutput = { assetId: Buffer } -export const DEFAULT_UNLOCK_TIMEOUT_MS = 5 * 60 * 1000 +export const DEFAULT_UNLOCK_TIMEOUT_MS = 24 * 60 * 60 * 1000 export class Wallet { readonly onAccountImported = new Event<[account: Account]>() @@ -111,6 +112,7 @@ export class Wallet { private readonly config: Config private readonly consensus: Consensus readonly networkId: number + private masterKey: MasterKey | null protected rebroadcastAfter: number protected defaultAccount: string | null = null @@ -155,6 +157,7 @@ export class Wallet { this.rebroadcastAfter = rebroadcastAfter ?? 10 this.locked = false this.lockTimeout = null + this.masterKey = null this.createTransactionMutex = new Mutex() this.eventLoopAbortController = new AbortController() @@ -223,12 +226,18 @@ export class Wallet { private async load(): Promise { this.encryptedAccountById.clear() this.accountById.clear() + this.masterKey = null + + const masterKeyValue = await this.walletDb.loadMasterKey() + if (masterKeyValue) { + this.masterKey = new MasterKey(masterKeyValue) + } for await (const [id, accountValue] of this.walletDb.loadAccounts()) { if (accountValue.encrypted) { const encryptedAccount = new EncryptedAccount({ - data: accountValue.data, walletDb: this.walletDb, + accountValue, }) this.encryptedAccountById.set(id, encryptedAccount) @@ -280,6 +289,10 @@ export class Wallet { clearTimeout(this.eventLoopTimeout) } + if (this.masterKey) { + await this.masterKey.destroy() + } + this.stopUnlockTimeout() await this.scanner.abort() @@ -1340,7 +1353,7 @@ export class Wallet { async createAccount( name: string, - options: { createdAt?: HeadValue | null; setDefault?: boolean; passphrase?: string } = { + options: { createdAt?: HeadValue | null; setDefault?: boolean } = { setDefault: false, }, ): Promise { @@ -1387,10 +1400,10 @@ export class Wallet { const accountsEncrypted = await this.walletDb.accountsEncrypted(tx) if (accountsEncrypted) { - Assert.isNotUndefined(options.passphrase) + Assert.isNotNull(this.masterKey) const encryptedAccount = await this.walletDb.setEncryptedAccount( account, - options.passphrase, + this.masterKey, tx, ) this.encryptedAccountById.set(account.id, encryptedAccount) @@ -1417,7 +1430,7 @@ export class Wallet { async importAccount( accountValue: AccountImport, - options?: { createdAt?: number; passphrase?: string }, + options?: { createdAt?: number }, ): Promise { let multisigKeys = accountValue.multisigKeys let secret: Buffer | undefined @@ -1499,8 +1512,8 @@ export class Wallet { const encrypted = await this.walletDb.accountsEncrypted(tx) if (encrypted) { - Assert.isNotUndefined(options?.passphrase) - await this.walletDb.setEncryptedAccount(account, options.passphrase, tx) + Assert.isNotNull(this.masterKey) + await this.walletDb.setEncryptedAccount(account, this.masterKey, tx) } else { await this.walletDb.setAccount(account, tx) } @@ -1552,6 +1565,10 @@ export class Wallet { return account } + async setName(account: Account, name: string, tx?: IDatabaseTransaction): Promise { + await account.setName(name, { masterKey: this.masterKey }, tx) + } + get accounts(): Account[] { return Array.from(this.accountById.values()) } @@ -1569,7 +1586,6 @@ export class Wallet { options?: { resetCreatedAt?: boolean resetScanningEnabled?: boolean - passphrase?: string }, tx?: IDatabaseTransaction, ): Promise { @@ -1590,10 +1606,10 @@ export class Wallet { const encrypted = await this.walletDb.accountsEncrypted(tx) if (encrypted) { - Assert.isNotUndefined(options?.passphrase) + Assert.isNotNull(this.masterKey) const encryptedAccount = await this.walletDb.setEncryptedAccount( newAccount, - options.passphrase, + this.masterKey, tx, ) this.encryptedAccountById.set(newAccount.id, encryptedAccount) @@ -1914,6 +1930,7 @@ export class Wallet { const unlock = await this.createTransactionMutex.lock() try { + Assert.isNull(this.masterKey) await this.walletDb.encryptAccounts(passphrase, tx) await this.load() } finally { @@ -1948,6 +1965,10 @@ export class Wallet { this.accountById.clear() this.locked = true + if (this.masterKey) { + await this.masterKey.lock() + } + this.logger.info( 'Wallet locked. Unlock the wallet to view your accounts and create transactions', ) @@ -1965,8 +1986,11 @@ export class Wallet { return } + Assert.isNotNull(this.masterKey) + const key = await this.masterKey.unlock(passphrase) + for (const [id, account] of this.encryptedAccountById.entries()) { - this.accountById.set(id, account.decrypt(passphrase)) + this.accountById.set(id, account.decrypt(key)) } this.startUnlockTimeout(timeout) diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index 6260f5e60e..252f065dbd 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -1,7 +1,8 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { encrypt, generateKey, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { generateKey, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import { MasterKey } from '../masterKey' import { AccountValueEncoding, DecryptedAccountValue, @@ -64,7 +65,7 @@ describe('AccountValueEncoding', () => { expect(deserializedValue).toEqual(value) }) - it('serializes an object encrypted account data into a buffer and deserializes to the original object', () => { + it('serializes an object encrypted account data into a buffer and deserializes to the original object', async () => { const encoder = new AccountValueEncoding() const key = generateKey() @@ -89,8 +90,11 @@ describe('AccountValueEncoding', () => { } const passphrase = 'foobarbaz' + const masterKey = MasterKey.generate(passphrase) + const xchacha20poly1305Key = await masterKey.unlock(passphrase) + const data = encoder.serialize(value) - const encryptedData = encrypt(data, passphrase) + const encryptedData = xchacha20poly1305Key.encrypt(data) const encryptedValue: EncryptedAccountValue = { encrypted: true, diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.test.ts b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts index 351f643200..497deaf11f 100644 --- a/ironfish/src/wallet/walletdb/masterKeyValue.test.ts +++ b/ironfish/src/wallet/walletdb/masterKeyValue.test.ts @@ -2,31 +2,18 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { xchacha20poly1305 } from '@ironfish/rust-nodejs' -import { MasterKeyValue, NullableMasterKeyValueEncoding } from './masterKeyValue' +import { MasterKeyValue, MasterKeyValueEncoding } from './masterKeyValue' describe('MasterKeyValueEncoding', () => { - describe('with a defined value', () => { - it('serializes the value into a buffer and deserializes to the original value', () => { - const encoder = new NullableMasterKeyValueEncoding() + it('serializes the value into a buffer and deserializes to the original value', () => { + const encoder = new MasterKeyValueEncoding() - const value: MasterKeyValue = { - nonce: Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH), - salt: Buffer.alloc(xchacha20poly1305.XSALT_LENGTH), - } - const buffer = encoder.serialize(value) - const deserializedValue = encoder.deserialize(buffer) - expect(deserializedValue).toEqual(value) - }) - }) - - describe('with a null value', () => { - it('serializes the value into a buffer and deserializes to the original value', () => { - const encoder = new NullableMasterKeyValueEncoding() - - const value = null - const buffer = encoder.serialize(value) - const deserializedValue = encoder.deserialize(buffer) - expect(deserializedValue).toEqual(value) - }) + const value: MasterKeyValue = { + nonce: Buffer.alloc(xchacha20poly1305.XNONCE_LENGTH), + salt: Buffer.alloc(xchacha20poly1305.XSALT_LENGTH), + } + const buffer = encoder.serialize(value) + const deserializedValue = encoder.deserialize(buffer) + expect(deserializedValue).toEqual(value) }) }) diff --git a/ironfish/src/wallet/walletdb/masterKeyValue.ts b/ironfish/src/wallet/walletdb/masterKeyValue.ts index a706dcfafa..6a739ab0e8 100644 --- a/ironfish/src/wallet/walletdb/masterKeyValue.ts +++ b/ironfish/src/wallet/walletdb/masterKeyValue.ts @@ -10,37 +10,23 @@ export type MasterKeyValue = { salt: Buffer } -export class NullableMasterKeyValueEncoding - implements IDatabaseEncoding -{ - serialize(value: MasterKeyValue | null): Buffer { - const bw = bufio.write(this.getSize(value)) - - if (value) { - bw.writeBytes(value.nonce) - bw.writeBytes(value.salt) - } - +export class MasterKeyValueEncoding implements IDatabaseEncoding { + serialize(value: MasterKeyValue): Buffer { + const bw = bufio.write(this.getSize()) + bw.writeBytes(value.nonce) + bw.writeBytes(value.salt) return bw.render() } - deserialize(buffer: Buffer): MasterKeyValue | null { + deserialize(buffer: Buffer): MasterKeyValue { const reader = bufio.read(buffer, true) - if (reader.left()) { - const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) - const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) - return { nonce, salt } - } - - return null + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + return { nonce, salt } } - getSize(value: MasterKeyValue | null): number { - if (!value) { - return 0 - } - + getSize(): number { return xchacha20poly1305.XNONCE_LENGTH + xchacha20poly1305.XSALT_LENGTH } } diff --git a/ironfish/src/wallet/walletdb/walletdb.test.ts b/ironfish/src/wallet/walletdb/walletdb.test.ts index 03d4e36e75..e1d12e56d2 100644 --- a/ironfish/src/wallet/walletdb/walletdb.test.ts +++ b/ironfish/src/wallet/walletdb/walletdb.test.ts @@ -14,6 +14,7 @@ import { AsyncUtils } from '../../utils' import { Account } from '../account/account' import { EncryptedAccount } from '../account/encryptedAccount' import { AccountDecryptionFailedError } from '../errors' +import { MasterKey } from '../masterKey' import { DecryptedAccountValue } from './accountValue' import { DecryptedNoteValue } from './decryptedNoteValue' @@ -478,20 +479,22 @@ describe('WalletDB', () => { throw new Error('Unexpected behavior') } - encryptedAccountById.set( - id, - new EncryptedAccount({ data: accountValue.data, walletDb }), - ) + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) } + const masterKeyValue = await walletDb.loadMasterKey() + Assert.isNotNull(masterKeyValue) + const masterKey = new MasterKey(masterKeyValue) + const key = await masterKey.unlock(passphrase) + const encryptedAccountA = encryptedAccountById.get(accountA.id) Assert.isNotUndefined(encryptedAccountA) - const decryptedAccountA = encryptedAccountA.decrypt(passphrase) + const decryptedAccountA = encryptedAccountA.decrypt(key) expect(accountA.serialize()).toMatchObject(decryptedAccountA.serialize()) const encryptedAccountB = encryptedAccountById.get(accountB.id) Assert.isNotUndefined(encryptedAccountB) - const decryptedAccountB = encryptedAccountB.decrypt(passphrase) + const decryptedAccountB = encryptedAccountB.decrypt(key) expect(accountB.serialize()).toMatchObject(decryptedAccountB.serialize()) }) }) @@ -512,10 +515,7 @@ describe('WalletDB', () => { throw new Error('Unexpected behavior') } - encryptedAccountById.set( - id, - new EncryptedAccount({ data: accountValue.data, walletDb }), - ) + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) } const encryptedAccountA = encryptedAccountById.get(accountA.id) @@ -560,10 +560,7 @@ describe('WalletDB', () => { throw new Error('Unexpected behavior') } - encryptedAccountById.set( - id, - new EncryptedAccount({ data: accountValue.data, walletDb }), - ) + encryptedAccountById.set(id, new EncryptedAccount({ accountValue, walletDb })) } const encryptedAccountA = encryptedAccountById.get(accountA.id) @@ -586,7 +583,10 @@ describe('WalletDB', () => { await useAccountFixture(node.wallet, 'A') const accountB = await useAccountFixture(node.wallet, 'B') - await walletDb.accounts.put(accountB.id, accountB.encrypt(passphrase).serialize()) + const masterKey = MasterKey.generate(passphrase) + await masterKey.unlock(passphrase) + + await walletDb.accounts.put(accountB.id, accountB.encrypt(masterKey).serialize()) await expect(walletDb.accountsEncrypted()).rejects.toThrow() }) @@ -675,6 +675,7 @@ describe('WalletDB', () => { const node = (await nodeTest.createSetup()).node const walletDb = node.wallet.walletDb const passphrase = 'foobar' + const masterKey = MasterKey.generate(passphrase) await useAccountFixture(node.wallet, 'A') @@ -690,7 +691,7 @@ describe('WalletDB', () => { } const account = new Account({ accountValue, walletDb }) - await expect(walletDb.setEncryptedAccount(account, passphrase)).rejects.toThrow() + await expect(walletDb.setEncryptedAccount(account, masterKey)).rejects.toThrow() }) it('saves the account', async () => { @@ -713,7 +714,12 @@ describe('WalletDB', () => { } const account = new Account({ accountValue, walletDb }) - await walletDb.setEncryptedAccount(account, passphrase) + const masterKeyValue = await walletDb.loadMasterKey() + Assert.isNotNull(masterKeyValue) + const masterKey = new MasterKey(masterKeyValue) + await masterKey.unlock(passphrase) + + await walletDb.setEncryptedAccount(account, masterKey) expect(await walletDb.accounts.get(account.id)).not.toBeUndefined() expect( @@ -721,37 +727,4 @@ describe('WalletDB', () => { ).not.toBeUndefined() }) }) - - describe('canDecryptAccounts', () => { - it('throws an error if the accounts are decrypted', async () => { - const node = (await nodeTest.createSetup()).node - const walletDb = node.wallet.walletDb - - await useAccountFixture(node.wallet, 'A') - - await expect(walletDb.canDecryptAccounts('invalid')).rejects.toThrow() - }) - - it('returns false if the passphrase is invalid', async () => { - const node = (await nodeTest.createSetup()).node - const walletDb = node.wallet.walletDb - const passphrase = 'foobar' - - await useAccountFixture(node.wallet, 'A') - await walletDb.encryptAccounts(passphrase) - - expect(await walletDb.canDecryptAccounts('invalid')).toBe(false) - }) - - it('returns true if the passphrase is valid', async () => { - const node = (await nodeTest.createSetup()).node - const walletDb = node.wallet.walletDb - const passphrase = 'foobar' - - await useAccountFixture(node.wallet, 'A') - await walletDb.encryptAccounts(passphrase) - - expect(await walletDb.canDecryptAccounts(passphrase)).toBe(true) - }) - }) }) diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 70b7a913ac..b9477924a3 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -33,12 +33,13 @@ import { BloomFilter } from '../../utils/bloomFilter' import { WorkerPool } from '../../workerPool' import { Account, calculateAccountPrefix } from '../account/account' import { EncryptedAccount } from '../account/encryptedAccount' -import { AccountDecryptionFailedError } from '../errors' +import { MasterKey } from '../masterKey' import { AccountValue, AccountValueEncoding } from './accountValue' import { AssetValue, AssetValueEncoding } from './assetValue' import { BalanceValue, BalanceValueEncoding } from './balanceValue' import { DecryptedNoteValue, DecryptedNoteValueEncoding } from './decryptedNoteValue' import { HeadValue, NullableHeadValueEncoding } from './headValue' +import { MasterKeyValue, MasterKeyValueEncoding } from './masterKeyValue' import { AccountsDBMeta, MetaValue, MetaValueEncoding } from './metaValue' import { MultisigIdentityValue, MultisigIdentityValueEncoder } from './multisigIdentityValue' import { ParticipantIdentity, ParticipantIdentityEncoding } from './participantIdentity' @@ -148,6 +149,11 @@ export class WalletDB { value: ParticipantIdentity }> + masterKey: IDatabaseStore<{ + key: 'key' + value: MasterKeyValue + }> + cacheStores: Array> nullifierBloomFilter: BloomFilter | null = null @@ -314,6 +320,12 @@ export class WalletDB { valueEncoding: new ParticipantIdentityEncoding(), }) + this.masterKey = this.db.addStore({ + name: 'mk', + keyEncoding: new StringEncoding<'key'>(), + valueEncoding: new MasterKeyValueEncoding(), + }) + // IDatabaseStores that cache and index decrypted chain data this.cacheStores = [ this.decryptedNotes, @@ -370,7 +382,7 @@ export class WalletDB { async setEncryptedAccount( account: Account, - passphrase: string, + masterKey: MasterKey, tx?: IDatabaseTransaction, ): Promise { return this.db.withTransaction(tx, async (tx) => { @@ -379,10 +391,7 @@ export class WalletDB { throw new Error('Cannot save encrypted account when accounts are decrypted') } - const validPassphrase = await this.canDecryptAccounts(passphrase, tx) - Assert.isTrue(validPassphrase, 'Your passphrase is incorrect') - - const encryptedAccount = account.encrypt(passphrase) + const encryptedAccount = account.encrypt(masterKey) await this.accounts.put(account.id, encryptedAccount.serialize(), tx) const nativeUnconfirmedBalance = await this.balances.get( @@ -406,33 +415,6 @@ export class WalletDB { }) } - async canDecryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { - return this.db.withTransaction(tx, async (tx) => { - for await (const [_, accountValue] of this.accounts.getAllIter(tx)) { - if (!accountValue.encrypted) { - throw new Error('Wallet is already decrypted') - } - - const encryptedAccount = new EncryptedAccount({ - data: accountValue.data, - walletDb: this, - }) - - try { - encryptedAccount.decrypt(passphrase) - } catch (e) { - if (e instanceof AccountDecryptionFailedError) { - return false - } - - throw e - } - } - - return true - }) - } - async removeAccount(account: Account, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { await this.accounts.del(account.id, tx) @@ -462,6 +444,18 @@ export class WalletDB { return meta } + async loadMasterKey(tx?: IDatabaseTransaction): Promise { + const record = await this.masterKey.get('key', tx) + return record ?? null + } + + async saveMasterKey(masterKey: MasterKey, tx?: IDatabaseTransaction): Promise { + const record = await this.loadMasterKey(tx) + Assert.isNull(record) + + await this.masterKey.put('key', { nonce: masterKey.nonce, salt: masterKey.salt }, tx) + } + async *loadAccounts( tx?: IDatabaseTransaction, ): AsyncGenerator<[string, AccountValue], void, unknown> { @@ -1261,32 +1255,51 @@ export class WalletDB { async encryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { + const record = await this.loadMasterKey(tx) + Assert.isNull(record) + + const masterKey = MasterKey.generate(passphrase) + await this.saveMasterKey(masterKey, tx) + + await masterKey.unlock(passphrase) + for await (const [id, accountValue] of this.accounts.getAllIter(tx)) { if (accountValue.encrypted) { throw new Error('Wallet is already encrypted') } const account = new Account({ accountValue, walletDb: this }) - const encryptedAccount = account.encrypt(passphrase) + const encryptedAccount = account.encrypt(masterKey) await this.accounts.put(id, encryptedAccount.serialize(), tx) } + + await masterKey.destroy() }) } async decryptAccounts(passphrase: string, tx?: IDatabaseTransaction): Promise { await this.db.withTransaction(tx, async (tx) => { + const masterKeyValue = await this.loadMasterKey(tx) + Assert.isNotNull(masterKeyValue) + + const masterKey = new MasterKey(masterKeyValue) + const key = await masterKey.unlock(passphrase) + for await (const [id, accountValue] of this.accounts.getAllIter(tx)) { if (!accountValue.encrypted) { throw new Error('Wallet is already decrypted') } const encryptedAccount = new EncryptedAccount({ - data: accountValue.data, + accountValue, walletDb: this, }) - const account = encryptedAccount.decrypt(passphrase) + const account = encryptedAccount.decrypt(key) await this.accounts.put(id, account.serialize(), tx) } + + await masterKey.destroy() + await this.masterKey.del('key', tx) }) }