//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
+import { UUID } from 'crypto'
import { utf8 } from "../convert"
+import { WalletType } from "../wallet"
export class WalletAesGcm {
- static decrypt (type: string, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
+ static decrypt (type: WalletType, id: UUID, key: CryptoKey, iv: ArrayBuffer, encrypted: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
const seedLength = type === 'BLAKE2b' ? 32 : 64
- const additionalData = utf8.toBuffer(type)
+ const additionalData = utf8.toBuffer(`${type};${id}`)
return crypto.subtle
.decrypt({ name: 'AES-GCM', iv, additionalData }, key, encrypted)
.then(decrypted => {
})
}
- static encrypt (type: string, key: CryptoKey, seed: ArrayBuffer, mnemonic?: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
+ static encrypt (type: WalletType, id: UUID, key: CryptoKey, seed: ArrayBuffer, mnemonic?: ArrayBuffer): Promise<Record<string, ArrayBuffer>> {
if (type == null) {
throw new Error('Wallet type missing')
}
+ if (id == null) {
+ throw new Error('Wallet ID missing')
+ }
if (key == null) {
throw new Error('Wallet key missing')
}
}
// restrict iv to 96 bits per GCM best practice
const iv = crypto.getRandomValues(new Uint8Array(12)).buffer
- const additionalData = utf8.toBuffer(type)
+ const additionalData = utf8.toBuffer(`${type};${id}`)
const encoded = new Uint8Array([...new Uint8Array(seed), ...new Uint8Array(mnemonic ?? [])])
return crypto.subtle
.encrypt({ name: 'AES-GCM', iv, additionalData }, key, encoded)
//! SPDX-License-Identifier: GPL-3.0-or-later
import { derive as nano25519_derive, sign as nano25519_sign } from 'nano25519/sync'
+import { UUID } from 'node:crypto'
import { parentPort, threadId } from 'node:worker_threads'
import { BIP44_COIN_NANO } from '../constants'
import { dec, utf8 } from '../convert'
import { Bip39, Bip44, Blake2b, WalletAesGcm } from '../crypto'
import { WalletType } from '../wallet'
-import { parseAction, parseData, parseIv, parseKeySalt, parseType } from './parsers'
+import { parseAction, parseData, parseId, parseIv, parseKeySalt, parseType } from './parsers'
import { Passkey } from './passkey'
import { VaultTimer } from './vault-timer'
let _timeout: number = 120_000
let _timer: VaultTimer = new VaultTimer(() => { }, 0)
let _type: 'BIP-44' | 'BLAKE2b' | 'Exodus' | undefined = undefined
+let _id: UUID | undefined = undefined
let _seed: ArrayBuffer | undefined = undefined
let _mnemonic: ArrayBuffer | undefined = undefined
Passkey.create(action, keySalt, data)
.then((key: CryptoKey | undefined): Promise<Record<string, boolean | number | ArrayBuffer> | void> => {
const type = parseType(action, data)
+ const id = parseId(action, data)
const iv = parseIv(action, data)
const { seed, mnemonicPhrase, mnemonicSalt, index, encrypted, message, timeout } = parseData(action, data)
switch (action) {
return config(timeout)
}
case 'create': {
- return create(type, key, keySalt, mnemonicSalt)
+ return create(type, id, key, keySalt, mnemonicSalt)
}
case 'derive': {
return derive(index)
}
case 'load': {
- return load(type, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
+ return load(type, id, key, keySalt, mnemonicPhrase ?? seed, mnemonicSalt)
}
case 'lock': {
return lock()
return sign(index, message)
}
case 'unlock': {
- return unlock(type, key, iv, encrypted)
+ return unlock(type, id, key, iv, encrypted)
}
case 'update': {
return update(key, keySalt)
* Generates a new mnemonic and seed and then returns the initialization vector
* vector, salt, and encrypted data representing the wallet in a locked state.
*/
-function create (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
+function create (type?: WalletType, id?: UUID, key?: CryptoKey, keySalt?: ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
if (type !== 'BIP-44' && type !== 'BLAKE2b') {
throw new TypeError('Unsupported software wallet algorithm', { cause: type })
}
try {
const entropy = crypto.getRandomValues(new Uint8Array(32))
return Bip39.fromEntropy(entropy)
- .then(bip39 => _load(type, key, keySalt, bip39.phrase, mnemonicSalt))
+ .then(bip39 => _load(type, id, key, keySalt, bip39.phrase, mnemonicSalt))
.then(({ iv, salt, encrypted }) => {
entropy.fill(0)
if (_seed == null || _mnemonic == null) {
* Encrypts an existing seed or mnemonic+salt and returns the initialization
* vector, salt, and encrypted data representing the wallet in a locked state.
*/
-function load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
+function load (type?: WalletType, id?: UUID, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
throw new TypeError('Unsupported software wallet algorithm', { cause: type })
}
- return _load(type, key, keySalt, secret, mnemonicSalt)
+ return _load(type, id, key, keySalt, secret, mnemonicSalt)
.then(record => {
if (_seed == null) {
throw new Error('Wallet seed not found')
/**
* Decrypts the input and sets the seed and, if it is included, the mnemonic.
*/
-function unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<Record<string, boolean>> {
+function unlock (type?: WalletType, id?: UUID, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<Record<string, boolean>> {
if (type == null) {
throw new TypeError('Wallet type is required')
}
+ if (id == null) {
+ throw new TypeError('Wallet ID is required')
+ }
if (type === 'Ledger') {
_locked = false
return Promise.resolve({ isLocked: false })
throw new TypeError('Wallet encrypted data is required')
}
_timer?.pause()
- return WalletAesGcm.decrypt(type, key, iv, encrypted)
+ return WalletAesGcm.decrypt(type, id, key, iv, encrypted)
.then(({ mnemonic, seed }) => {
if (!(seed instanceof ArrayBuffer)) {
throw new TypeError('Invalid seed')
throw new TypeError('Invalid mnemonic')
}
_type = type
+ _id = id
_seed = seed
_mnemonic = mnemonic
_locked = false
if (_type == null) {
throw new Error('Wallet type not found')
}
+ if (_id == null) {
+ throw new Error('Wallet ID not found')
+ }
if (key == null || salt == null) {
throw new TypeError('Wallet password is required')
}
- return WalletAesGcm.encrypt(_type, key, _seed, _mnemonic)
+ return WalletAesGcm.encrypt(_type, _id, key, _seed, _mnemonic)
.then(({ iv, encrypted }) => {
_timer = new VaultTimer(_autolock, _timeout)
return { iv, salt, encrypted }
* Encrypts an existing seed or mnemonic+salt and returns the initialization
* vector, salt, and encrypted data representing the wallet in a locked state.
*/
-function _load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
+function _load (type?: 'BIP-44' | 'BLAKE2b' | 'Exodus', id?: UUID, key?: CryptoKey, keySalt?: ArrayBuffer, secret?: string | ArrayBuffer, mnemonicSalt?: string): Promise<Record<string, ArrayBuffer>> {
try {
if (!_locked) {
throw new Error('Wallet is in use')
if (type == null) {
throw new TypeError('Wallet type is required')
}
+ if (id == null) {
+ throw new TypeError('Wallet ID is required')
+ }
+ if (!utf8.isUuid(id)) {
+ throw new TypeError('Invalid wallet ID')
+ }
if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus') {
throw new TypeError('Invalid wallet type')
}
}
}
_type = type
+ _id = id
let seed: Promise<ArrayBuffer>
if (secret instanceof ArrayBuffer) {
if (type === 'BLAKE2b') {
return seed.then(seed => {
_seed = seed
return WalletAesGcm
- .encrypt(type, key, _seed, _mnemonic)
+ .encrypt(type, id, key, _seed, _mnemonic)
.then(({ iv, encrypted }) => ({ iv, salt: keySalt, encrypted }))
})
} catch (err) {
//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>\r
//! SPDX-License-Identifier: GPL-3.0-or-later\r
\r
+import { UUID } from 'crypto'\r
import { Account } from '../account'\r
import { Block } from '../block'\r
import { ADDRESS_GAP } from '../constants'\r
-import { bytes } from '../convert'\r
+import { bytes, utf8 } from '../convert'\r
import { Ledger } from '../ledger'\r
import { Rpc } from '../rpc'\r
import { Vault } from '../vault'\r
\r
#accounts: Map<number, Account> = new Map<number, Account>()\r
#eventTarget: EventTarget = new EventTarget()\r
- #id: string = crypto.randomUUID()\r
+ #id: UUID = crypto.randomUUID()\r
#vault: Vault = new Vault()\r
\r
#mnemonic?: ArrayBuffer\r
#seed?: ArrayBuffer\r
#type: WalletType\r
\r
- constructor (type: WalletType, id?: string)\r
- constructor (type: unknown, id?: string) {\r
+ constructor (type: WalletType, id?: UUID)\r
+ constructor (type: unknown, id: unknown) {\r
if (!(this.constructor as typeof Wallet).isInternal) {\r
throw new Error(`Wallet cannot be instantiated directly. Use 'await Wallet.create()' instead.`)\r
}\r
if (type !== 'BIP-44' && type !== 'BLAKE2b' && type !== 'Exodus' && type !== 'Ledger') {\r
throw new TypeError('Invalid wallet type', { cause: type })\r
}\r
+ if (id !== undefined && !(utf8.isUuid(id))) {\r
+ throw new TypeError('Invalid wallet ID', { cause: id })\r
+ }\r
this.#id = id ?? this.#id\r
this.#type = type\r
this.#vault.addEventListener('locked', () => this.dispatchEvent(new Event('locked')))\r