Files
sattle/src/lnurlcash/walletMaterialStorage.ts
T

167 lines
5.5 KiB
TypeScript

// 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)
}