From f168064808cf63394d94adaaf742259dfbd76830 Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 26 Nov 2024 11:00:14 -0800 Subject: [PATCH] adds template for migrations on encrypted wallets (#5655) * supports migrations on encrypted wallets adds a template migration , 033-encrypted-wallet-template, to demonstrate how to write wallet migrations on wallet databases that may be encrypted now that account data may be encrypted we need to be careful to decrypt the account before running the migration and to re-encrypt the migrated data before writing it back to the database adds EncryptedWalletMigrationError thrown when a migration tries to access an encrypted account without the wallet passphrase to allow client code (e.g., 'migrations:start' command) to handle decryption flow optionally passes wallet passphrase through migrator to an individual migration to decrypt/encrypt account data in migrations updates 'start' command to catch EncryptedWalletMigrationError and prompt for wallet passphrase. passes wallet passphrase through node openDB flow to suppoprt running migrator with wallet passphrase NOTE: this example assumes that the schema for the masterKey store does not change and assumes that the schema for encrypted account data does not change. if the masterKey schema changes then that change must be addressed in a separate migration. if the encrypted account schema changes, then that change should be addressed separately from any change to the decrypted account schema in a separate migration. * removes superfluous comments * updates template migration for changes to MasterKey removes util from migration data * converts encrypted-wallet-template to template renames 033-encrypted-wallet-template to 000-encrypted-wallet-template so that the migration is not runnable resets walletDb version to 32 --- .../data/000-encrypted-wallet-template.ts | 165 +++++++++++++ .../new/accountValue.ts | 229 ++++++++++++++++++ .../new/headValue.ts | 41 ++++ .../new/index.ts | 20 ++ .../new/multisigKeys.ts | 92 +++++++ .../old/accountValue.ts | 229 ++++++++++++++++++ .../old/headValue.ts | 41 ++++ .../old/index.ts | 31 +++ .../old/masterKeyValue.ts | 32 +++ .../old/multisigKeys.ts | 92 +++++++ .../000-encrypted-wallet-template/stores.ts | 16 ++ 11 files changed, 988 insertions(+) create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/new/accountValue.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/new/headValue.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/new/index.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/new/multisigKeys.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/old/accountValue.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/old/headValue.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/old/index.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/old/masterKeyValue.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/old/multisigKeys.ts create mode 100644 ironfish/src/migrations/data/000-encrypted-wallet-template/stores.ts diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template.ts new file mode 100644 index 0000000000..233b604698 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template.ts @@ -0,0 +1,165 @@ +/* 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 { Assert } from '../../assert' +import { Logger } from '../../logger' +import { IDatabase, IDatabaseTransaction } from '../../storage' +import { createDB } from '../../storage/utils' +import { MasterKey } from '../../wallet/masterKey' +import { EncryptedWalletMigrationError } from '../errors' +import { Database, Migration, MigrationContext } from '../migration' +import { + AccountValueEncoding as NewAccountValueEncoding, + DecryptedAccountValue as NewDecryptedAccountValue, +} from './000-encrypted-wallet-template/new/accountValue' +import { + AccountValueEncoding as OldAccountValueEncoding, + DecryptedAccountValue as OldDecryptedAccountValue, + EncryptedAccountValue as OldEncryptedAccountValue, +} from './000-encrypted-wallet-template/old/accountValue' +import { GetStores } from './000-encrypted-wallet-template/stores' + +export class Migration000 extends Migration { + path = __filename + database = Database.WALLET + + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.walletDatabasePath }) + } + + async forward( + context: MigrationContext, + db: IDatabase, + tx: IDatabaseTransaction | undefined, + logger: Logger, + dryRun: boolean, + walletPassphrase: string | undefined, + ): Promise { + const stores = GetStores(db) + const oldEncoding = new OldAccountValueEncoding() + const newEncoding = new NewAccountValueEncoding() + + for await (const account of stores.old.accounts.getAllValuesIter(tx)) { + let decryptedAccount + + // Check if the account is encrypted, and throw an error to allow client + // code to prompt for passphrase. + if (account.encrypted) { + if (!walletPassphrase) { + throw new EncryptedWalletMigrationError('Cannot run migration on encrypted wallet') + } + + const masterKeyValue = await stores.old.masterKey.get('key') + Assert.isNotUndefined(masterKeyValue) + + const masterKey = new MasterKey(masterKeyValue) + await masterKey.unlock(walletPassphrase) + + // Decrypt encrypted account data + const decrypted = masterKey.decrypt(account.data, account.salt, account.nonce) + decryptedAccount = oldEncoding.deserializeDecrypted(decrypted) + + // Apply migration to decrypted account data + logger.info(` Migrating account ${decryptedAccount.name}`) + const migrated = this.accountForward(decryptedAccount) + + // Re-encrypt the migrated data and write it to the store. + // Assumes that schema for encrypted accounts has NOT changed. + const migratedSerialized = newEncoding.serialize(migrated) + const { ciphertext: data, salt, nonce } = masterKey.encrypt(migratedSerialized) + + const encryptedAccount: OldEncryptedAccountValue = { + encrypted: true, + salt, + nonce, + data, + } + + await stores.new.accounts.put(decryptedAccount.id, encryptedAccount, tx) + } else { + decryptedAccount = account + + logger.info(` Migrating account ${decryptedAccount.name}`) + const migrated = this.accountForward(decryptedAccount) + + await stores.new.accounts.put(decryptedAccount.id, migrated, tx) + } + } + } + + // Implement logic to migrate (decrypted) account data to the new schema + accountForward(oldValue: OldDecryptedAccountValue): NewDecryptedAccountValue { + const newValue = oldValue + return newValue + } + + /** + * Writing a backwards migration is optional but suggested + */ + async backward( + context: MigrationContext, + db: IDatabase, + tx: IDatabaseTransaction | undefined, + logger: Logger, + dryRun: boolean, + walletPassphrase: string | undefined, + ): Promise { + const stores = GetStores(db) + const oldEncoding = new OldAccountValueEncoding() + const newEncoding = new NewAccountValueEncoding() + + for await (const account of stores.new.accounts.getAllValuesIter(tx)) { + let decryptedAccount + + // Check if the account is encrypted, and throw an error to allow client + // code to prompt for passphrase. + if (account.encrypted) { + if (!walletPassphrase) { + throw new EncryptedWalletMigrationError('Cannot run migration on encrypted wallet') + } + + // Load master key from database + const masterKeyValue = await stores.old.masterKey.get('key') + Assert.isNotUndefined(masterKeyValue) + + const masterKey = new MasterKey(masterKeyValue) + await masterKey.unlock(walletPassphrase) + + // Decrypt encrypted account data + const decrypted = masterKey.decrypt(account.data, account.salt, account.nonce) + decryptedAccount = newEncoding.deserializeDecrypted(decrypted) + + // Apply migration to decrypted account data + logger.info(` Migrating account ${decryptedAccount.name}`) + const migrated = this.accountBackward(decryptedAccount) + + // Re-encrypt the migrated data and write it to the store. + // Assumes that schema for encrypted accounts has NOT changed. + const migratedSerialized = oldEncoding.serialize(migrated) + const { ciphertext: data, salt, nonce } = masterKey.encrypt(migratedSerialized) + + const encryptedAccount: OldEncryptedAccountValue = { + encrypted: true, + salt, + nonce, + data, + } + + await stores.old.accounts.put(decryptedAccount.id, encryptedAccount, tx) + } else { + decryptedAccount = account + + logger.info(` Migrating account ${decryptedAccount.name}`) + const migrated = this.accountBackward(decryptedAccount) + + await stores.old.accounts.put(decryptedAccount.id, migrated, tx) + } + } + } + + // Implement logic to rever (decrypted) account data to the old schema + accountBackward(newValue: NewDecryptedAccountValue): OldDecryptedAccountValue { + const oldValue = newValue + return oldValue + } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/new/accountValue.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/accountValue.ts new file mode 100644 index 0000000000..65b1854e73 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/accountValue.ts @@ -0,0 +1,229 @@ +/* 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 { KEY_LENGTH, PUBLIC_ADDRESS_LENGTH, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' +import { MultisigKeys } from '../../../../wallet/interfaces/multisigKeys' +import { HeadValue, NullableHeadValueEncoding } from './headValue' +import { MultisigKeysEncoding } from './multisigKeys' + +export const VIEW_KEY_LENGTH = 64 +const VERSION_LENGTH = 2 + +export type EncryptedAccountValue = { + encrypted: true + salt: Buffer + nonce: Buffer + data: Buffer +} + +export type DecryptedAccountValue = { + encrypted: false + version: number + id: string + name: string + spendingKey: string | null + viewKey: string + incomingViewKey: string + outgoingViewKey: string + publicAddress: string + createdAt: HeadValue | null + scanningEnabled: boolean + multisigKeys?: MultisigKeys + proofAuthorizingKey: string | null +} + +export type AccountValue = EncryptedAccountValue | DecryptedAccountValue +export class AccountValueEncoding implements IDatabaseEncoding { + serialize(value: AccountValue): Buffer { + if (value.encrypted) { + return this.serializeEncrypted(value) + } else { + return this.serializeDecrypted(value) + } + } + + serializeEncrypted(value: EncryptedAccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + + let flags = 0 + flags |= Number(!!value.encrypted) << 5 + bw.writeU8(flags) + bw.writeBytes(value.salt) + bw.writeBytes(value.nonce) + bw.writeVarBytes(value.data) + + return bw.render() + } + + serializeDecrypted(value: DecryptedAccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + let flags = 0 + flags |= Number(!!value.spendingKey) << 0 + flags |= Number(!!value.createdAt) << 1 + flags |= Number(!!value.multisigKeys) << 2 + flags |= Number(!!value.proofAuthorizingKey) << 3 + flags |= Number(!!value.scanningEnabled) << 4 + flags |= Number(!!value.encrypted) << 5 + + bw.writeU8(flags) + bw.writeU16(value.version) + bw.writeVarString(value.id, 'utf8') + bw.writeVarString(value.name, 'utf8') + + if (value.spendingKey) { + bw.writeBytes(Buffer.from(value.spendingKey, 'hex')) + } + + bw.writeBytes(Buffer.from(value.viewKey, 'hex')) + bw.writeBytes(Buffer.from(value.incomingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.outgoingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.publicAddress, 'hex')) + + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + bw.writeBytes(encoding.serialize(value.createdAt)) + } + + if (value.multisigKeys) { + const encoding = new MultisigKeysEncoding() + bw.writeU64(encoding.getSize(value.multisigKeys)) + bw.writeBytes(encoding.serialize(value.multisigKeys)) + } + + if (value.proofAuthorizingKey) { + bw.writeBytes(Buffer.from(value.proofAuthorizingKey, 'hex')) + } + + return bw.render() + } + + deserialize(buffer: Buffer): AccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const encrypted = Boolean(flags & (1 << 5)) + + if (encrypted) { + return this.deserializeEncrypted(buffer) + } else { + return this.deserializeDecrypted(buffer) + } + } + + deserializeEncrypted(buffer: Buffer): EncryptedAccountValue { + const reader = bufio.read(buffer, true) + + // Skip flags + reader.readU8() + + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const data = reader.readVarBytes() + return { + encrypted: true, + nonce, + salt, + data, + } + } + + deserializeDecrypted(buffer: Buffer): DecryptedAccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const version = reader.readU16() + const hasSpendingKey = flags & (1 << 0) + const hasCreatedAt = flags & (1 << 1) + const hasMultisigKeys = flags & (1 << 2) + const hasProofAuthorizingKey = flags & (1 << 3) + const scanningEnabled = Boolean(flags & (1 << 4)) + const id = reader.readVarString('utf8') + const name = reader.readVarString('utf8') + const spendingKey = hasSpendingKey ? reader.readBytes(KEY_LENGTH).toString('hex') : null + const viewKey = reader.readBytes(VIEW_KEY_LENGTH).toString('hex') + const incomingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const outgoingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const publicAddress = reader.readBytes(PUBLIC_ADDRESS_LENGTH).toString('hex') + + let createdAt = null + if (hasCreatedAt) { + const encoding = new NullableHeadValueEncoding() + createdAt = encoding.deserialize(reader.readBytes(encoding.nonNullSize)) + } + + let multisigKeys = undefined + if (hasMultisigKeys) { + const multisigKeysLength = reader.readU64() + const encoding = new MultisigKeysEncoding() + multisigKeys = encoding.deserialize(reader.readBytes(multisigKeysLength)) + } + + const proofAuthorizingKey = hasProofAuthorizingKey + ? reader.readBytes(KEY_LENGTH).toString('hex') + : null + + return { + encrypted: false, + version, + id, + name, + viewKey, + incomingViewKey, + outgoingViewKey, + spendingKey, + publicAddress, + createdAt, + scanningEnabled, + multisigKeys, + proofAuthorizingKey, + } + } + + getSize(value: AccountValue): number { + if (value.encrypted) { + return this.getSizeEncrypted(value) + } else { + return this.getSizeDecrypted(value) + } + } + + getSizeEncrypted(value: EncryptedAccountValue): number { + let size = 0 + size += 1 // flags + size += xchacha20poly1305.XSALT_LENGTH + size += xchacha20poly1305.XNONCE_LENGTH + size += bufio.sizeVarBytes(value.data) + return size + } + + getSizeDecrypted(value: DecryptedAccountValue): number { + let size = 0 + size += 1 // flags + size += VERSION_LENGTH + size += bufio.sizeVarString(value.id, 'utf8') + size += bufio.sizeVarString(value.name, 'utf8') + if (value.spendingKey) { + size += KEY_LENGTH + } + size += VIEW_KEY_LENGTH + size += KEY_LENGTH + size += KEY_LENGTH + size += PUBLIC_ADDRESS_LENGTH + + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + size += encoding.nonNullSize + } + + if (value.multisigKeys) { + const encoding = new MultisigKeysEncoding() + size += 8 // size of multi sig keys + size += encoding.getSize(value.multisigKeys) + } + if (value.proofAuthorizingKey) { + size += KEY_LENGTH + } + + return size + } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/new/headValue.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/headValue.ts new file mode 100644 index 0000000000..9da18b093c --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/headValue.ts @@ -0,0 +1,41 @@ +/* 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 bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' + +export type HeadValue = { + hash: Buffer + sequence: number +} + +export class NullableHeadValueEncoding implements IDatabaseEncoding { + readonly nonNullSize = 32 + 4 // 256-bit block hash + 32-bit integer + + serialize(value: HeadValue | null): Buffer { + const bw = bufio.write(this.getSize(value)) + + if (value) { + bw.writeHash(value.hash) + bw.writeU32(value.sequence) + } + + return bw.render() + } + + deserialize(buffer: Buffer): HeadValue | null { + const reader = bufio.read(buffer, true) + + if (reader.left()) { + const hash = reader.readHash() + const sequence = reader.readU32() + return { hash, sequence } + } + + return null + } + + getSize(value: HeadValue | null): number { + return value ? this.nonNullSize : 0 + } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/new/index.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/index.ts new file mode 100644 index 0000000000..ca24820f30 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/index.ts @@ -0,0 +1,20 @@ +/* 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 { IDatabase, IDatabaseStore, StringEncoding } from '../../../../storage' +import { AccountValue, AccountValueEncoding } from './accountValue' + +export function GetNewStores(db: IDatabase): { + accounts: IDatabaseStore<{ key: string; value: AccountValue }> +} { + const accounts: IDatabaseStore<{ key: string; value: AccountValue }> = db.addStore( + { + name: 'a', + keyEncoding: new StringEncoding(), + valueEncoding: new AccountValueEncoding(), + }, + false, + ) + + return { accounts } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/new/multisigKeys.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/multisigKeys.ts new file mode 100644 index 0000000000..9aa3ace613 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/new/multisigKeys.ts @@ -0,0 +1,92 @@ +/* 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 bufio from 'bufio' +import { Assert } from '../../../../assert' +import { IDatabaseEncoding } from '../../../../storage' +import { + MultisigHardwareSigner, + MultisigKeys, + MultisigSigner, +} from '../../../../wallet/interfaces/multisigKeys' + +export class MultisigKeysEncoding implements IDatabaseEncoding { + serialize(value: MultisigKeys): Buffer { + const bw = bufio.write(this.getSize(value)) + + let flags = 0 + flags |= Number(!!isSignerMultisig(value)) << 0 + flags |= Number(!!isHardwareSignerMultisig(value)) << 1 + bw.writeU8(flags) + + bw.writeVarBytes(Buffer.from(value.publicKeyPackage, 'hex')) + if (isSignerMultisig(value)) { + bw.writeVarBytes(Buffer.from(value.secret, 'hex')) + bw.writeVarBytes(Buffer.from(value.keyPackage, 'hex')) + } else if (isHardwareSignerMultisig(value)) { + bw.writeVarBytes(Buffer.from(value.identity, 'hex')) + } + + return bw.render() + } + + deserialize(buffer: Buffer): MultisigKeys { + const reader = bufio.read(buffer, true) + + const flags = reader.readU8() + const isSigner = flags & (1 << 0) + const isHardwareSigner = flags & (1 << 1) + + const publicKeyPackage = reader.readVarBytes().toString('hex') + if (isSigner) { + const secret = reader.readVarBytes().toString('hex') + const keyPackage = reader.readVarBytes().toString('hex') + return { + publicKeyPackage, + secret, + keyPackage, + } + } else if (isHardwareSigner) { + const identity = reader.readVarBytes().toString('hex') + return { + publicKeyPackage, + identity, + } + } + + return { + publicKeyPackage, + } + } + + getSize(value: MultisigKeys): number { + let size = 0 + size += 1 // flags + + size += bufio.sizeVarString(value.publicKeyPackage, 'hex') + if (isSignerMultisig(value)) { + size += bufio.sizeVarString(value.secret, 'hex') + size += bufio.sizeVarString(value.keyPackage, 'hex') + } else if (isHardwareSignerMultisig(value)) { + size += bufio.sizeVarString(value.identity, 'hex') + } + + return size + } +} + +export function isSignerMultisig(multisigKeys: MultisigKeys): multisigKeys is MultisigSigner { + return 'keyPackage' in multisigKeys && 'secret' in multisigKeys +} + +export function isHardwareSignerMultisig( + multisigKeys: MultisigKeys, +): multisigKeys is MultisigHardwareSigner { + return 'identity' in multisigKeys +} + +export function AssertIsSignerMultisig( + multisigKeys: MultisigKeys, +): asserts multisigKeys is MultisigSigner { + Assert.isTrue(isSignerMultisig(multisigKeys)) +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/old/accountValue.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/accountValue.ts new file mode 100644 index 0000000000..65b1854e73 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/accountValue.ts @@ -0,0 +1,229 @@ +/* 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 { KEY_LENGTH, PUBLIC_ADDRESS_LENGTH, xchacha20poly1305 } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' +import { MultisigKeys } from '../../../../wallet/interfaces/multisigKeys' +import { HeadValue, NullableHeadValueEncoding } from './headValue' +import { MultisigKeysEncoding } from './multisigKeys' + +export const VIEW_KEY_LENGTH = 64 +const VERSION_LENGTH = 2 + +export type EncryptedAccountValue = { + encrypted: true + salt: Buffer + nonce: Buffer + data: Buffer +} + +export type DecryptedAccountValue = { + encrypted: false + version: number + id: string + name: string + spendingKey: string | null + viewKey: string + incomingViewKey: string + outgoingViewKey: string + publicAddress: string + createdAt: HeadValue | null + scanningEnabled: boolean + multisigKeys?: MultisigKeys + proofAuthorizingKey: string | null +} + +export type AccountValue = EncryptedAccountValue | DecryptedAccountValue +export class AccountValueEncoding implements IDatabaseEncoding { + serialize(value: AccountValue): Buffer { + if (value.encrypted) { + return this.serializeEncrypted(value) + } else { + return this.serializeDecrypted(value) + } + } + + serializeEncrypted(value: EncryptedAccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + + let flags = 0 + flags |= Number(!!value.encrypted) << 5 + bw.writeU8(flags) + bw.writeBytes(value.salt) + bw.writeBytes(value.nonce) + bw.writeVarBytes(value.data) + + return bw.render() + } + + serializeDecrypted(value: DecryptedAccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + let flags = 0 + flags |= Number(!!value.spendingKey) << 0 + flags |= Number(!!value.createdAt) << 1 + flags |= Number(!!value.multisigKeys) << 2 + flags |= Number(!!value.proofAuthorizingKey) << 3 + flags |= Number(!!value.scanningEnabled) << 4 + flags |= Number(!!value.encrypted) << 5 + + bw.writeU8(flags) + bw.writeU16(value.version) + bw.writeVarString(value.id, 'utf8') + bw.writeVarString(value.name, 'utf8') + + if (value.spendingKey) { + bw.writeBytes(Buffer.from(value.spendingKey, 'hex')) + } + + bw.writeBytes(Buffer.from(value.viewKey, 'hex')) + bw.writeBytes(Buffer.from(value.incomingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.outgoingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.publicAddress, 'hex')) + + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + bw.writeBytes(encoding.serialize(value.createdAt)) + } + + if (value.multisigKeys) { + const encoding = new MultisigKeysEncoding() + bw.writeU64(encoding.getSize(value.multisigKeys)) + bw.writeBytes(encoding.serialize(value.multisigKeys)) + } + + if (value.proofAuthorizingKey) { + bw.writeBytes(Buffer.from(value.proofAuthorizingKey, 'hex')) + } + + return bw.render() + } + + deserialize(buffer: Buffer): AccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const encrypted = Boolean(flags & (1 << 5)) + + if (encrypted) { + return this.deserializeEncrypted(buffer) + } else { + return this.deserializeDecrypted(buffer) + } + } + + deserializeEncrypted(buffer: Buffer): EncryptedAccountValue { + const reader = bufio.read(buffer, true) + + // Skip flags + reader.readU8() + + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const data = reader.readVarBytes() + return { + encrypted: true, + nonce, + salt, + data, + } + } + + deserializeDecrypted(buffer: Buffer): DecryptedAccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const version = reader.readU16() + const hasSpendingKey = flags & (1 << 0) + const hasCreatedAt = flags & (1 << 1) + const hasMultisigKeys = flags & (1 << 2) + const hasProofAuthorizingKey = flags & (1 << 3) + const scanningEnabled = Boolean(flags & (1 << 4)) + const id = reader.readVarString('utf8') + const name = reader.readVarString('utf8') + const spendingKey = hasSpendingKey ? reader.readBytes(KEY_LENGTH).toString('hex') : null + const viewKey = reader.readBytes(VIEW_KEY_LENGTH).toString('hex') + const incomingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const outgoingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const publicAddress = reader.readBytes(PUBLIC_ADDRESS_LENGTH).toString('hex') + + let createdAt = null + if (hasCreatedAt) { + const encoding = new NullableHeadValueEncoding() + createdAt = encoding.deserialize(reader.readBytes(encoding.nonNullSize)) + } + + let multisigKeys = undefined + if (hasMultisigKeys) { + const multisigKeysLength = reader.readU64() + const encoding = new MultisigKeysEncoding() + multisigKeys = encoding.deserialize(reader.readBytes(multisigKeysLength)) + } + + const proofAuthorizingKey = hasProofAuthorizingKey + ? reader.readBytes(KEY_LENGTH).toString('hex') + : null + + return { + encrypted: false, + version, + id, + name, + viewKey, + incomingViewKey, + outgoingViewKey, + spendingKey, + publicAddress, + createdAt, + scanningEnabled, + multisigKeys, + proofAuthorizingKey, + } + } + + getSize(value: AccountValue): number { + if (value.encrypted) { + return this.getSizeEncrypted(value) + } else { + return this.getSizeDecrypted(value) + } + } + + getSizeEncrypted(value: EncryptedAccountValue): number { + let size = 0 + size += 1 // flags + size += xchacha20poly1305.XSALT_LENGTH + size += xchacha20poly1305.XNONCE_LENGTH + size += bufio.sizeVarBytes(value.data) + return size + } + + getSizeDecrypted(value: DecryptedAccountValue): number { + let size = 0 + size += 1 // flags + size += VERSION_LENGTH + size += bufio.sizeVarString(value.id, 'utf8') + size += bufio.sizeVarString(value.name, 'utf8') + if (value.spendingKey) { + size += KEY_LENGTH + } + size += VIEW_KEY_LENGTH + size += KEY_LENGTH + size += KEY_LENGTH + size += PUBLIC_ADDRESS_LENGTH + + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + size += encoding.nonNullSize + } + + if (value.multisigKeys) { + const encoding = new MultisigKeysEncoding() + size += 8 // size of multi sig keys + size += encoding.getSize(value.multisigKeys) + } + if (value.proofAuthorizingKey) { + size += KEY_LENGTH + } + + return size + } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/old/headValue.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/headValue.ts new file mode 100644 index 0000000000..9da18b093c --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/headValue.ts @@ -0,0 +1,41 @@ +/* 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 bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' + +export type HeadValue = { + hash: Buffer + sequence: number +} + +export class NullableHeadValueEncoding implements IDatabaseEncoding { + readonly nonNullSize = 32 + 4 // 256-bit block hash + 32-bit integer + + serialize(value: HeadValue | null): Buffer { + const bw = bufio.write(this.getSize(value)) + + if (value) { + bw.writeHash(value.hash) + bw.writeU32(value.sequence) + } + + return bw.render() + } + + deserialize(buffer: Buffer): HeadValue | null { + const reader = bufio.read(buffer, true) + + if (reader.left()) { + const hash = reader.readHash() + const sequence = reader.readU32() + return { hash, sequence } + } + + return null + } + + getSize(value: HeadValue | null): number { + return value ? this.nonNullSize : 0 + } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/old/index.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/index.ts new file mode 100644 index 0000000000..faf4491604 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/index.ts @@ -0,0 +1,31 @@ +/* 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 { IDatabase, IDatabaseStore, StringEncoding } from '../../../../storage' +import { AccountValue, AccountValueEncoding } from './accountValue' +import { MasterKeyValue, MasterKeyValueEncoding } from './masterKeyValue' + +export function GetOldStores(db: IDatabase): { + accounts: IDatabaseStore<{ key: string; value: AccountValue }> + masterKey: IDatabaseStore<{ key: string; value: MasterKeyValue }> +} { + const accounts: IDatabaseStore<{ key: string; value: AccountValue }> = db.addStore( + { + name: 'a', + keyEncoding: new StringEncoding(), + valueEncoding: new AccountValueEncoding(), + }, + false, + ) + + const masterKey: IDatabaseStore<{ key: string; value: MasterKeyValue }> = db.addStore( + { + name: 'mk', + keyEncoding: new StringEncoding<'key'>(), + valueEncoding: new MasterKeyValueEncoding(), + }, + false, + ) + + return { accounts, masterKey } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/old/masterKeyValue.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/masterKeyValue.ts new file mode 100644 index 0000000000..7c3b133d95 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/masterKeyValue.ts @@ -0,0 +1,32 @@ +/* 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 { xchacha20poly1305 } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' + +export type MasterKeyValue = { + nonce: Buffer + salt: Buffer +} + +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 { + const reader = bufio.read(buffer, true) + + const nonce = reader.readBytes(xchacha20poly1305.XNONCE_LENGTH) + const salt = reader.readBytes(xchacha20poly1305.XSALT_LENGTH) + return { nonce, salt } + } + + getSize(): number { + return xchacha20poly1305.XNONCE_LENGTH + xchacha20poly1305.XSALT_LENGTH + } +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/old/multisigKeys.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/multisigKeys.ts new file mode 100644 index 0000000000..9aa3ace613 --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/old/multisigKeys.ts @@ -0,0 +1,92 @@ +/* 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 bufio from 'bufio' +import { Assert } from '../../../../assert' +import { IDatabaseEncoding } from '../../../../storage' +import { + MultisigHardwareSigner, + MultisigKeys, + MultisigSigner, +} from '../../../../wallet/interfaces/multisigKeys' + +export class MultisigKeysEncoding implements IDatabaseEncoding { + serialize(value: MultisigKeys): Buffer { + const bw = bufio.write(this.getSize(value)) + + let flags = 0 + flags |= Number(!!isSignerMultisig(value)) << 0 + flags |= Number(!!isHardwareSignerMultisig(value)) << 1 + bw.writeU8(flags) + + bw.writeVarBytes(Buffer.from(value.publicKeyPackage, 'hex')) + if (isSignerMultisig(value)) { + bw.writeVarBytes(Buffer.from(value.secret, 'hex')) + bw.writeVarBytes(Buffer.from(value.keyPackage, 'hex')) + } else if (isHardwareSignerMultisig(value)) { + bw.writeVarBytes(Buffer.from(value.identity, 'hex')) + } + + return bw.render() + } + + deserialize(buffer: Buffer): MultisigKeys { + const reader = bufio.read(buffer, true) + + const flags = reader.readU8() + const isSigner = flags & (1 << 0) + const isHardwareSigner = flags & (1 << 1) + + const publicKeyPackage = reader.readVarBytes().toString('hex') + if (isSigner) { + const secret = reader.readVarBytes().toString('hex') + const keyPackage = reader.readVarBytes().toString('hex') + return { + publicKeyPackage, + secret, + keyPackage, + } + } else if (isHardwareSigner) { + const identity = reader.readVarBytes().toString('hex') + return { + publicKeyPackage, + identity, + } + } + + return { + publicKeyPackage, + } + } + + getSize(value: MultisigKeys): number { + let size = 0 + size += 1 // flags + + size += bufio.sizeVarString(value.publicKeyPackage, 'hex') + if (isSignerMultisig(value)) { + size += bufio.sizeVarString(value.secret, 'hex') + size += bufio.sizeVarString(value.keyPackage, 'hex') + } else if (isHardwareSignerMultisig(value)) { + size += bufio.sizeVarString(value.identity, 'hex') + } + + return size + } +} + +export function isSignerMultisig(multisigKeys: MultisigKeys): multisigKeys is MultisigSigner { + return 'keyPackage' in multisigKeys && 'secret' in multisigKeys +} + +export function isHardwareSignerMultisig( + multisigKeys: MultisigKeys, +): multisigKeys is MultisigHardwareSigner { + return 'identity' in multisigKeys +} + +export function AssertIsSignerMultisig( + multisigKeys: MultisigKeys, +): asserts multisigKeys is MultisigSigner { + Assert.isTrue(isSignerMultisig(multisigKeys)) +} diff --git a/ironfish/src/migrations/data/000-encrypted-wallet-template/stores.ts b/ironfish/src/migrations/data/000-encrypted-wallet-template/stores.ts new file mode 100644 index 0000000000..b046b7c66f --- /dev/null +++ b/ironfish/src/migrations/data/000-encrypted-wallet-template/stores.ts @@ -0,0 +1,16 @@ +/* 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 { IDatabase } from '../../../storage' +import { GetNewStores } from './new' +import { GetOldStores } from './old' + +export function GetStores(db: IDatabase): { + old: ReturnType + new: ReturnType +} { + const oldStores = GetOldStores(db) + const newStores = GetNewStores(db) + + return { old: oldStores, new: newStores } +}