mirror of
https://github.com/tompro/sattle.git
synced 2026-10-26 01:49:58 +00:00
167 lines
5.5 KiB
TypeScript
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)
|
|
}
|