Skip to content

Commit

Permalink
feat(ironfish,rust-nodejs): Add EncryptedAccount class (#5226)
Browse files Browse the repository at this point in the history
* feat(ironfish,rust-nodejs): Add EncryptedAccount class

* chore(rust-nodejs): lint rust

* chore(rust-nodejs): cargo clippy fix

* feat(ironfish): Add test for invalid passphrase

* feat(ironfish): Add error type for failed decryption
  • Loading branch information
rohanjadvani authored Aug 8, 2024
1 parent 0a465c9 commit de0bdda
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 36 deletions.
4 changes: 2 additions & 2 deletions ironfish-rust-nodejs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ export const TRANSACTION_EXPIRATION_LENGTH: number
export const TRANSACTION_FEE_LENGTH: number
export const LATEST_TRANSACTION_VERSION: number
export function verifyTransactions(serializedTransactions: Array<Buffer>): boolean
export function encrypt(plaintext: string, passphrase: string): string
export function decrypt(encryptedBlob: string, passphrase: string): string
export function encrypt(plaintext: Buffer, passphrase: string): Buffer
export function decrypt(encryptedBlob: Buffer, passphrase: string): Buffer
export const enum LanguageCode {
English = 0,
ChineseSimplified = 1,
Expand Down
29 changes: 12 additions & 17 deletions ironfish-rust-nodejs/src/xchacha20poly1305.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,31 @@
* 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/. */

use ironfish::{
serializing::{bytes_to_hex, hex_to_vec_bytes},
xchacha20poly1305::{self, EncryptOutput},
};
use napi::bindgen_prelude::*;
use ironfish::xchacha20poly1305::{self, EncryptOutput};
use napi::{bindgen_prelude::*, JsBuffer};
use napi_derive::napi;

use crate::to_napi_err;

#[napi]
pub fn encrypt(plaintext: String, passphrase: String) -> Result<String> {
let plaintext_bytes = hex_to_vec_bytes(&plaintext).map_err(to_napi_err)?;
let passphrase_bytes = hex_to_vec_bytes(&passphrase).map_err(to_napi_err)?;
let result =
xchacha20poly1305::encrypt(&plaintext_bytes, &passphrase_bytes).map_err(to_napi_err)?;
pub fn encrypt(plaintext: JsBuffer, passphrase: String) -> Result<Buffer> {
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<u8> = vec![];
result.write(&mut vec).map_err(to_napi_err)?;

Ok(bytes_to_hex(&vec))
Ok(Buffer::from(&vec[..]))
}

#[napi]
pub fn decrypt(encrypted_blob: String, passphrase: String) -> Result<String> {
let encrypted_blob_bytes = hex_to_vec_bytes(&encrypted_blob).map_err(to_napi_err)?;
let passphrase_bytes = hex_to_vec_bytes(&passphrase).map_err(to_napi_err)?;
pub fn decrypt(encrypted_blob: JsBuffer, passphrase: String) -> Result<Buffer> {
let encrypted_bytes = encrypted_blob.into_value()?;

let encrypted_output = EncryptOutput::read(&encrypted_blob_bytes[..]).map_err(to_napi_err)?;
let encrypted_output = EncryptOutput::read(encrypted_bytes.as_ref()).map_err(to_napi_err)?;
let result =
xchacha20poly1305::decrypt(encrypted_output, &passphrase_bytes).map_err(to_napi_err)?;
xchacha20poly1305::decrypt(encrypted_output, passphrase.as_bytes()).map_err(to_napi_err)?;

Ok(bytes_to_hex(&result[..]))
Ok(Buffer::from(&result[..]))
}
7 changes: 4 additions & 3 deletions ironfish/src/testUtilities/fixtures/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
* 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 { Blockchain } from '../../blockchain'
import { AccountValue, AssertSpending, SpendingAccount, Wallet } from '../../wallet'
import { AssertSpending, SpendingAccount, Wallet } from '../../wallet'
import { DecryptedAccountValue } from '../../wallet/walletdb/accountValue'
import { HeadValue } from '../../wallet/walletdb/headValue'
import { useMinerBlockFixture } from './blocks'
import { FixtureGenerate, useFixture } from './fixture'
Expand All @@ -26,7 +27,7 @@ export function useAccountFixture(
serialize: async (
account: SpendingAccount,
): Promise<{
value: AccountValue
value: DecryptedAccountValue
head: HeadValue | null
}> => {
return {
Expand All @@ -39,7 +40,7 @@ export function useAccountFixture(
value,
head,
}: {
value: AccountValue
value: DecryptedAccountValue
head: HeadValue | null
}): Promise<SpendingAccount> => {
const createdAt = value.createdAt
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"EncryptedAccount can decrypt an encrypted account": [
{
"value": {
"encrypted": false,
"version": 4,
"id": "0332daf1-4ba3-42b3-9d72-6803529df295",
"name": "test",
"spendingKey": "f7bff5aabef137c1231d16fdaee2c0b7da61e81cebd08e8be35d0cd0dea1f015",
"viewKey": "c8ee06884cc6aa31002c058a511aeea219059579350cbe6deb07416ad049e7cfef7877b730dd1e4d982d5228e8badf0c7020b1d07ff0eb23b298d9c97b0d5e1c",
"incomingViewKey": "d9753f22ba723bdc381cd3dba675ef36247a6807e5c0d082f88948496f687601",
"outgoingViewKey": "03268cd3770209e043742554a83f6944a768980309ba2beec2bc8995fe446130",
"publicAddress": "7742fa58b9e1efc337b05d54dea06d1dabd89c78cd98c7656f667314da865d04",
"createdAt": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
},
"scanningEnabled": true,
"proofAuthorizingKey": "67e962ba0d98e71f351534ad8d9bb8cfe01c76f291b06933b694b57b7808c10a"
},
"head": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
}
}
],
"EncryptedAccount throws an error when an invalid passphrase is used": [
{
"value": {
"encrypted": false,
"version": 4,
"id": "2710f42e-f848-4d67-b0a2-9e4eac56df7d",
"name": "test",
"spendingKey": "eb527038b1b452c2abb13c1c7135309d6acd9e4737a7f18aa75bb4363141e077",
"viewKey": "b05272abd89b4eb48435b336d638771e69854fd36947971d22b3bd9cde7a2a5c0d83d88ccc8ef8259693c1391eedc1a25e5fccdf554ff605b593c15a619aa851",
"incomingViewKey": "fd48d3ddf807a6d1a881af440f2b3b23fc670b70323ecbb08f8dd54ff9220401",
"outgoingViewKey": "62603f4717d3a7f7ab00887ad77c1567b103f06fc885aa4a000625cf4b278f81",
"publicAddress": "40fd3b67d0abeacda175781f1bb97d5a08dff7b3850cc94ce0b3c3dfee742a41",
"createdAt": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
},
"scanningEnabled": true,
"proofAuthorizingKey": "c0159378c279f74958da06b63f3db79a85e84abf4919b1c9b321392de5be9a06"
},
"head": {
"hash": {
"type": "Buffer",
"data": "base64:R5HXrp+X3xAO8VWOhHctagm0N2I4goP3XG8goyqIqoY="
},
"sequence": 1
}
}
]
}
13 changes: 10 additions & 3 deletions ironfish/src/wallet/account/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { WithNonNull, WithRequired } from '../../utils'
import { DecryptedNote } from '../../workerPool/tasks/decryptNotes'
import { AssetBalances } from '../assetBalances'
import { MultisigKeys, MultisigSigner } from '../interfaces/multisigKeys'
import { AccountValue } from '../walletdb/accountValue'
import { DecryptedAccountValue } from '../walletdb/accountValue'
import { AssetValue } from '../walletdb/assetValue'
import { BalanceValue } from '../walletdb/balanceValue'
import { DecryptedNoteValue } from '../walletdb/decryptedNoteValue'
Expand Down Expand Up @@ -76,7 +76,13 @@ export class Account {
readonly multisigKeys?: MultisigKeys
readonly proofAuthorizingKey: string | null

constructor({ accountValue, walletDb }: { accountValue: AccountValue; walletDb: WalletDB }) {
constructor({
accountValue,
walletDb,
}: {
accountValue: DecryptedAccountValue
walletDb: WalletDB
}) {
this.id = accountValue.id
this.name = accountValue.name
this.spendingKey = accountValue.spendingKey
Expand All @@ -102,8 +108,9 @@ export class Account {
return this.spendingKey !== null
}

serialize(): AccountValue {
serialize(): DecryptedAccountValue {
return {
encrypted: false,
version: this.version,
id: this.id,
name: this.name,
Expand Down
52 changes: 52 additions & 0 deletions ironfish/src/wallet/account/encryptedAccount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* 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 } from '@ironfish/rust-nodejs'
import { useAccountFixture } from '../../testUtilities/fixtures/account'
import { createNodeTest } from '../../testUtilities/nodeTest'
import { AccountDecryptionFailedError } from '../errors'
import { AccountValueEncoding } from '../walletdb/accountValue'
import { EncryptedAccount } from './encryptedAccount'

describe('EncryptedAccount', () => {
const nodeTest = createNodeTest()

it('can decrypt an encrypted account', async () => {
const passphrase = 'foobarbaz'
const { node } = nodeTest
const account = await useAccountFixture(node.wallet)

const encoder = new AccountValueEncoding()
const data = encoder.serialize(account.serialize())

const encryptedData = encrypt(data, passphrase)
const encryptedAccount = new EncryptedAccount({
data: encryptedData,
walletDb: node.wallet.walletDb,
})

const decryptedAccount = encryptedAccount.decrypt(passphrase)
const decryptedData = encoder.serialize(decryptedAccount.serialize())
expect(data.toString('hex')).toEqual(decryptedData.toString('hex'))
})

it('throws an error when an invalid passphrase is used', async () => {
const passphrase = 'foobarbaz'
const invalidPassphrase = 'fakepassphrase'
const { node } = nodeTest
const account = await useAccountFixture(node.wallet)

const encoder = new AccountValueEncoding()
const data = encoder.serialize(account.serialize())

const encryptedData = encrypt(data, passphrase)
const encryptedAccount = new EncryptedAccount({
data: encryptedData,
walletDb: node.wallet.walletDb,
})

expect(() => encryptedAccount.decrypt(invalidPassphrase)).toThrow(
AccountDecryptionFailedError,
)
})
})
37 changes: 37 additions & 0 deletions ironfish/src/wallet/account/encryptedAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* 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 } from '@ironfish/rust-nodejs'
import { AccountDecryptionFailedError } from '../errors'
import { AccountValueEncoding, EncryptedAccountValue } from '../walletdb/accountValue'
import { WalletDB } from '../walletdb/walletdb'
import { Account } from './account'

export class EncryptedAccount {
private readonly walletDb: WalletDB
readonly data: Buffer

constructor({ data, walletDb }: { data: Buffer; walletDb: WalletDB }) {
this.data = data
this.walletDb = walletDb
}

decrypt(passphrase: string): Account {
try {
const decryptedAccountValue = decrypt(this.data, passphrase)
const encoder = new AccountValueEncoding()
const accountValue = encoder.deserialize(decryptedAccountValue)

return new Account({ accountValue, walletDb: this.walletDb })
} catch {
throw new AccountDecryptionFailedError()
}
}

serialize(): EncryptedAccountValue {
return {
encrypted: true,
data: this.data,
}
}
}
9 changes: 9 additions & 0 deletions ironfish/src/wallet/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,12 @@ export class DuplicateMultisigSecretNameError extends Error {
this.message = `Multisig secret already exists with the name ${name}`
}
}

export class AccountDecryptionFailedError extends Error {
name = this.constructor.name

constructor() {
super()
this.message = 'Failed to decrypt account'
}
}
3 changes: 3 additions & 0 deletions ironfish/src/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,7 @@ export class Wallet {

const account = new Account({
accountValue: {
encrypted: false,
version: ACCOUNT_SCHEMA_VERSION,
id: uuid(),
name,
Expand Down Expand Up @@ -1389,6 +1390,7 @@ export class Wallet {
name,
multisigKeys,
scanningEnabled: true,
encrypted: false,
},
walletDb: this.walletDb,
})
Expand Down Expand Up @@ -1441,6 +1443,7 @@ export class Wallet {
createdAt: options?.resetCreatedAt ? null : account.createdAt,
scanningEnabled: options?.resetScanningEnabled ? true : account.scanningEnabled,
id: uuid(),
encrypted: false,
},
walletDb: this.walletDb,
})
Expand Down
8 changes: 5 additions & 3 deletions ironfish/src/wallet/walletdb/accountValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
* 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 { generateKey } from '@ironfish/rust-nodejs'
import { AccountValue, AccountValueEncoding } from './accountValue'
import { AccountValueEncoding, DecryptedAccountValue } from './accountValue'

describe('AccountValueEncoding', () => {
it('serializes the object into a buffer and deserializes to the original object', () => {
const encoder = new AccountValueEncoding()

const key = generateKey()
const value: AccountValue = {
const value: DecryptedAccountValue = {
encrypted: false,
id: 'id',
name: 'foobar👁️🏃🐟',
incomingViewKey: key.incomingViewKey,
Expand All @@ -34,7 +35,8 @@ describe('AccountValueEncoding', () => {
const encoder = new AccountValueEncoding()

const key = generateKey()
const value: AccountValue = {
const value: DecryptedAccountValue = {
encrypted: false,
id: 'id',
name: 'foobar👁️🏃🐟',
incomingViewKey: key.incomingViewKey,
Expand Down
19 changes: 14 additions & 5 deletions ironfish/src/wallet/walletdb/accountValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export const KEY_LENGTH = ACCOUNT_KEY_LENGTH
export const VIEW_KEY_LENGTH = 64
const VERSION_LENGTH = 2

export interface AccountValue {
export interface EncryptedAccountValue {
encrypted: true
data: Buffer
}

export interface DecryptedAccountValue {
encrypted: false
version: number
id: string
name: string
Expand All @@ -28,8 +34,10 @@ export interface AccountValue {
proofAuthorizingKey: string | null
}

export class AccountValueEncoding implements IDatabaseEncoding<AccountValue> {
serialize(value: AccountValue): Buffer {
export type AccountValue = EncryptedAccountValue | DecryptedAccountValue

export class AccountValueEncoding implements IDatabaseEncoding<DecryptedAccountValue> {
serialize(value: DecryptedAccountValue): Buffer {
const bw = bufio.write(this.getSize(value))
let flags = 0
flags |= Number(!!value.spendingKey) << 0
Expand Down Expand Up @@ -69,7 +77,7 @@ export class AccountValueEncoding implements IDatabaseEncoding<AccountValue> {
return bw.render()
}

deserialize(buffer: Buffer): AccountValue {
deserialize(buffer: Buffer): DecryptedAccountValue {
const reader = bufio.read(buffer, true)
const flags = reader.readU8()
const version = reader.readU16()
Expand Down Expand Up @@ -104,6 +112,7 @@ export class AccountValueEncoding implements IDatabaseEncoding<AccountValue> {
: null

return {
encrypted: false,
version,
id,
name,
Expand All @@ -119,7 +128,7 @@ export class AccountValueEncoding implements IDatabaseEncoding<AccountValue> {
}
}

getSize(value: AccountValue): number {
getSize(value: DecryptedAccountValue): number {
let size = 0
size += 1 // flags
size += VERSION_LENGTH
Expand Down
Loading

0 comments on commit de0bdda

Please sign in to comment.