mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
feat(keys): persist BIP-32 wallet material
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// The v2 wallet-material repository owns one authenticated serialization under
|
||||
// a new namespace. Owner markers remain claims on restore and become trusted
|
||||
// only after plaintext or password proof reproduces the linking-key owner.
|
||||
|
||||
import {
|
||||
parseStoredWalletMaterial,
|
||||
parseWalletMaterial,
|
||||
serializeWalletMaterial,
|
||||
stampStoredWalletMaterialOwner,
|
||||
storedWalletMaterialClaimedOwnerId,
|
||||
storedWalletMaterialOwnerId,
|
||||
stripStoredSecretOwner,
|
||||
walletMaterialOwnerId,
|
||||
STORED_WALLET_MATERIAL_VERSION,
|
||||
} from './storage/storedSecret'
|
||||
import type {StoredWalletMaterial, WalletMaterialV2} from './storage/storedSecret'
|
||||
import {decryptSecretParts, encryptSecretParts} from './passwordWrap'
|
||||
import {WALLET_MATERIAL_STORAGE_KEY} from './storage/walletOwnerEvents'
|
||||
|
||||
export class InvalidWalletMaterialError extends Error {
|
||||
override readonly name = 'InvalidWalletMaterialError'
|
||||
|
||||
constructor() {
|
||||
super('Saved plaintext is not valid v2 wallet material.')
|
||||
}
|
||||
}
|
||||
|
||||
export class WalletMaterialOwnerMismatchError extends Error {
|
||||
override readonly name = 'WalletMaterialOwnerMismatchError'
|
||||
|
||||
constructor() {
|
||||
super('Saved wallet material is owned by a different wallet.')
|
||||
}
|
||||
}
|
||||
|
||||
export class WalletMaterialProofMismatchError extends Error {
|
||||
override readonly name = 'WalletMaterialProofMismatchError'
|
||||
|
||||
constructor() {
|
||||
super('Proven material does not match the saved wallet material.')
|
||||
}
|
||||
}
|
||||
|
||||
export class NoEncryptedWalletMaterialError extends Error {
|
||||
override readonly name = 'NoEncryptedWalletMaterialError'
|
||||
|
||||
constructor() {
|
||||
super('No encrypted wallet material saved.')
|
||||
}
|
||||
}
|
||||
|
||||
const readStoredWalletMaterial = (): StoredWalletMaterial | null => {
|
||||
const raw = localStorage.getItem(WALLET_MATERIAL_STORAGE_KEY)
|
||||
if (raw === null) return null
|
||||
let value: unknown
|
||||
try {
|
||||
value = JSON.parse(raw)
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) return null
|
||||
throw error
|
||||
}
|
||||
return parseStoredWalletMaterial(value)?.secret ?? null
|
||||
}
|
||||
|
||||
const materialFromPlaintext = (stored: StoredWalletMaterial): WalletMaterialV2 | null => {
|
||||
if (stored.enc) return null
|
||||
const material = parseWalletMaterial(stored.value)
|
||||
if (material === null) return null
|
||||
const claim = storedWalletMaterialClaimedOwnerId(stored)
|
||||
return claim === null || claim === walletMaterialOwnerId(material) ? material : null
|
||||
}
|
||||
|
||||
export const savedWalletMaterialExists = (): boolean => readStoredWalletMaterial() !== null
|
||||
|
||||
export const savedWalletMaterialIsEncrypted = (): boolean =>
|
||||
readStoredWalletMaterial()?.enc === true
|
||||
|
||||
export const getSavedWalletMaterialStored = (): StoredWalletMaterial | null =>
|
||||
readStoredWalletMaterial()
|
||||
|
||||
export const savedWalletMaterialOwnerId = (): string | null => {
|
||||
const stored = readStoredWalletMaterial()
|
||||
return stored === null ? null : storedWalletMaterialOwnerId(stored)
|
||||
}
|
||||
|
||||
export const savedWalletMaterialOwnerMatches = (material: WalletMaterialV2): boolean =>
|
||||
savedWalletMaterialOwnerId() === walletMaterialOwnerId(material)
|
||||
|
||||
export const getPlainWalletMaterial = (): WalletMaterialV2 | null => {
|
||||
const stored = readStoredWalletMaterial()
|
||||
return stored === null ? null : materialFromPlaintext(stored)
|
||||
}
|
||||
|
||||
export const saveWalletMaterial = async (
|
||||
material: WalletMaterialV2,
|
||||
password?: string,
|
||||
): Promise<void> => {
|
||||
const value = serializeWalletMaterial(material)
|
||||
const ownerId = walletMaterialOwnerId(material)
|
||||
if (!password) {
|
||||
localStorage.setItem(
|
||||
WALLET_MATERIAL_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
enc: false,
|
||||
value,
|
||||
ownerId,
|
||||
version: STORED_WALLET_MATERIAL_VERSION,
|
||||
}),
|
||||
)
|
||||
return
|
||||
}
|
||||
const parts = await encryptSecretParts(value, password)
|
||||
localStorage.setItem(
|
||||
WALLET_MATERIAL_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
enc: true,
|
||||
...parts,
|
||||
ownerId,
|
||||
version: STORED_WALLET_MATERIAL_VERSION,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export const restoreWalletMaterialStored = (stored: StoredWalletMaterial): void => {
|
||||
if (parseStoredWalletMaterial(stored) === null) throw new InvalidWalletMaterialError()
|
||||
localStorage.setItem(
|
||||
WALLET_MATERIAL_STORAGE_KEY,
|
||||
JSON.stringify(stripStoredSecretOwner(stored)),
|
||||
)
|
||||
}
|
||||
|
||||
export const decryptSavedWalletMaterial = async (
|
||||
password: string,
|
||||
): Promise<WalletMaterialV2> => {
|
||||
const stored = readStoredWalletMaterial()
|
||||
if (stored === null || !stored.enc) throw new NoEncryptedWalletMaterialError()
|
||||
const material = parseWalletMaterial(await decryptSecretParts(stored, password))
|
||||
if (material === null) throw new InvalidWalletMaterialError()
|
||||
const claimedOwnerId = storedWalletMaterialClaimedOwnerId(stored)
|
||||
if (claimedOwnerId !== null && claimedOwnerId !== walletMaterialOwnerId(material)) {
|
||||
throw new WalletMaterialOwnerMismatchError()
|
||||
}
|
||||
return material
|
||||
}
|
||||
|
||||
export const ensureSavedWalletMaterialOwner = (material: WalletMaterialV2): void => {
|
||||
const stored = readStoredWalletMaterial()
|
||||
if (stored === null) return
|
||||
if (stored.enc === false && stored.value !== serializeWalletMaterial(material)) {
|
||||
throw new WalletMaterialProofMismatchError()
|
||||
}
|
||||
const ownerId = walletMaterialOwnerId(material)
|
||||
const claimedOwnerId = storedWalletMaterialClaimedOwnerId(stored)
|
||||
if (storedWalletMaterialOwnerId(stored) === ownerId) return
|
||||
if (claimedOwnerId !== null && claimedOwnerId !== ownerId) {
|
||||
throw new WalletMaterialOwnerMismatchError()
|
||||
}
|
||||
localStorage.setItem(
|
||||
WALLET_MATERIAL_STORAGE_KEY,
|
||||
JSON.stringify(stampStoredWalletMaterialOwner(stored, ownerId)),
|
||||
)
|
||||
}
|
||||
|
||||
export const clearSavedWalletMaterial = (): void => {
|
||||
localStorage.removeItem(WALLET_MATERIAL_STORAGE_KEY)
|
||||
}
|
||||
Reference in New Issue
Block a user