diff --git a/ironfish/src/migrations/data/033-multisig-keys-identity.ts b/ironfish/src/migrations/data/033-multisig-keys-identity.ts new file mode 100644 index 0000000000..741da9b13d --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity.ts @@ -0,0 +1,172 @@ +/* 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 { multisig } from '@ironfish/rust-nodejs' +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 './033-multisig-keys-identity/new/accountValue' +import { + AccountValueEncoding as OldAccountValueEncoding, + DecryptedAccountValue as OldDecryptedAccountValue, + EncryptedAccountValue as OldEncryptedAccountValue, +} from './033-multisig-keys-identity/old/accountValue' +import { isSignerMultisig } from './033-multisig-keys-identity/old/multisigKeys' +import { GetStores } from './033-multisig-keys-identity/stores' + +export class Migration033 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. + 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) + } + } + } + + accountForward(oldValue: OldDecryptedAccountValue): NewDecryptedAccountValue { + const multisigKeys = oldValue.multisigKeys + if (!multisigKeys || !isSignerMultisig(multisigKeys)) { + return oldValue + } + + const secret = new multisig.ParticipantSecret(Buffer.from(multisigKeys.secret, 'hex')) + const newValue = { + ...oldValue, + multisigKeys: { + ...multisigKeys, + identity: secret.toIdentity().serialize().toString('hex'), + }, + } + return newValue + } + + 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. + 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) + } + } + } + + accountBackward(newValue: NewDecryptedAccountValue): OldDecryptedAccountValue { + const oldValue = newValue + return oldValue + } +} diff --git a/ironfish/src/migrations/data/033-multisig-keys-identity/new/accountValue.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/new/accountValue.ts new file mode 100644 index 0000000000..95c0f4d5c1 --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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 { HeadValue, NullableHeadValueEncoding } from './headValue' +import { MultisigKeys } from './interfaces/multisigKeys' +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/033-multisig-keys-identity/new/headValue.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/new/headValue.ts new file mode 100644 index 0000000000..9da18b093c --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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/033-multisig-keys-identity/new/index.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/new/index.ts new file mode 100644 index 0000000000..bcc37b3010 --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/new/index.ts @@ -0,0 +1,23 @@ +/* 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' + +/* GetNewStores must be defined for each migration. It should return a reference + * to each datastore that the migration modifies. + */ +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/033-multisig-keys-identity/new/interfaces/multisigKeys.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/new/interfaces/multisigKeys.ts new file mode 100644 index 0000000000..c8d3c884e7 --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/new/interfaces/multisigKeys.ts @@ -0,0 +1,21 @@ +/* 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/. */ + +export interface MultisigSigner { + secret: string + identity: string + keyPackage: string + publicKeyPackage: string +} + +export interface MultisigHardwareSigner { + identity: string + publicKeyPackage: string +} + +export interface MultisigCoordinator { + publicKeyPackage: string +} + +export type MultisigKeys = MultisigSigner | MultisigHardwareSigner | MultisigCoordinator diff --git a/ironfish/src/migrations/data/033-multisig-keys-identity/new/multisigKeys.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/new/multisigKeys.ts new file mode 100644 index 0000000000..56e37fb471 --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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 './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.identity, '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 identity = reader.readVarBytes().toString('hex') + const keyPackage = reader.readVarBytes().toString('hex') + return { + publicKeyPackage, + secret, + identity, + 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.identity, '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 && !('secret' in multisigKeys) +} + +export function AssertIsSignerMultisig( + multisigKeys: MultisigKeys, +): asserts multisigKeys is MultisigSigner { + Assert.isTrue(isSignerMultisig(multisigKeys)) +} diff --git a/ironfish/src/migrations/data/033-multisig-keys-identity/old/accountValue.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/old/accountValue.ts new file mode 100644 index 0000000000..95c0f4d5c1 --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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 { HeadValue, NullableHeadValueEncoding } from './headValue' +import { MultisigKeys } from './interfaces/multisigKeys' +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/033-multisig-keys-identity/old/headValue.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/old/headValue.ts new file mode 100644 index 0000000000..9da18b093c --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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/033-multisig-keys-identity/old/index.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/old/index.ts new file mode 100644 index 0000000000..faf4491604 --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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/033-multisig-keys-identity/old/interfaces/multisigKeys.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/old/interfaces/multisigKeys.ts new file mode 100644 index 0000000000..7686bcb59b --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/old/interfaces/multisigKeys.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/. */ + +export interface MultisigSigner { + secret: string + keyPackage: string + publicKeyPackage: string +} + +export interface MultisigHardwareSigner { + identity: string + publicKeyPackage: string +} + +export interface MultisigCoordinator { + publicKeyPackage: string +} + +export type MultisigKeys = MultisigSigner | MultisigHardwareSigner | MultisigCoordinator diff --git a/ironfish/src/migrations/data/033-multisig-keys-identity/old/masterKeyValue.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/old/masterKeyValue.ts new file mode 100644 index 0000000000..7c3b133d95 --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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/033-multisig-keys-identity/old/multisigKeys.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/old/multisigKeys.ts new file mode 100644 index 0000000000..96c441476e --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/old/multisigKeys.ts @@ -0,0 +1,88 @@ +/* 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 './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/033-multisig-keys-identity/stores.ts b/ironfish/src/migrations/data/033-multisig-keys-identity/stores.ts new file mode 100644 index 0000000000..b046b7c66f --- /dev/null +++ b/ironfish/src/migrations/data/033-multisig-keys-identity/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 } +} diff --git a/ironfish/src/migrations/data/index.ts b/ironfish/src/migrations/data/index.ts index 8b7bc1b25c..6c13717c83 100644 --- a/ironfish/src/migrations/data/index.ts +++ b/ironfish/src/migrations/data/index.ts @@ -21,6 +21,7 @@ import { Migration029 } from './029-backfill-assets-owner-wallet' import { Migration030 } from './030-value-to-unspent-note' import { Migration031 } from './031-add-pak-to-account' import { Migration032 } from './032-add-account-scanning' +import { Migration033 } from './033-multisig-keys-identity' export const MIGRATIONS = [ Migration014, @@ -42,4 +43,5 @@ export const MIGRATIONS = [ Migration030, Migration031, Migration032, + Migration033, ] diff --git a/ironfish/src/wallet/interfaces/multisigKeys.ts b/ironfish/src/wallet/interfaces/multisigKeys.ts index 7686bcb59b..c8d3c884e7 100644 --- a/ironfish/src/wallet/interfaces/multisigKeys.ts +++ b/ironfish/src/wallet/interfaces/multisigKeys.ts @@ -4,6 +4,7 @@ export interface MultisigSigner { secret: string + identity: string keyPackage: string publicKeyPackage: string } diff --git a/ironfish/src/wallet/wallet.ts b/ironfish/src/wallet/wallet.ts index 8d8f21452f..858ce44080 100644 --- a/ironfish/src/wallet/wallet.ts +++ b/ironfish/src/wallet/wallet.ts @@ -1510,15 +1510,20 @@ export class Wallet { } multisigKeys = { - keyPackage: accountValue.multisigKeys.keyPackage, - publicKeyPackage: accountValue.multisigKeys.publicKeyPackage, + ...accountValue.multisigKeys, secret: multisigIdentity.secret.toString('hex'), } secret = multisigIdentity.secret identity = Buffer.from(accountValue.multisigKeys.identity, 'hex') } else if (isMultisigSignerImport(accountValue.multisigKeys)) { secret = Buffer.from(accountValue.multisigKeys.secret, 'hex') + // Derive identity from secret for backwards compatibility: legacy + // MultisigKeysImport may not include identity identity = new multisig.ParticipantSecret(secret).toIdentity().serialize() + multisigKeys = { + ...accountValue.multisigKeys, + identity: identity.toString('hex'), + } } else if (isMultisigHardwareSignerImport(accountValue.multisigKeys)) { identity = Buffer.from(accountValue.multisigKeys.identity, 'hex') } diff --git a/ironfish/src/wallet/walletdb/accountValue.test.ts b/ironfish/src/wallet/walletdb/accountValue.test.ts index ee716a96b5..ea0f8d0b9a 100644 --- a/ironfish/src/wallet/walletdb/accountValue.test.ts +++ b/ironfish/src/wallet/walletdb/accountValue.test.ts @@ -56,6 +56,7 @@ describe('AccountValueEncoding', () => { multisigKeys: { publicKeyPackage: 'cccc', secret: 'deaf', + identity: 'c0ffee', keyPackage: 'beef', }, proofAuthorizingKey: key.proofAuthorizingKey, @@ -84,6 +85,7 @@ describe('AccountValueEncoding', () => { multisigKeys: { publicKeyPackage: 'cccc', secret: 'deaf', + identity: 'c0ffee', keyPackage: 'beef', }, proofAuthorizingKey: key.proofAuthorizingKey, diff --git a/ironfish/src/wallet/walletdb/multiSigKeys.test.ts b/ironfish/src/wallet/walletdb/multiSigKeys.test.ts index a72b2a4e16..d699976983 100644 --- a/ironfish/src/wallet/walletdb/multiSigKeys.test.ts +++ b/ironfish/src/wallet/walletdb/multiSigKeys.test.ts @@ -17,6 +17,7 @@ describe('multisigKeys encoder', () => { publicKeyPackage: 'aaaaaa', secret: 'aaaaaa', keyPackage: 'bbbb', + identity: 'cccc', } const buffer = encoder.serialize(value) const deserializedValue = encoder.deserialize(buffer) diff --git a/ironfish/src/wallet/walletdb/multisigKeys.ts b/ironfish/src/wallet/walletdb/multisigKeys.ts index d6f8ad4c55..6c0450e7c9 100644 --- a/ironfish/src/wallet/walletdb/multisigKeys.ts +++ b/ironfish/src/wallet/walletdb/multisigKeys.ts @@ -22,6 +22,7 @@ export class MultisigKeysEncoding implements IDatabaseEncoding { bw.writeVarBytes(Buffer.from(value.publicKeyPackage, 'hex')) if (isSignerMultisig(value)) { bw.writeVarBytes(Buffer.from(value.secret, 'hex')) + bw.writeVarBytes(Buffer.from(value.identity, 'hex')) bw.writeVarBytes(Buffer.from(value.keyPackage, 'hex')) } else if (isHardwareSignerMultisig(value)) { bw.writeVarBytes(Buffer.from(value.identity, 'hex')) @@ -40,10 +41,12 @@ export class MultisigKeysEncoding implements IDatabaseEncoding { const publicKeyPackage = reader.readVarBytes().toString('hex') if (isSigner) { const secret = reader.readVarBytes().toString('hex') + const identity = reader.readVarBytes().toString('hex') const keyPackage = reader.readVarBytes().toString('hex') return { publicKeyPackage, secret, + identity, keyPackage, } } else if (isHardwareSigner) { @@ -66,6 +69,7 @@ export class MultisigKeysEncoding implements IDatabaseEncoding { size += bufio.sizeVarString(value.publicKeyPackage, 'hex') if (isSignerMultisig(value)) { size += bufio.sizeVarString(value.secret, 'hex') + size += bufio.sizeVarString(value.identity, 'hex') size += bufio.sizeVarString(value.keyPackage, 'hex') } else if (isHardwareSignerMultisig(value)) { size += bufio.sizeVarString(value.identity, 'hex') @@ -82,7 +86,7 @@ export function isSignerMultisig(multisigKeys: MultisigKeys): multisigKeys is Mu export function isHardwareSignerMultisig( multisigKeys: MultisigKeys, ): multisigKeys is MultisigHardwareSigner { - return 'identity' in multisigKeys + return 'identity' in multisigKeys && !('secret' in multisigKeys) } export function AssertIsSignerMultisig( diff --git a/ironfish/src/wallet/walletdb/walletdb.ts b/ironfish/src/wallet/walletdb/walletdb.ts index 13c8e2e9b6..cafa934666 100644 --- a/ironfish/src/wallet/walletdb/walletdb.ts +++ b/ironfish/src/wallet/walletdb/walletdb.ts @@ -45,7 +45,7 @@ import { MultisigIdentityValue, MultisigIdentityValueEncoder } from './multisigI import { ParticipantIdentity, ParticipantIdentityEncoding } from './participantIdentity' import { TransactionValue, TransactionValueEncoding } from './transactionValue' -const VERSION_DATABASE_ACCOUNTS = 32 +const VERSION_DATABASE_ACCOUNTS = 33 const getAccountsDBMetaDefaults = (): AccountsDBMeta => ({ defaultAccountId: null,