\r
import { Blake2b } from './blake2b'\r
import { ACCOUNT_KEY_LENGTH, ALPHABET, PREFIX, PREFIX_LEGACY } from './constants'\r
-import { base32, bytes, hex } from './convert'\r
+import { base32, bytes, hex, utf8 } from './convert'\r
+import { Pool } from './pool'\r
import { Rpc } from './rpc'\r
-import { Safe } from './safe'\r
-import { NanoNaCl } from '#workers'\r
+import { NanoNaCl, SafeWorker } from '#workers'\r
\r
/**\r
* Represents a single Nano address and the associated public key. To include the\r
*/\r
export class Account {\r
static #isInternal: boolean = false\r
+ static #poolSafe: Pool\r
#a: string\r
#pub: string\r
#prv: string | null\r
#r?: bigint\r
#rep?: Account\r
#w?: bigint\r
- #s: Safe\r
\r
get address () { return `${PREFIX}${this.#a}` }\r
get publicKey () { return this.#pub }\r
this.#pub = publicKey\r
this.#prv = privateKey ?? null\r
this.#i = index\r
- this.#s = new Safe()\r
+ Account.#poolSafe ??= new Pool(SafeWorker)\r
Account.#isInternal = false\r
}\r
\r
* allow garbage collection.\r
*/\r
destroy (): void {\r
- this.#s.destroy(this.#pub)\r
+ Account.#poolSafe.assign({\r
+ method: 'destroy',\r
+ name: this.#pub\r
+ })\r
this.#prv = null\r
this.#i = undefined\r
this.#f = undefined\r
return account\r
}\r
\r
- async lock (password: string): Promise<boolean>\r
- async lock (key: CryptoKey): Promise<boolean>\r
- async lock (passkey: string | CryptoKey): Promise<boolean> {\r
+ async lock (password: string | Uint8Array): Promise<boolean> {\r
+ if (typeof password === 'string') {\r
+ password = utf8.toBytes(password)\r
+ }\r
try {\r
if (this.#prv != null) {\r
- await this.#s.put(this.#pub, passkey as string, this.#prv)\r
+ await Account.#poolSafe.assign({\r
+ method: 'put',\r
+ name: this.#prv,\r
+ password\r
+ })\r
}\r
} catch (err) {\r
console.error(`Failed to lock account ${this.address}`, err)\r
return true\r
}\r
\r
- async unlock (password: string): Promise<boolean>\r
- async unlock (key: CryptoKey): Promise<boolean>\r
- async unlock (passkey: string | CryptoKey): Promise<boolean> {\r
+ async unlock (password: string | Uint8Array): Promise<boolean> {\r
+ if (typeof password === 'string') {\r
+ password = utf8.toBytes(password)\r
+ }\r
try {\r
- this.#prv = await this.#s.get(this.#pub, passkey as string)\r
+ this.#prv = await Account.#poolSafe.assign({\r
+ method: 'get',\r
+ name: this.#pub,\r
+ password\r
+ })\r
} catch (err) {\r
console.error(`Failed to unlock account ${this.address}`, err)\r
return false\r
const encoder = new TextEncoder()\r
const decoder = new TextDecoder()\r
\r
-export const base32 = {\r
+export class base32 {\r
/**\r
* Converts a base32 string to a Uint8Array of bytes.\r
*\r
* @param {string} base32 - String to convert\r
* @returns {Uint8Array} Byte array representation of the input string\r
*/\r
- toBytes (base32: string): Uint8Array {\r
+ static toBytes (base32: string): Uint8Array {\r
const leftover = (base32.length * 5) % 8\r
const offset = leftover === 0\r
? 0\r
output = output.slice(1)\r
}\r
return output\r
- },\r
+ }\r
+\r
/**\r
* Converts a base32 string to a hexadecimal string.\r
*\r
* @param {string} base32 - String to convert\r
* @returns {string} Hexadecimal representation of the input base32\r
*/\r
- toHex (base32: string): string {\r
+ static toHex (base32: string): string {\r
return bytes.toHex(this.toBytes(base32))\r
}\r
}\r
\r
-export const bin = {\r
+export class bin {\r
/**\r
* Convert a binary string to a Uint8Array of bytes.\r
*\r
* @param {string} bin - String to convert\r
* @returns {Uint8Array} Byte array representation of the input string\r
*/\r
- toBytes (bin: string): Uint8Array<ArrayBuffer> {\r
+ static toBytes (bin: string): Uint8Array<ArrayBuffer> {\r
const bytes: number[] = []\r
while (bin.length > 0) {\r
const bits = bin.substring(0, 8)\r
bin = bin.substring(8)\r
}\r
return new Uint8Array(bytes)\r
- },\r
+ }\r
+\r
/**\r
* Convert a binary string to a hexadecimal string.\r
*\r
* @param {string} bin - String to convert\r
* @returns {string} Hexadecimal string representation of the input binary\r
*/\r
- toHex (bin: string): string {\r
+ static toHex (bin: string): string {\r
return parseInt(bin, 2).toString(16)\r
}\r
}\r
\r
-export const buffer = {\r
+export class buffer {\r
/**\r
* Converts an ArrayBuffer to a base32 string.\r
*\r
* @param {ArrayBuffer} buffer - Buffer to convert\r
* @returns {string} Base32 string representation of the input buffer\r
*/\r
- toBase32 (buffer: ArrayBuffer): string {\r
+ static toBase32 (buffer: ArrayBuffer): string {\r
return bytes.toBase32(new Uint8Array(buffer))\r
- },\r
+ }\r
+\r
/**\r
* Converts an ArrayBuffer to a binary string.\r
*\r
* @param {ArrayBuffer} buffer - Buffer to convert\r
* @returns {string} Binary string representation of the input buffer\r
*/\r
- toBin (buffer: ArrayBuffer): string {\r
+ static toBin (buffer: ArrayBuffer): string {\r
return bytes.toBin(new Uint8Array(buffer))\r
- },\r
+ }\r
+\r
/**\r
* Sums an ArrayBuffer to a decimal integer. If the result is larger than\r
* Number.MAX_SAFE_INTEGER, it will be returned as a bigint.\r
* @param {ArrayBuffer} buffer - Buffer to convert\r
* @returns {bigint|number} Decimal sum of the literal buffer values\r
*/\r
- toDec (buffer: ArrayBuffer): bigint | number {\r
+ static toDec (buffer: ArrayBuffer): bigint | number {\r
return bytes.toDec(new Uint8Array(buffer))\r
- },\r
+ }\r
+\r
/**\r
* Converts an ArrayBuffer to a hexadecimal string.\r
*\r
* @param {ArrayBuffer} buffer - Buffer to convert\r
* @returns {string} Hexadecimal string representation of the input buffer\r
*/\r
- toHex (buffer: ArrayBuffer): string {\r
+ static toHex (buffer: ArrayBuffer): string {\r
return bytes.toHex(new Uint8Array(buffer))\r
- },\r
+ }\r
+\r
/**\r
* Converts an ArrayBuffer to a UTF-8 text string.\r
*\r
* @param {ArrayBuffer} buffer - Buffer to convert\r
* @returns {string} UTF-8 encoded text string\r
*/\r
- toUtf8 (buffer: ArrayBuffer): string {\r
+ static toUtf8 (buffer: ArrayBuffer): string {\r
return bytes.toUtf8(new Uint8Array(buffer))\r
}\r
}\r
\r
-export const bytes = {\r
+export class bytes {\r
/**\r
* Converts a Uint8Aarray of bytes to a base32 string.\r
*\r
* @param {Uint8Array} bytes - Byte array to convert\r
* @returns {string} Base32 string representation of the input bytes\r
*/\r
- toBase32 (bytes: Uint8Array): string {\r
+ static toBase32 (bytes: Uint8Array): string {\r
const leftover = (bytes.length * 8) % 5\r
const offset = leftover === 0\r
? 0\r
output += ALPHABET[(value << (5 - (bits + offset))) & 31]\r
}\r
return output\r
- },\r
+ }\r
+\r
/**\r
* Convert a Uint8Array of bytes to a binary string.\r
*\r
* @param {Uint8Array} bytes - Byte array to convert\r
* @returns {string} Binary string representation of the input value\r
*/\r
- toBin (bytes: Uint8Array): string {\r
+ static toBin (bytes: Uint8Array): string {\r
return [...bytes].map(b => b.toString(2).padStart(8, '0')).join('')\r
- },\r
+ }\r
+\r
/**\r
* Sums an array of bytes to a decimal integer. If the result is larger than\r
* Number.MAX_SAFE_INTEGER, it will be returned as a bigint.\r
* @param {Uint8Array} bytes - Byte array to convert\r
* @returns {bigint|number} Decimal sum of the literal byte values\r
*/\r
- toDec (bytes: Uint8Array): bigint | number {\r
+ static toDec (bytes: Uint8Array): bigint | number {\r
const integers: bigint[] = []\r
bytes.reverse().forEach(b => integers.push(BigInt(b)))\r
let decimal = 0n\r
} else {\r
return Number(decimal)\r
}\r
- },\r
+ }\r
+\r
/**\r
* Converts a Uint8Array of bytes to a hexadecimal string.\r
*\r
* @param {Uint8Array} bytes - Byte array to convert\r
* @returns {string} Hexadecimal string representation of the input bytes\r
*/\r
- toHex (bytes: Uint8Array): string {\r
+ static toHex (bytes: Uint8Array): string {\r
const byteArray = [...bytes].map(byte => byte.toString(16).padStart(2, '0'))\r
return byteArray.join('').toUpperCase()\r
- },\r
+ }\r
+\r
/**\r
* Converts a Uint8Array of bytes to a UTF-8 text string.\r
*\r
* @param {Uint8Array} bytes - Byte array to convert\r
* @returns {string} UTF-8 encoded text string\r
*/\r
- toUtf8 (bytes: Uint8Array): string {\r
+ static toUtf8 (bytes: Uint8Array): string {\r
return decoder.decode(bytes)\r
}\r
}\r
\r
-export const dec = {\r
+export class dec {\r
/**\r
* Convert a decimal integer to a binary string.\r
*\r
* @param {number} [padding=0] - Minimum length of the resulting string padded as necessary with starting zeroes\r
* @returns {string} Binary string representation of the input decimal\r
*/\r
- toBin (decimal: bigint | number | string, padding: number = 0): string {\r
+ static toBin (decimal: bigint | number | string, padding: number = 0): string {\r
if (typeof padding !== 'number') {\r
throw new TypeError('Invalid padding')\r
}\r
} catch (err) {\r
throw new RangeError('Invalid decimal integer')\r
}\r
- },\r
+ }\r
+\r
/**\r
* Convert a decimal integer to a Uint8Array of bytes. Fractional part is truncated.\r
*\r
* @param {number} [padding=0] - Minimum length of the resulting array padded as necessary with starting 0x00 bytes\r
* @returns {Uint8Array} Byte array representation of the input decimal\r
*/\r
- toBytes (decimal: bigint | number | string, padding: number = 0): Uint8Array {\r
+ static toBytes (decimal: bigint | number | string, padding: number = 0): Uint8Array {\r
if (typeof padding !== 'number') {\r
throw new TypeError('Invalid padding')\r
}\r
const result = new Uint8Array(Math.max(padding, bytes.length))\r
result.set(bytes)\r
return (result.reverse())\r
- },\r
+ }\r
+\r
/**\r
* Convert a decimal integer to a hexadecimal string.\r
*\r
* @param {number} [padding=0] - Minimum length of the resulting string padded as necessary with starting zeroes\r
* @returns {string} Hexadecimal string representation of the input decimal\r
*/\r
- toHex (decimal: bigint | number | string, padding: number = 0): string {\r
+ static toHex (decimal: bigint | number | string, padding: number = 0): string {\r
if (typeof padding !== 'number') {\r
throw new TypeError('Invalid padding')\r
}\r
}\r
}\r
\r
-export const hex = {\r
+export class hex {\r
/**\r
* Convert a hexadecimal string to an array of decimal byte values.\r
*\r
* @param {number}[padding=0] - Minimum length of the resulting array padded as necessary with starting 0 values\r
* @returns {number[]} Decimal array representation of the input value\r
*/\r
- toArray (hex: string, padding: number = 0): number[] {\r
+ static toArray (hex: string, padding: number = 0): number[] {\r
if (typeof hex !== 'string' || !/^[A-Fa-f0-9]+$/i.test(hex)) {\r
throw new TypeError('Invalid string when converting hex to array')\r
}\r
hexArray.unshift('0')\r
}\r
return hexArray.map(v => parseInt(v, 16))\r
- },\r
+ }\r
+\r
/**\r
* Convert a hexadecimal string to a binary string.\r
*\r
* @param {string} hex - Hexadecimal number string to convert\r
* @returns {string} Binary string representation of the input value\r
*/\r
- toBin (hex: string): string {\r
+ static toBin (hex: string): string {\r
return [...hex].map(c => dec.toBin(parseInt(c, 16), 4)).join('')\r
- },\r
+ }\r
+\r
/**\r
* Convert a hexadecimal string to a Uint8Array of bytes.\r
*\r
* @param {number} [padding=0] - Minimum length of the resulting array padded as necessary with starting 0x00 bytes\r
* @returns {Uint8Array} Byte array representation of the input value\r
*/\r
- toBytes (hex: string, padding: number = 0): Uint8Array<ArrayBuffer> {\r
+ static toBytes (hex: string, padding: number = 0): Uint8Array<ArrayBuffer> {\r
return new Uint8Array(this.toArray(hex, padding))\r
}\r
}\r
\r
-export const utf8 = {\r
+export class utf8 {\r
/**\r
* Convert a UTF-8 text string to a Uint8Array of bytes.\r
*\r
* @param {string} utf8 - String to convert\r
* @returns {Uint8Array} Byte array representation of the input string\r
*/\r
- toBytes (utf8: string): Uint8Array {\r
+ static toBytes (utf8: string): Uint8Array {\r
return encoder.encode(utf8)\r
- },\r
+ }\r
+\r
/**\r
* Convert a string to a hexadecimal representation\r
*\r
* @param {string} utf8 - String to convert\r
* @returns {string} Hexadecimal representation of the input string\r
*/\r
- toHex (utf8: string): string {\r
+ static toHex (utf8: string): string {\r
return bytes.toHex(this.toBytes(utf8))\r
}\r
}\r
\r
-export const obj = {\r
+export class obj {\r
/**\r
* Convert a numerically-indexed object of 8-bit values to a Uint8Array of bytes.\r
*\r
* @param {object} obj - Object to convert\r
* @returns {Uint8Array} Byte array representation of the input object\r
*/\r
- toBytes (obj: { [key: number]: number }): Uint8Array {\r
+ static toBytes (obj: { [key: number]: number }): Uint8Array {\r
const values = Object.keys(obj)\r
.map(key => +key)\r
.sort((a, b) => a - b)\r
}\r
}\r
\r
-export default { base32, bin, bytes, dec, hex, utf8 }\r
+export default `\r
+ const base32 = ${base32}\r
+ const bin = ${bin}\r
+ const bytes = ${bytes}\r
+ const dec = ${dec}\r
+ const hex = ${hex}\r
+ const obj = ${obj}\r
+ const utf8 = ${utf8}\r
+`\r
if (next?.length > 0) {
const buffer = new TextEncoder().encode(JSON.stringify(next)).buffer
thread.job = job
+ console.log(JSON.stringify(next))
+ console.log(thread)
+ console.log(thread.worker)
+ console.log(this.#url)
thread.worker.postMessage({ buffer }, [buffer])
}
}
+++ /dev/null
-// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import { buffer, hex, utf8 } from './convert'
-import { Entropy } from './entropy'
-
-const { subtle } = globalThis.crypto
-const ERR_MSG = 'Failed to store item in Safe'
-
-export class Safe {
- #storage: Storage
-
- constructor () {
- this.#storage = globalThis.sessionStorage
- }
-
- /**
- * Removes data from the Safe without decrypting.
- */
- destroy (name: string): void {
- try {
- this.#storage.removeItem(name)
- } catch (err) {
- console.log(err)
- }
- }
-
- /**
- * Encrypts data with a password and stores it in the Safe.
- */
- async put (name: string, password: string, data: any): Promise<boolean>
- /**
- * Encrypts data with a CryptoKey and stores it in the Safe.
- */
- async put (name: string, key: CryptoKey, data: any): Promise<boolean>
- async put (name: string, passkey: string | CryptoKey, data: any): Promise<boolean> {
- if (typeof passkey === 'string') {
- try {
- passkey = await subtle.importKey('raw', utf8.toBytes(passkey), 'PBKDF2', false, ['deriveBits', 'deriveKey'])
- } catch {
- throw new Error(ERR_MSG)
- }
- }
- if (this.#storage.getItem(name)) {
- throw new Error(ERR_MSG)
- }
- return this.overwrite(name, passkey, data)
- }
-
- /**
- * Encrypts data with a password and stores it in the Safe.
- */
- async overwrite (name: string, password: string, data: any): Promise<boolean>
- /**
- * Encrypts data with a CryptoKey and stores it in the Safe.
- */
- async overwrite (name: string, key: CryptoKey, data: any): Promise<boolean>
- async overwrite (name: string, passkey: string | CryptoKey, data: any): Promise<boolean> {
- if (typeof passkey === 'string') {
- try {
- passkey = await subtle.importKey('raw', utf8.toBytes(passkey), 'PBKDF2', false, ['deriveBits', 'deriveKey'])
- } catch {
- throw new Error(ERR_MSG)
- }
- }
- if (this.#isInvalid(name, passkey, data)) {
- throw new Error(ERR_MSG)
- }
-
- const iv = await Entropy.create()
- if (passkey.usages.includes('deriveKey')) {
- try {
- const derivationAlgorithm: Pbkdf2Params = {
- name: 'PBKDF2',
- hash: 'SHA-512',
- salt: iv.bytes,
- iterations: 210000
- }
- const derivedKeyType: AesKeyGenParams = {
- name: 'AES-GCM',
- length: 256
- }
- passkey = await subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['encrypt'])
- } catch {
- throw new Error(ERR_MSG)
- }
- }
-
- try {
- if (typeof data === 'bigint') {
- data = data.toString()
- }
- data = JSON.stringify(data)
- const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, utf8.toBytes(data))
- const record = {
- encrypted: buffer.toHex(encrypted),
- iv: iv.hex
- }
- this.#storage.setItem(name, JSON.stringify(record))
- } catch (err) {
- throw new Error(ERR_MSG)
- }
- return (this.#storage.getItem(name) != null)
- }
-
- /**
- * Retrieves data from the Safe and decrypts data with a password.
- */
- async get (name: string, password: string): Promise<any>
- /**
- * Retrieves data from the Safe and decrypts data with a CryptoKey.
- */
- async get (name: string, key: CryptoKey): Promise<any>
- async get (name: string, passkey: string | CryptoKey): Promise<any> {
- if (typeof passkey === 'string') {
- try {
- passkey = await subtle.importKey('raw', utf8.toBytes(passkey), 'PBKDF2', false, ['deriveBits', 'deriveKey'])
- } catch {
- return null
- }
- }
- if (this.#isInvalid(name, passkey)) {
- return null
- }
-
- const item = this.#storage.getItem(name)
- if (item == null) {
- return null
- }
- const record = JSON.parse(item)
- const encrypted = hex.toBytes(record.encrypted)
- const iv = await Entropy.import(record.iv)
-
- try {
- if (passkey.usages.includes('deriveKey')) {
- const derivationAlgorithm: Pbkdf2Params = {
- name: 'PBKDF2',
- hash: 'SHA-512',
- salt: iv.bytes,
- iterations: 210000
- }
- const derivedKeyType: AesKeyGenParams = {
- name: 'AES-GCM',
- length: 256
- }
- passkey = await subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['decrypt'])
- }
- } catch (err) {
- return null
- }
-
- try {
- const decrypted = await subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, encrypted)
- const decoded = buffer.toUtf8(decrypted)
- const data = JSON.parse(decoded)
- this.#storage.removeItem(name)
- return data
- } catch (err) {
- return null
- }
- }
-
- #isInvalid (name: string, passkey: string | CryptoKey, data?: any): boolean {
- if (typeof name !== 'string' || name === '') {
- return true
- }
- if (typeof passkey !== 'string' || passkey === '') {
- if (!(passkey instanceof CryptoKey)) {
- return true
- }
- }
- if (typeof data === 'object') {
- try {
- JSON.stringify(data)
- } catch (err) {
- return true
- }
- }
- return false
- }
-}
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-import { KeyPair, Wallet } from '.'\r
+import { KeyPair, Wallet } from './wallet'\r
import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
import { SEED_LENGTH_BIP44 } from '#src/lib/constants.js'\r
import { Entropy } from '#src/lib/entropy.js'\r
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-import { KeyPair, Wallet } from '.'\r
+import { KeyPair, Wallet } from './wallet'\r
import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
import { Blake2b } from '#src/lib/blake2b.js'\r
import { SEED_LENGTH_BLAKE2B } from '#src/lib/constants.js'\r
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-import { Account, AccountList } from '#src/lib/account.js'\r
-import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
-import { ADDRESS_GAP } from '#src/lib/constants.js'\r
-import { Entropy } from '#src/lib/entropy.js'\r
-import { Pool } from '#src/lib/pool.js'\r
-import { Rpc } from '#src/lib/rpc.js'\r
-import { Safe } from '#src/lib/safe.js'\r
-import { NanoNaClWorker } from '#workers'\r
-\r
export { Bip44Wallet } from './bip44-wallet'\r
export { Blake2bWallet } from './blake2b-wallet'\r
export { LedgerWallet } from './ledger-wallet'\r
-export type KeyPair = {\r
- publicKey?: string,\r
- privateKey?: string,\r
- index?: number\r
-}\r
-\r
-/**\r
-* Represents a wallet containing numerous Nano accounts derived from a single\r
-* source, the form of which can vary based on the type of wallet. The Wallet\r
-* class itself is abstract and cannot be directly instantiated. Currently, three\r
-* types of wallets are supported, each as a derived class: Bip44Wallet,\r
-* Blake2bWallet, LedgerWallet.\r
-*/\r
-export abstract class Wallet {\r
- #accounts: AccountList\r
- #id: Entropy\r
- #locked: boolean = true\r
- #mnemonic: Bip39Mnemonic | null\r
- #poolNanoNacl: Pool\r
- #safe: Safe\r
- #seed: string | null\r
- get id () { return this.#id.hex }\r
- get isLocked () { return this.#locked }\r
- get isUnlocked () { return !this.#locked }\r
- get mnemonic () {\r
- if (this.#mnemonic instanceof Bip39Mnemonic) {\r
- return this.#mnemonic.phrase\r
- }\r
- return ''\r
- }\r
- get seed () {\r
- if (typeof this.#seed === 'string') {\r
- return this.#seed\r
- }\r
- return ''\r
- }\r
-\r
- abstract ckd (index: number[]): Promise<KeyPair[]>\r
-\r
- constructor (id: Entropy, seed?: string, mnemonic?: Bip39Mnemonic) {\r
- if (this.constructor === Wallet) {\r
- throw new Error('Wallet is an abstract class and cannot be instantiated directly.')\r
- }\r
- this.#accounts = new AccountList()\r
- this.#id = id\r
- this.#mnemonic = mnemonic ?? null\r
- this.#poolNanoNacl = new Pool(NanoNaClWorker)\r
- this.#safe = new Safe()\r
- this.#seed = seed ?? null\r
- }\r
-\r
- /**\r
- * Removes encrypted secrets in storage and releases variable references to\r
- * allow garbage collection.\r
- */\r
- destroy (): void {\r
- let i = 0\r
- for (const a in this.#accounts) {\r
- this.#accounts[a].destroy()\r
- delete this.#accounts[a]\r
- i++\r
- }\r
- this.#safe.destroy(this.id)\r
- this.#mnemonic = null\r
- this.#seed = null\r
- this.#poolNanoNacl.terminate()\r
- }\r
-\r
- /**\r
- * Retrieves an account from a wallet using its child key derivation function.\r
- * Defaults to the first account at index 0.\r
- *\r
- * ```\r
- * console.log(await wallet.account(5))\r
- * // outputs sixth account of the wallet\r
- * // {\r
- * // privateKey: <...>,\r
- * // index: 5\r
- * // }\r
- * ```\r
- *\r
- * @param {number} index - Wallet index of secret key. Default: 0\r
- * @returns {Account} Account derived at the specified wallet index\r
- */\r
- async account (index: number = 0): Promise<Account> {\r
- return (await this.accounts(index))[index]\r
- }\r
-\r
- /**\r
- * Retrieves accounts from a wallet using its child key derivation function.\r
- * Defaults to the first account at index 0.\r
- *\r
- * The returned object will have keys corresponding with the requested range\r
- * of account indexes. The value of each key will be the Account derived for\r
- * that index in the wallet.\r
- *\r
- * ```\r
- * console.log(await wallet.accounts(5))\r
- * // outputs sixth account of the wallet\r
- * // {\r
- * // 5: {\r
- * // privateKey: <...>,\r
- * // index: 5\r
- * // }\r
- * // }\r
- * ```\r
- *\r
- * @param {number} from - Start index of secret keys. Default: 0\r
- * @param {number} to - End index of secret keys. Default: `from`\r
- * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts\r
- */\r
- async accounts (from: number = 0, to: number = from): Promise<AccountList> {\r
- if (from > to) {\r
- const swap = from\r
- from = to\r
- to = swap\r
- }\r
- const output = new AccountList()\r
- const indexes: number[] = []\r
- for (let i = from; i <= to; i++) {\r
- if (this.#accounts[i] == null) {\r
- indexes.push(i)\r
- } else {\r
- output[i] = this.#accounts[i]\r
- }\r
- }\r
- if (indexes.length > 0) {\r
- let results = await this.ckd(indexes)\r
- const data: any = []\r
- results.forEach(r => data.push({\r
- method: 'convert',\r
- privateKey: r.privateKey,\r
- index: r.index\r
- }))\r
- const keypairs: KeyPair[] = await this.#poolNanoNacl.assign(data)\r
- for (const keypair of keypairs) {\r
- if (keypair.privateKey == null) throw new RangeError('Account private key missing')\r
- if (keypair.publicKey == null) throw new RangeError('Account public key missing')\r
- if (keypair.index == null) throw new RangeError('Account keys derived but index missing')\r
- const { privateKey, index } = keypair\r
- output[keypair.index] = Account.fromPrivateKey(privateKey, index)\r
- this.#accounts[keypair.index] = output[keypair.index]\r
- }\r
- }\r
- return output\r
- }\r
-\r
- /**\r
- * Fetches the lowest-indexed unopened account from a wallet in sequential\r
- * order. An account is unopened if it has no frontier block.\r
- *\r
- * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
- * @param {number} batchSize - Number of accounts to fetch and check per RPC callout\r
- * @param {number} from - Account index from which to start the search\r
- * @returns {Promise<Account>} The lowest-indexed unopened account belonging to the wallet\r
- */\r
- async getNextNewAccount (rpc: Rpc, batchSize: number = ADDRESS_GAP, from: number = 0): Promise<Account> {\r
- if (!Number.isSafeInteger(batchSize) || batchSize < 1) {\r
- throw new RangeError(`Invalid batch size ${batchSize}`)\r
- }\r
- const accounts = await this.accounts(from, from + batchSize - 1)\r
- const addresses = []\r
- for (const a in accounts) {\r
- addresses.push(accounts[a].address)\r
- }\r
- const data = {\r
- "accounts": addresses\r
- }\r
- const { errors } = await rpc.call('accounts_frontiers', data)\r
- for (const key of Object.keys(errors ?? {})) {\r
- const value = errors[key]\r
- if (value === 'Account not found') {\r
- return Account.fromAddress(key)\r
- }\r
- }\r
- return await this.getNextNewAccount(rpc, batchSize, from + batchSize)\r
- }\r
-\r
- /**\r
- * Refreshes wallet account balances, frontiers, and representatives from the\r
- * current state on the network.\r
- *\r
- * A successful response will set these properties on each account.\r
- *\r
- * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
- * @returns {Promise<Account[]>} Accounts with updated balances, frontiers, and representatives\r
- */\r
- async refresh (rpc: Rpc | string | URL, from: number = 0, to: number = from): Promise<AccountList> {\r
- if (typeof rpc === 'string' || rpc.constructor === URL) {\r
- rpc = new Rpc(rpc)\r
- }\r
- if (rpc.constructor !== Rpc) {\r
- throw new TypeError('RPC must be a valid node')\r
- }\r
- const accounts = await this.accounts(from, to)\r
- for (const a in accounts) {\r
- try {\r
- await accounts[a].refresh(rpc)\r
- } catch (err) {\r
- delete accounts[a]\r
- }\r
- }\r
- return accounts\r
- }\r
-\r
- /**\r
- * Locks the wallet with a password that will be needed to unlock it later.\r
- *\r
- * @param {string} password Used to lock the wallet\r
- * @returns True if successfully locked\r
- */\r
- async lock (password: string): Promise<boolean>\r
- /**\r
- * Locks the wallet with a CryptoKey that will be needed to unlock it later.\r
- *\r
- * @param {CryptoKey} key Used to lock the wallet\r
- * @returns True if successfully locked\r
- */\r
- async lock (key: CryptoKey): Promise<boolean>\r
- async lock (passkey: string | CryptoKey): Promise<boolean> {\r
- let success = true\r
- try {\r
- const data: { id: string, mnemonic: string | null, seed: string | null } = {\r
- id: this.id,\r
- mnemonic: null,\r
- seed: null\r
- }\r
- if (this.#mnemonic instanceof Bip39Mnemonic) {\r
- data.mnemonic = this.#mnemonic.phrase\r
- }\r
- if (typeof this.#seed === 'string') {\r
- data.seed = this.#seed\r
- }\r
- success &&= await this.#safe.put(this.id, passkey as string, data)\r
- const promises = []\r
- for (const account of this.#accounts) {\r
- promises.push(account.lock(passkey as string))\r
- }\r
- await Promise.all(promises)\r
- if (!success) {\r
- throw null\r
- }\r
- } catch (err) {\r
- throw new Error('Failed to lock wallet')\r
- }\r
- this.#locked = true\r
- this.#mnemonic = null\r
- this.#seed = null\r
- return true\r
- }\r
-\r
- /**\r
- * Unlocks the wallet using the same password as used prior to lock it.\r
- *\r
- * @param {string} password Used previously to lock the wallet\r
- * @returns True if successfully unlocked\r
- */\r
- async unlock (password: string): Promise<boolean>\r
- /**\r
- * Unlocks the wallet using the same CryptoKey as used prior to lock it.\r
- *\r
- * @param {CryptoKey} key Used previously to lock the wallet\r
- * @returns True if successfully unlocked\r
- */\r
- async unlock (key: CryptoKey): Promise<boolean>\r
- async unlock (passkey: string | CryptoKey): Promise<boolean> {\r
- try {\r
- const { id, mnemonic, seed } = await this.#safe.get(this.id, passkey as string)\r
- if (id !== this.id) {\r
- throw null\r
- }\r
- const promises = []\r
- for (const account of this.#accounts) {\r
- promises.push(account.unlock(passkey as string))\r
- }\r
- await Promise.all(promises)\r
- if (mnemonic != null) {\r
- this.#mnemonic = await Bip39Mnemonic.fromPhrase(mnemonic)\r
- }\r
- if (seed != null) {\r
- this.#seed = seed\r
- }\r
- this.#locked = false\r
- } catch (err) {\r
- throw new Error('Failed to unlock wallet')\r
- }\r
- return true\r
- }\r
-}\r
// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
// SPDX-License-Identifier: GPL-3.0-or-later\r
\r
-import { KeyPair, Wallet } from '.'\r
+import { KeyPair, Wallet } from './wallet'\r
import { Entropy } from '#src/lib/entropy.js'\r
import { Ledger } from '#src/lib/ledger.js'\r
\r
--- /dev/null
+// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>\r
+// SPDX-License-Identifier: GPL-3.0-or-later\r
+\r
+import { Account, AccountList } from '#src/lib/account.js'\r
+import { Bip39Mnemonic } from '#src/lib/bip39-mnemonic.js'\r
+import { ADDRESS_GAP } from '#src/lib/constants.js'\r
+import { Entropy } from '#src/lib/entropy.js'\r
+import { Pool } from '#src/lib/pool.js'\r
+import { Rpc } from '#src/lib/rpc.js'\r
+import { NanoNaClWorker, SafeWorker } from '#workers'\r
+import { utf8 } from '../convert'\r
+\r
+export type KeyPair = {\r
+ publicKey?: string,\r
+ privateKey?: string,\r
+ index?: number\r
+}\r
+\r
+/**\r
+* Represents a wallet containing numerous Nano accounts derived from a single\r
+* source, the form of which can vary based on the type of wallet. The Wallet\r
+* class itself is abstract and cannot be directly instantiated. Currently, three\r
+* types of wallets are supported, each as a derived class: Bip44Wallet,\r
+* Blake2bWallet, LedgerWallet.\r
+*/\r
+export abstract class Wallet {\r
+ #accounts: AccountList\r
+ #id: Entropy\r
+ #locked: boolean = true\r
+ #mnemonic: Bip39Mnemonic | null\r
+ #poolNanoNacl: Pool\r
+ #poolSafe: Pool\r
+ #seed: string | null\r
+ get id () { return this.#id.hex }\r
+ get isLocked () { return this.#locked }\r
+ get isUnlocked () { return !this.#locked }\r
+ get mnemonic () {\r
+ if (this.#mnemonic instanceof Bip39Mnemonic) {\r
+ return this.#mnemonic.phrase\r
+ }\r
+ return ''\r
+ }\r
+ get seed () {\r
+ if (typeof this.#seed === 'string') {\r
+ return this.#seed\r
+ }\r
+ return ''\r
+ }\r
+\r
+ abstract ckd (index: number[]): Promise<KeyPair[]>\r
+\r
+ constructor (id: Entropy, seed?: string, mnemonic?: Bip39Mnemonic) {\r
+ if (this.constructor === Wallet) {\r
+ throw new Error('Wallet is an abstract class and cannot be instantiated directly.')\r
+ }\r
+ this.#accounts = new AccountList()\r
+ this.#id = id\r
+ this.#mnemonic = mnemonic ?? null\r
+ this.#poolNanoNacl = new Pool(NanoNaClWorker)\r
+ this.#poolSafe = new Pool(SafeWorker)\r
+ console.log(SafeWorker)\r
+ this.#seed = seed ?? null\r
+ }\r
+\r
+ /**\r
+ * Removes encrypted secrets in storage and releases variable references to\r
+ * allow garbage collection.\r
+ */\r
+ destroy (): void {\r
+ let i = 0\r
+ for (const a in this.#accounts) {\r
+ this.#accounts[a].destroy()\r
+ delete this.#accounts[a]\r
+ i++\r
+ }\r
+ this.#mnemonic = null\r
+ this.#seed = null\r
+ this.#poolNanoNacl.terminate()\r
+ this.#poolSafe.assign({\r
+ method: 'destroy',\r
+ name: this.id\r
+ }).finally(this.#poolSafe.terminate)\r
+ }\r
+\r
+ /**\r
+ * Retrieves an account from a wallet using its child key derivation function.\r
+ * Defaults to the first account at index 0.\r
+ *\r
+ * ```\r
+ * console.log(await wallet.account(5))\r
+ * // outputs sixth account of the wallet\r
+ * // {\r
+ * // privateKey: <...>,\r
+ * // index: 5\r
+ * // }\r
+ * ```\r
+ *\r
+ * @param {number} index - Wallet index of secret key. Default: 0\r
+ * @returns {Account} Account derived at the specified wallet index\r
+ */\r
+ async account (index: number = 0): Promise<Account> {\r
+ return (await this.accounts(index))[index]\r
+ }\r
+\r
+ /**\r
+ * Retrieves accounts from a wallet using its child key derivation function.\r
+ * Defaults to the first account at index 0.\r
+ *\r
+ * The returned object will have keys corresponding with the requested range\r
+ * of account indexes. The value of each key will be the Account derived for\r
+ * that index in the wallet.\r
+ *\r
+ * ```\r
+ * console.log(await wallet.accounts(5))\r
+ * // outputs sixth account of the wallet\r
+ * // {\r
+ * // 5: {\r
+ * // privateKey: <...>,\r
+ * // index: 5\r
+ * // }\r
+ * // }\r
+ * ```\r
+ *\r
+ * @param {number} from - Start index of secret keys. Default: 0\r
+ * @param {number} to - End index of secret keys. Default: `from`\r
+ * @returns {AccountList} Object with keys of account indexes and values of the corresponding Accounts\r
+ */\r
+ async accounts (from: number = 0, to: number = from): Promise<AccountList> {\r
+ if (from > to) {\r
+ const swap = from\r
+ from = to\r
+ to = swap\r
+ }\r
+ const output = new AccountList()\r
+ const indexes: number[] = []\r
+ for (let i = from; i <= to; i++) {\r
+ if (this.#accounts[i] == null) {\r
+ indexes.push(i)\r
+ } else {\r
+ output[i] = this.#accounts[i]\r
+ }\r
+ }\r
+ if (indexes.length > 0) {\r
+ let results = await this.ckd(indexes)\r
+ const data: any = []\r
+ results.forEach(r => data.push({\r
+ method: 'convert',\r
+ privateKey: r.privateKey,\r
+ index: r.index\r
+ }))\r
+ const keypairs: KeyPair[] = await this.#poolNanoNacl.assign(data)\r
+ for (const keypair of keypairs) {\r
+ if (keypair.privateKey == null) throw new RangeError('Account private key missing')\r
+ if (keypair.publicKey == null) throw new RangeError('Account public key missing')\r
+ if (keypair.index == null) throw new RangeError('Account keys derived but index missing')\r
+ const { privateKey, index } = keypair\r
+ output[keypair.index] = Account.fromPrivateKey(privateKey, index)\r
+ this.#accounts[keypair.index] = output[keypair.index]\r
+ }\r
+ }\r
+ return output\r
+ }\r
+\r
+ /**\r
+ * Fetches the lowest-indexed unopened account from a wallet in sequential\r
+ * order. An account is unopened if it has no frontier block.\r
+ *\r
+ * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
+ * @param {number} batchSize - Number of accounts to fetch and check per RPC callout\r
+ * @param {number} from - Account index from which to start the search\r
+ * @returns {Promise<Account>} The lowest-indexed unopened account belonging to the wallet\r
+ */\r
+ async getNextNewAccount (rpc: Rpc, batchSize: number = ADDRESS_GAP, from: number = 0): Promise<Account> {\r
+ if (!Number.isSafeInteger(batchSize) || batchSize < 1) {\r
+ throw new RangeError(`Invalid batch size ${batchSize}`)\r
+ }\r
+ const accounts = await this.accounts(from, from + batchSize - 1)\r
+ const addresses = []\r
+ for (const a in accounts) {\r
+ addresses.push(accounts[a].address)\r
+ }\r
+ const data = {\r
+ "accounts": addresses\r
+ }\r
+ const { errors } = await rpc.call('accounts_frontiers', data)\r
+ for (const key of Object.keys(errors ?? {})) {\r
+ const value = errors[key]\r
+ if (value === 'Account not found') {\r
+ return Account.fromAddress(key)\r
+ }\r
+ }\r
+ return await this.getNextNewAccount(rpc, batchSize, from + batchSize)\r
+ }\r
+\r
+ /**\r
+ * Refreshes wallet account balances, frontiers, and representatives from the\r
+ * current state on the network.\r
+ *\r
+ * A successful response will set these properties on each account.\r
+ *\r
+ * @param {Rpc|string|URL} rpc - RPC node information required to refresh accounts, calculate PoW, and process blocks\r
+ * @returns {Promise<Account[]>} Accounts with updated balances, frontiers, and representatives\r
+ */\r
+ async refresh (rpc: Rpc | string | URL, from: number = 0, to: number = from): Promise<AccountList> {\r
+ if (typeof rpc === 'string' || rpc.constructor === URL) {\r
+ rpc = new Rpc(rpc)\r
+ }\r
+ if (rpc.constructor !== Rpc) {\r
+ throw new TypeError('RPC must be a valid node')\r
+ }\r
+ const accounts = await this.accounts(from, to)\r
+ for (const a in accounts) {\r
+ try {\r
+ await accounts[a].refresh(rpc)\r
+ } catch (err) {\r
+ delete accounts[a]\r
+ }\r
+ }\r
+ return accounts\r
+ }\r
+\r
+ /**\r
+ * Locks the wallet with a password that will be needed to unlock it later.\r
+ *\r
+ * @param {(string|Uint8Array)} password Used to lock the wallet\r
+ * @returns True if successfully locked\r
+ */\r
+ async lock (password: string | Uint8Array): Promise<boolean> {\r
+ if (typeof password === 'string') {\r
+ password = utf8.toBytes(password)\r
+ }\r
+ let success = true\r
+ try {\r
+ const data: { id: string, mnemonic: string | null, seed: string | null } = {\r
+ id: this.id,\r
+ mnemonic: null,\r
+ seed: null\r
+ }\r
+ if (this.#mnemonic instanceof Bip39Mnemonic) {\r
+ data.mnemonic = this.#mnemonic.phrase\r
+ }\r
+ if (typeof this.#seed === 'string') {\r
+ data.seed = this.#seed\r
+ }\r
+ success &&= await this.#poolSafe.assign({\r
+ method: 'put',\r
+ name: this.id,\r
+ password,\r
+ data\r
+ })\r
+ const promises = []\r
+ for (const account of this.#accounts) {\r
+ promises.push(account.lock(password))\r
+ }\r
+ await Promise.all(promises)\r
+ password.fill(0)\r
+ if (!success) {\r
+ throw null\r
+ }\r
+ } catch (err) {\r
+ throw new Error('Failed to lock wallet')\r
+ }\r
+ this.#locked = true\r
+ this.#mnemonic = null\r
+ this.#seed = null\r
+ return true\r
+ }\r
+\r
+ /**\r
+ * Unlocks the wallet using the same password as used prior to lock it.\r
+ *\r
+ * @param {(string|Uint8Array)} password Used previously to lock the wallet\r
+ * @returns True if successfully unlocked\r
+ */\r
+ async unlock (password: string | Uint8Array): Promise<boolean> {\r
+ if (typeof password === 'string') {\r
+ password = utf8.toBytes(password)\r
+ }\r
+ try {\r
+ const { id, mnemonic, seed } = await this.#poolSafe.assign({\r
+ method: 'get',\r
+ name: this.id,\r
+ password\r
+ })\r
+ if (id !== this.id) {\r
+ throw null\r
+ }\r
+ const promises = []\r
+ for (const account of this.#accounts) {\r
+ promises.push(account.unlock(password))\r
+ }\r
+ await Promise.all(promises)\r
+ password.fill(0)\r
+ if (mnemonic != null) {\r
+ this.#mnemonic = await Bip39Mnemonic.fromPhrase(mnemonic)\r
+ }\r
+ if (seed != null) {\r
+ this.#seed = seed\r
+ }\r
+ this.#locked = false\r
+ } catch (err) {\r
+ throw new Error('Failed to unlock wallet')\r
+ }\r
+ return true\r
+ }\r
+}\r
// SPDX-License-Identifier: GPL-3.0-or-later
import { default as Bip44CkdWorker, Bip44Ckd } from './bip44-ckd'
import { default as NanoNaClWorker, NanoNaCl } from './nano-nacl'
+import { default as SafeWorker, Safe } from './safe'
export {
Bip44Ckd,
Bip44CkdWorker,
NanoNaCl,
- NanoNaClWorker
+ NanoNaClWorker,
+ Safe,
+ SafeWorker
}
--- /dev/null
+// SPDX-FileCopyrightText: 2025 Chris Duncan <chris@zoso.dev>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+'use strict'
+
+import { buffer, hex, obj, utf8, default as Convert } from '#src/lib/convert.js'
+import { Entropy } from '#src/lib/entropy.js'
+import { WorkerInterface } from '#src/lib/pool.js'
+
+type SafeInput = {
+ method: string
+ name: string
+ password?: { [key: number]: number }
+ data?: any
+}
+
+type SafeOutput = {
+ method: string
+ name: string
+ result: any
+}
+
+const { subtle } = globalThis.crypto
+const ERR_MSG = 'Failed to store item in Safe'
+
+export class Safe extends WorkerInterface {
+ static #storage: Storage = globalThis.sessionStorage
+
+ static {
+ Safe.listen()
+ }
+
+ static async work (data: any[]): Promise<any[]> {
+ return new Promise(async (resolve, reject): Promise<void> => {
+ const results: SafeOutput[] = []
+ for (const d of data) {
+ console.log(d)
+ const { name, method, password, data } = d as SafeInput
+ console.log(globalThis.sessionStorage)
+ const backup = this.#storage.getItem(name)
+ let result
+ try {
+ const passwordBytes = obj.toBytes(password ?? [])
+ switch (d.method) {
+ case 'put': {
+ result = await this.put(name, passwordBytes, data)
+ break
+ }
+ case 'overwrite': {
+ result = await this.overwrite(name, passwordBytes, data)
+ break
+ }
+ case 'get': {
+ result = await this.get(name, passwordBytes)
+ break
+ }
+ case 'destroy': {
+ result = await this.destroy(name)
+ break
+ }
+ default: {
+ result = `unknown Safe method ${method}`
+ }
+ }
+ results.push({ name, method, result })
+ } catch (err) {
+ console.log(err)
+ if (backup != null) this.#storage.setItem(d.name, backup)
+ result = false
+ }
+ }
+ resolve(results)
+ })
+ }
+
+ /**
+ * Removes data from the Safe without decrypting.
+ */
+ static destroy (name: string): boolean {
+ try {
+ this.#storage.removeItem(name)
+ } catch (err) {
+ throw new Error(ERR_MSG)
+ }
+ return (this.#storage.getItem(name) == null)
+ }
+
+ /**
+ * Encrypts data with a password or CryptoKey and stores it in the Safe.
+ */
+ static async put (name: string, password: Uint8Array, data: any): Promise<boolean> {
+ if (this.#storage.getItem(name)) {
+ throw new Error(ERR_MSG)
+ }
+ return this.overwrite(name, password, data)
+ }
+
+ /**
+ * Encrypts data with a password as bytes and stores it in the Safe.
+ */
+ static async overwrite (name: string, password: Uint8Array, data: any): Promise<boolean> {
+ let passkey
+ try {
+ passkey = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
+ } catch {
+ throw new Error(ERR_MSG)
+ } finally {
+ password.fill(0)
+ }
+ if (this.#isInvalid(name, passkey, data)) {
+ throw new Error(ERR_MSG)
+ }
+
+ try {
+ const iv = await Entropy.create()
+ if (typeof data === 'bigint') {
+ data = data.toString()
+ }
+ data = JSON.stringify(data)
+ const derivationAlgorithm: Pbkdf2Params = {
+ name: 'PBKDF2',
+ hash: 'SHA-512',
+ salt: iv.bytes,
+ iterations: 210000
+ }
+ const derivedKeyType: AesKeyGenParams = {
+ name: 'AES-GCM',
+ length: 256
+ }
+ passkey = await subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['encrypt'])
+ const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, utf8.toBytes(data))
+ const record = {
+ encrypted: buffer.toHex(encrypted),
+ iv: iv.hex
+ }
+ this.#storage.setItem(name, JSON.stringify(record))
+ } catch (err) {
+ throw new Error(ERR_MSG)
+ }
+
+ return (this.#storage.getItem(name) != null)
+ }
+
+ /**
+ * Retrieves data from the Safe and decrypts data with a password as bytes.
+ */
+ static async get (name: string, password: Uint8Array): Promise<any> {
+ let passkey
+ try {
+ passkey = await subtle.importKey('raw', password, 'PBKDF2', false, ['deriveBits', 'deriveKey'])
+ } catch {
+ return null
+ } finally {
+ password.fill(0)
+ }
+ if (this.#isInvalid(name, passkey)) {
+ return null
+ }
+
+ const item = this.#storage.getItem(name)
+ if (item == null) {
+ return null
+ }
+ const record = JSON.parse(item)
+ const encrypted = hex.toBytes(record.encrypted)
+ const iv = await Entropy.import(record.iv)
+
+ try {
+ const derivationAlgorithm: Pbkdf2Params = {
+ name: 'PBKDF2',
+ hash: 'SHA-512',
+ salt: iv.bytes,
+ iterations: 210000
+ }
+ const derivedKeyType: AesKeyGenParams = {
+ name: 'AES-GCM',
+ length: 256
+ }
+ passkey = await subtle.deriveKey(derivationAlgorithm, passkey, derivedKeyType, false, ['decrypt'])
+ const decrypted = await subtle.decrypt({ name: 'AES-GCM', iv: iv.buffer }, passkey, encrypted)
+ const decoded = buffer.toUtf8(decrypted)
+ const data = JSON.parse(decoded)
+ this.#storage.removeItem(name)
+ return data
+ } catch (err) {
+ return null
+ }
+ }
+
+ static #isInvalid (name: string, passkey: CryptoKey, data?: any): boolean {
+ if (typeof name !== 'string' || name === '') {
+ return true
+ }
+ if (!(passkey instanceof CryptoKey)) {
+ return true
+ }
+ if (typeof data === 'object') {
+ try {
+ JSON.stringify(data)
+ } catch (err) {
+ return true
+ }
+ }
+ return false
+ }
+}
+
+export default `
+ ${Convert}
+ const Entropy = ${Entropy}
+ const WorkerInterface = ${WorkerInterface}
+ const Safe = ${Safe}
+`
import { SendBlock, ReceiveBlock, ChangeBlock } from './lib/block'
import { Rolodex } from './lib/rolodex'
import { Rpc } from './lib/rpc'
-import { Safe } from './lib/safe'
import { Tools } from './lib/tools'
import { Bip44Wallet, Blake2bWallet, LedgerWallet } from './lib/wallets'
-export { Account, Blake2b, SendBlock, ReceiveBlock, ChangeBlock, Rpc, Rolodex, Safe, Tools, Bip44Wallet, Blake2bWallet, LedgerWallet }
+export { Account, Blake2b, SendBlock, ReceiveBlock, ChangeBlock, Rpc, Rolodex, Tools, Bip44Wallet, Blake2bWallet, LedgerWallet }