Formatting

This commit is contained in:
2026-09-05 18:30:35 +02:00
parent 454a9db1d5
commit 4caa075052
133 changed files with 8260 additions and 8181 deletions
+91 -89
View File
@@ -1,6 +1,6 @@
import {sha256} from '@noble/hashes/sha2.js'
import {bytesToHex, utf8ToBytes} from '@noble/hashes/utils.js'
import {describe, expect, it} from 'vitest'
import { sha256 } from '@noble/hashes/sha2.js';
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
import { describe, expect, it } from 'vitest';
import {
decryptSavedWalletMaterial,
@@ -17,77 +17,79 @@ import {
saveWalletMaterial,
serializeWalletMaterial,
walletMaterialLinkingKey,
} from './keys'
import {parseJsonObject, stubLocalStorage} from './test-utils'
} from './keys';
import { parseJsonObject, stubLocalStorage } from './test-utils';
const FIXED_MNEMONIC =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
const PASSWORD = 'correct horse battery staple'
const MATERIAL_STORAGE_KEY = 'sattle_wallet_material_v2'
const LEGACY_STORAGE_KEY = 'sattle_linking_key'
const LINKING_KEY_HEX = 'da2a752d3d28668288ff50bed644e4a95f726d8711fd8754060f0a9e378f84aa'
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
const PASSWORD = 'correct horse battery staple';
const MATERIAL_STORAGE_KEY = 'sattle_wallet_material_v2';
const LEGACY_STORAGE_KEY = 'sattle_linking_key';
const LINKING_KEY_HEX = 'da2a752d3d28668288ff50bed644e4a95f726d8711fd8754060f0a9e378f84aa';
const CASH_ROOT_HEX =
'c7a2496e9b453a67c5d2a1f04936ec1259440d45454c795a99a66269e4cd3005111e1cc966fca2fe32f054f14caceab90449e536d94cf6935ea12a087e414f60'
'c7a2496e9b453a67c5d2a1f04936ec1259440d45454c795a99a66269e4cd3005111e1cc966fca2fe32f054f14caceab90449e536d94cf6935ea12a087e414f60';
const materialHash = (serialized: string): string =>
bytesToHex(sha256(utf8ToBytes(serialized)))
const materialHash = (serialized: string): string => bytesToHex(sha256(utf8ToBytes(serialized)));
const readRawMaterialRecord = (): Record<string, unknown> => {
const raw = localStorage.getItem(MATERIAL_STORAGE_KEY)
if (raw === null) throw new Error('expected a saved wallet-material record')
return parseJsonObject(raw)
}
const raw = localStorage.getItem(MATERIAL_STORAGE_KEY);
if (raw === null) throw new Error('expected a saved wallet-material record');
return parseJsonObject(raw);
};
describe('WalletMaterialV2 persistence', () => {
it('round-trips canonical plaintext material in the new namespace', async () => {
// Given complete wallet material and an unrelated legacy record
stubLocalStorage()
localStorage.setItem(LEGACY_STORAGE_KEY, JSON.stringify({enc: false, value: '07'.repeat(32)}))
const material = deriveWalletMaterial(FIXED_MNEMONIC)
stubLocalStorage();
localStorage.setItem(
LEGACY_STORAGE_KEY,
JSON.stringify({ enc: false, value: '07'.repeat(32) }),
);
const material = deriveWalletMaterial(FIXED_MNEMONIC);
// When the material is saved without a password
await saveWalletMaterial(material)
await saveWalletMaterial(material);
// Then only the new namespace carries the exact complete material
expect(savedWalletMaterialExists()).toBe(true)
expect(savedWalletMaterialIsEncrypted()).toBe(false)
expect(getPlainWalletMaterial()).toEqual(material)
expect(savedWalletMaterialExists()).toBe(true);
expect(savedWalletMaterialIsEncrypted()).toBe(false);
expect(getPlainWalletMaterial()).toEqual(material);
expect(readRawMaterialRecord()).toEqual({
enc: false,
value: serializeWalletMaterial(material),
materialHash: materialHash(serializeWalletMaterial(material)),
ownerId: linkingPubKeyHex(walletMaterialLinkingKey(material)),
version: 2,
})
expect(localStorage.getItem(LEGACY_STORAGE_KEY)).not.toBeNull()
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).not.toContain(FIXED_MNEMONIC)
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).not.toContain('"seed"')
})
});
expect(localStorage.getItem(LEGACY_STORAGE_KEY)).not.toBeNull();
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).not.toContain(FIXED_MNEMONIC);
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).not.toContain('"seed"');
});
it('round-trips canonical password-encrypted material without plaintext fields', async () => {
// Given complete wallet material
stubLocalStorage()
const material = deriveWalletMaterial(FIXED_MNEMONIC)
stubLocalStorage();
const material = deriveWalletMaterial(FIXED_MNEMONIC);
// When the material is password protected
await saveWalletMaterial(material, PASSWORD)
await saveWalletMaterial(material, PASSWORD);
// Then only authenticated decryption returns both parts
expect(savedWalletMaterialIsEncrypted()).toBe(true)
expect(getPlainWalletMaterial()).toBeNull()
expect(await decryptSavedWalletMaterial(PASSWORD)).toEqual(material)
await expect(decryptSavedWalletMaterial('wrong password')).rejects.toThrow()
const raw = JSON.stringify(readRawMaterialRecord())
expect(raw).not.toContain(LINKING_KEY_HEX)
expect(raw).not.toContain(CASH_ROOT_HEX)
expect(raw).not.toContain(FIXED_MNEMONIC)
})
expect(savedWalletMaterialIsEncrypted()).toBe(true);
expect(getPlainWalletMaterial()).toBeNull();
expect(await decryptSavedWalletMaterial(PASSWORD)).toEqual(material);
await expect(decryptSavedWalletMaterial('wrong password')).rejects.toThrow();
const raw = JSON.stringify(readRawMaterialRecord());
expect(raw).not.toContain(LINKING_KEY_HEX);
expect(raw).not.toContain(CASH_ROOT_HEX);
expect(raw).not.toContain(FIXED_MNEMONIC);
});
it('strips restored owner claims until the material proves its real owner', () => {
// Given plaintext material carrying a foreign owner claim
stubLocalStorage()
const material = deriveWalletMaterial(FIXED_MNEMONIC)
const foreignOwner = linkingPubKeyHex(new Uint8Array(32).fill(9))
stubLocalStorage();
const material = deriveWalletMaterial(FIXED_MNEMONIC);
const foreignOwner = linkingPubKeyHex(new Uint8Array(32).fill(9));
// When the record is restored and subsequently proven by plaintext unlock
restoreWalletMaterialStored({
@@ -96,25 +98,25 @@ describe('WalletMaterialV2 persistence', () => {
materialHash: materialHash(serializeWalletMaterial(material)),
ownerId: foreignOwner,
version: 2,
})
const beforeProof = readRawMaterialRecord()
const unlocked = getPlainWalletMaterial()
if (unlocked === null) throw new Error('expected restored plaintext material')
ensureSavedWalletMaterialOwner(unlocked)
});
const beforeProof = readRawMaterialRecord();
const unlocked = getPlainWalletMaterial();
if (unlocked === null) throw new Error('expected restored plaintext material');
ensureSavedWalletMaterialOwner(unlocked);
// Then the claim was absent before proof and only the derived owner was stamped
expect(beforeProof).toEqual({
enc: false,
value: serializeWalletMaterial(material),
materialHash: materialHash(serializeWalletMaterial(material)),
})
expect(savedWalletMaterialOwnerId()).toBe(linkingPubKeyHex(walletMaterialLinkingKey(material)))
})
});
expect(savedWalletMaterialOwnerId()).toBe(linkingPubKeyHex(walletMaterialLinkingKey(material)));
});
it('rejects a plaintext owner mismatch without returning or stamping material', () => {
// Given canonical material stamped with a foreign current owner
stubLocalStorage()
const material = deriveWalletMaterial(FIXED_MNEMONIC)
stubLocalStorage();
const material = deriveWalletMaterial(FIXED_MNEMONIC);
localStorage.setItem(
MATERIAL_STORAGE_KEY,
JSON.stringify({
@@ -124,72 +126,72 @@ describe('WalletMaterialV2 persistence', () => {
ownerId: linkingPubKeyHex(new Uint8Array(32).fill(9)),
version: 2,
}),
)
const before = localStorage.getItem(MATERIAL_STORAGE_KEY)
);
const before = localStorage.getItem(MATERIAL_STORAGE_KEY);
// When it is read or offered for proof
const unlocked = getPlainWalletMaterial()
const stamp = () => ensureSavedWalletMaterialOwner(material)
const unlocked = getPlainWalletMaterial();
const stamp = () => ensureSavedWalletMaterialOwner(material);
// Then neither operation trusts or rewrites the foreign record
expect(unlocked).toBeNull()
expect(stamp).toThrow('different wallet')
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).toBe(before)
})
expect(unlocked).toBeNull();
expect(stamp).toThrow('different wallet');
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).toBe(before);
});
it('rejects an encrypted owner mismatch after authentication', async () => {
// Given valid encrypted material whose owner marker was replaced
stubLocalStorage()
const material = deriveWalletMaterial(FIXED_MNEMONIC)
await saveWalletMaterial(material, PASSWORD)
stubLocalStorage();
const material = deriveWalletMaterial(FIXED_MNEMONIC);
await saveWalletMaterial(material, PASSWORD);
const foreign = {
...readRawMaterialRecord(),
ownerId: linkingPubKeyHex(new Uint8Array(32).fill(9)),
}
localStorage.setItem(MATERIAL_STORAGE_KEY, JSON.stringify(foreign))
const before = localStorage.getItem(MATERIAL_STORAGE_KEY)
};
localStorage.setItem(MATERIAL_STORAGE_KEY, JSON.stringify(foreign));
const before = localStorage.getItem(MATERIAL_STORAGE_KEY);
// When the authenticated material contradicts the current owner marker
const unlock = decryptSavedWalletMaterial(PASSWORD)
const unlock = decryptSavedWalletMaterial(PASSWORD);
// Then no material is returned or marker changed
await expect(unlock).rejects.toThrow('different wallet')
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).toBe(before)
})
await expect(unlock).rejects.toThrow('different wallet');
expect(localStorage.getItem(MATERIAL_STORAGE_KEY)).toBe(before);
});
it.each([
['legacy key-only plaintext', LINKING_KEY_HEX, 2],
['wrong outer version', serializeWalletMaterial(deriveWalletMaterial(FIXED_MNEMONIC)), 1],
[
'malformed material plaintext',
JSON.stringify({version: 2, linkingKeyHex: LINKING_KEY_HEX, cashRootHex: '00'}),
JSON.stringify({ version: 2, linkingKeyHex: LINKING_KEY_HEX, cashRootHex: '00' }),
2,
],
])('rejects %s records at the storage boundary', (_name, value, version) => {
// Given an invalid record under the v2 material namespace
stubLocalStorage()
const material = deriveWalletMaterial(FIXED_MNEMONIC)
stubLocalStorage();
const material = deriveWalletMaterial(FIXED_MNEMONIC);
const record = {
enc: false,
value,
materialHash: materialHash(value),
ownerId: linkingPubKeyHex(walletMaterialLinkingKey(material)),
version,
}
localStorage.setItem(MATERIAL_STORAGE_KEY, JSON.stringify(record))
};
localStorage.setItem(MATERIAL_STORAGE_KEY, JSON.stringify(record));
// When storage helpers parse it
// Then the record is unusable as wallet material
expect(isValidStoredWalletMaterial(record)).toBe(false)
expect(savedWalletMaterialExists()).toBe(false)
expect(getPlainWalletMaterial()).toBeNull()
})
expect(isValidStoredWalletMaterial(record)).toBe(false);
expect(savedWalletMaterialExists()).toBe(false);
expect(getPlainWalletMaterial()).toBeNull();
});
it('rejects authenticated ciphertext containing malformed material', async () => {
// Given valid AES-GCM parts whose plaintext is not a v2 material envelope
stubLocalStorage()
const parts = await encryptSecretParts(LINKING_KEY_HEX, PASSWORD)
const material = deriveWalletMaterial(FIXED_MNEMONIC)
stubLocalStorage();
const parts = await encryptSecretParts(LINKING_KEY_HEX, PASSWORD);
const material = deriveWalletMaterial(FIXED_MNEMONIC);
localStorage.setItem(
MATERIAL_STORAGE_KEY,
JSON.stringify({
@@ -199,12 +201,12 @@ describe('WalletMaterialV2 persistence', () => {
ownerId: linkingPubKeyHex(walletMaterialLinkingKey(material)),
version: 2,
}),
)
);
// When the password successfully authenticates the malformed plaintext
const unlock = decryptSavedWalletMaterial(PASSWORD)
const unlock = decryptSavedWalletMaterial(PASSWORD);
// Then it still fails closed as invalid material
await expect(unlock).rejects.toThrow('valid v2 wallet material')
})
})
await expect(unlock).rejects.toThrow('valid v2 wallet material');
});
});