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
+87 -87
View File
@@ -2,14 +2,14 @@
// tamper rejection, publish/fetch and restore against an in-memory relay
// (the transport is injected - no network), and the debounced publisher.
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
import {bytesToHex} from '@noble/hashes/utils.js'
import type {NostrEvent} from 'nostr-tools/core'
import {finalizeEvent, getPublicKey} from 'nostr-tools/pure'
import {v2 as nip44v2} from 'nostr-tools/nip44'
import {buildNoteUrl} from 'lnurlcash-kit'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { bytesToHex } from '@noble/hashes/utils.js';
import type { NostrEvent } from 'nostr-tools/core';
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
import { v2 as nip44v2 } from 'nostr-tools/nip44';
import { buildNoteUrl } from 'lnurlcash-kit';
import {deriveBearerAesKey, linkingPubKeyHex, saveLinkingKey} from './keys'
import { deriveBearerAesKey, linkingPubKeyHex, saveLinkingKey } from './keys';
import {
BACKUP_EVENT_KIND,
backupPubkey,
@@ -21,8 +21,8 @@ import {
parseBackupEvent,
publishBackup,
restoreFromNostr,
} from './nostrBackup'
import type {BackupPartPayload, BackupTransport} from './nostrBackup'
} from './nostrBackup';
import type { BackupPartPayload, BackupTransport } from './nostrBackup';
import {
loadBearers,
loadSettings,
@@ -30,21 +30,21 @@ import {
persistBearer,
persistSettings,
readEncryptedBearers,
} from './storage'
import type {Bearer} from './types'
import {addTrustedMint, isMintUnconfirmed, readTrustedMints} from './trustedMints'
import {requiredValue, stubLocalStorage} from './test-utils'
} from './storage';
import type { Bearer } from './types';
import { addTrustedMint, isMintUnconfirmed, readTrustedMints } from './trustedMints';
import { requiredValue, stubLocalStorage } from './test-utils';
const LINKING_KEY = new Uint8Array(32).fill(7)
const OTHER_KEY = new Uint8Array(32).fill(9)
const OWNER_ID = linkingPubKeyHex(LINKING_KEY)
const LINKING_KEY = new Uint8Array(32).fill(7);
const OTHER_KEY = new Uint8Array(32).fill(9);
const OWNER_ID = linkingPubKeyHex(LINKING_KEY);
const K1_A = 'aa'.repeat(32)
const K1_B = 'bb'.repeat(32)
const MINT_PUBKEY = 'ab'.repeat(33)
const K1_A = 'aa'.repeat(32);
const K1_B = 'bb'.repeat(32);
const MINT_PUBKEY = 'ab'.repeat(33);
// never connected - the recording transport below stands in for the relays
const RELAYS = ['wss://relay-a.example', 'wss://relay-b.example']
const RELAYS = ['wss://relay-a.example', 'wss://relay-b.example'];
const bearerFixture = (overrides: Partial<Bearer> = {}): Bearer => ({
id: 'fixture',
@@ -55,20 +55,20 @@ const bearerFixture = (overrides: Partial<Bearer> = {}): Bearer => ({
createdAt: 1000,
updatedAt: 1000,
...overrides,
})
});
// an in-memory relay set. It serves EVERY event it ever accepted, older
// addressable copies included - like a relay that never replaces - which
// is exactly the case fetchBackup's client-side latest-pick exists for
const createRecordingTransport = (): {
transport: BackupTransport
events: NostrEvent[]
transport: BackupTransport;
events: NostrEvent[];
} => {
const events: NostrEvent[] = []
const events: NostrEvent[] = [];
const transport: BackupTransport = {
publish: (_relays, event) => {
events.push(event)
return Promise.resolve()
events.push(event);
return Promise.resolve();
},
fetch: (_relays, filter) =>
Promise.resolve(
@@ -78,121 +78,121 @@ const createRecordingTransport = (): {
(!filter.authors || filter.authors.includes(e.pubkey)),
),
),
}
return {transport, events}
}
};
return { transport, events };
};
// flips the end of a base64 payload to different-but-valid characters
const tamperContent = (content: string): string =>
content.slice(0, -4) + (content.endsWith('AAAA') ? 'BBBB' : 'AAAA')
content.slice(0, -4) + (content.endsWith('AAAA') ? 'BBBB' : 'AAAA');
beforeEach(() => {
stubLocalStorage()
})
stubLocalStorage();
});
afterEach(() => {
vi.useRealTimers()
})
vi.useRealTimers();
});
describe('buildBackupEvent / parseBackupEvent', () => {
const secretKey = deriveBackupKey(LINKING_KEY)
const secretKey = deriveBackupKey(LINKING_KEY);
const records = [
// long unique sentinel id: a short id like 'r1' randomly appears in
// base64 ciphertext (~17% for 700 chars), which flakes the no-plaintext
// assertion below
{id: 'record-id-plaintext-sentinel-7f3a', iv: '00'.repeat(12), ciphertext: 'ab'.repeat(40)},
]
const notes = {bearers: records, nextByHost: {'mint.example': 3}}
const mints = [{server: 'mint.example', mintPubkey: MINT_PUBKEY, addedAt: 1000, locked: true}]
const settings = {defaultMint: 'mint.example'}
{ id: 'record-id-plaintext-sentinel-7f3a', iv: '00'.repeat(12), ciphertext: 'ab'.repeat(40) },
];
const notes = { bearers: records, nextByHost: { 'mint.example': 3 } };
const mints = [{ server: 'mint.example', mintPubkey: MINT_PUBKEY, addedAt: 1000, locked: true }];
const settings = { defaultMint: 'mint.example' };
it('round-trips all three parts through build and parse', () => {
const events = buildBackupEvents(secretKey, {notes, mints, settings}, 1000)
expect(events).toHaveLength(3)
const events = buildBackupEvents(secretKey, { notes, mints, settings }, 1000);
expect(events).toHaveLength(3);
expect(events.map((e) => e.kind)).toEqual([
BACKUP_EVENT_KIND,
BACKUP_EVENT_KIND,
BACKUP_EVENT_KIND,
])
]);
expect(events.map((e) => e.tags)).toEqual([
[['d', 'notes']],
[['d', 'mints']],
[['d', 'settings']],
])
expect(events.every((e) => e.pubkey === backupPubkey(secretKey))).toBe(true)
]);
expect(events.every((e) => e.pubkey === backupPubkey(secretKey))).toBe(true);
expect(parseBackupEvent(secretKey, requiredValue(events[0]))).toEqual({
part: 'notes',
bearers: records,
nextByHost: {'mint.example': 3},
})
nextByHost: { 'mint.example': 3 },
});
expect(parseBackupEvent(secretKey, requiredValue(events[1]))).toEqual({
part: 'mints',
trustedMints: mints,
})
});
expect(parseBackupEvent(secretKey, requiredValue(events[2]))).toEqual({
part: 'settings',
settings,
})
})
});
});
it('leaves no plaintext in the payload', () => {
const event = buildBackupEvent(secretKey, 'notes', notes)
expect(event.content).not.toContain('record-id-plaintext-sentinel-7f3a')
expect(event.content).not.toContain('ciphertext')
})
const event = buildBackupEvent(secretKey, 'notes', notes);
expect(event.content).not.toContain('record-id-plaintext-sentinel-7f3a');
expect(event.content).not.toContain('ciphertext');
});
it('builds events only for the parts present', () => {
const events = buildBackupEvents(secretKey, {settings}, 1000)
expect(events).toHaveLength(1)
expect(requiredValue(events[0]).tags).toEqual([['d', 'settings']])
})
const events = buildBackupEvents(secretKey, { settings }, 1000);
expect(events).toHaveLength(1);
expect(requiredValue(events[0]).tags).toEqual([['d', 'settings']]);
});
it('rejects a payload encrypted for a different key', () => {
const event = buildBackupEvent(secretKey, 'settings', settings)
expect(parseBackupEvent(deriveBackupKey(OTHER_KEY), event)).toBeNull()
})
const event = buildBackupEvent(secretKey, 'settings', settings);
expect(parseBackupEvent(deriveBackupKey(OTHER_KEY), event)).toBeNull();
});
it('rejects the wrong kind', () => {
const event = buildBackupEvent(secretKey, 'settings', settings)
expect(parseBackupEvent(secretKey, {...event, kind: 30079})).toBeNull()
})
const event = buildBackupEvent(secretKey, 'settings', settings);
expect(parseBackupEvent(secretKey, { ...event, kind: 30079 })).toBeNull();
});
it('rejects an unknown d-tag', () => {
const event = buildBackupEvent(secretKey, 'settings', settings)
expect(parseBackupEvent(secretKey, {...event, tags: [['d', 'secrets']]})).toBeNull()
})
const event = buildBackupEvent(secretKey, 'settings', settings);
expect(parseBackupEvent(secretKey, { ...event, tags: [['d', 'secrets']] })).toBeNull();
});
it('rejects a modified ciphertext - the signature no longer matches', () => {
const event = buildBackupEvent(secretKey, 'settings', settings)
const tampered = {...event, content: tamperContent(event.content)}
expect(parseBackupEvent(secretKey, tampered)).toBeNull()
})
const event = buildBackupEvent(secretKey, 'settings', settings);
const tampered = { ...event, content: tamperContent(event.content) };
expect(parseBackupEvent(secretKey, tampered)).toBeNull();
});
it('rejects an event signed by a different key', () => {
const foreign = buildBackupEvent(deriveBackupKey(OTHER_KEY), 'settings', settings)
expect(parseBackupEvent(secretKey, foreign)).toBeNull()
})
const foreign = buildBackupEvent(deriveBackupKey(OTHER_KEY), 'settings', settings);
expect(parseBackupEvent(secretKey, foreign)).toBeNull();
});
it('rejects a validly signed event whose payload is not a backup envelope', () => {
// a same-key event of the right kind and d-tag, but its decrypted
// content is not a version-1 envelope
const conversationKey = nip44v2.utils.getConversationKey(secretKey, getPublicKey(secretKey))
const conversationKey = nip44v2.utils.getConversationKey(secretKey, getPublicKey(secretKey));
const event = finalizeEvent(
{
kind: BACKUP_EVENT_KIND,
created_at: 1000,
tags: [['d', 'settings']],
content: nip44v2.encrypt(JSON.stringify({version: 2, settings: {}}), conversationKey),
content: nip44v2.encrypt(JSON.stringify({ version: 2, settings: {} }), conversationKey),
},
secretKey,
)
expect(parseBackupEvent(secretKey, event)).toBeNull()
})
);
expect(parseBackupEvent(secretKey, event)).toBeNull();
});
it('rejects a legacy version-1 notes envelope and a v2 notes envelope without counters', () => {
const conversationKey = nip44v2.utils.getConversationKey(secretKey, getPublicKey(secretKey))
const conversationKey = nip44v2.utils.getConversationKey(secretKey, getPublicKey(secretKey));
const build = (payload: unknown): NostrEvent =>
finalizeEvent(
{
@@ -202,13 +202,13 @@ describe('buildBackupEvent / parseBackupEvent', () => {
content: nip44v2.encrypt(JSON.stringify(payload), conversationKey),
},
secretKey,
)
);
// v1 notes carried bare bearer arrays and no counters - pre-funds-document
expect(parseBackupEvent(secretKey, build({version: 1, bearers: records}))).toBeNull()
expect(parseBackupEvent(secretKey, build({version: 2, bearers: records}))).toBeNull()
expect(parseBackupEvent(secretKey, build({ version: 1, bearers: records }))).toBeNull();
expect(parseBackupEvent(secretKey, build({ version: 2, bearers: records }))).toBeNull();
// a v2 envelope whose counters are malformed is rejected, not coerced
expect(
parseBackupEvent(secretKey, build({version: 2, bearers: records, nextByHost: 'junk'})),
).toBeNull()
})
})
parseBackupEvent(secretKey, build({ version: 2, bearers: records, nextByHost: 'junk' })),
).toBeNull();
});
});