**Motivations:** - Export Signet and mining wallet backups to git with only 2 versions kept - Document and add backup/restore scripts for signet and mining wallet **Correctifs:** - Backup-to-git uses SSH URL for passwordless cron; copy timestamped files only; prune to 2 versions; remove *-latest from backup repo **Evolutions:** - data/backup-to-git-cron.sh: daily export to git.4nkweb.com/4nk/backup - save-signet-datadir-backup.sh, restore-signet-from-backup.sh, export-mining-wallet.sh, import-mining-wallet.sh - features/backup-to-git-daily-cron.md, docs/MAINTENANCE.md backup section - .gitignore: data/backup-to-git.log **Pages affectées:** - .gitignore, data/backup-to-git-cron.sh, docs/MAINTENANCE.md, features/backup-to-git-daily-cron.md - save-signet-datadir-backup.sh, restore-signet-from-backup.sh, export-mining-wallet.sh, import-mining-wallet.sh - Plus autres fichiers modifiés ou non suivis déjà présents dans le working tree
280 lines
7.3 KiB
TypeScript
280 lines
7.3 KiB
TypeScript
import {
|
|
generateUuid,
|
|
uuidToBip32Words,
|
|
publicKeyToBip32Words,
|
|
bip32WordsToPublicKey,
|
|
} from './bip32';
|
|
import {
|
|
getDerivedPublicKeys,
|
|
publicKeyBelongsToIdentity,
|
|
} from './crypto';
|
|
import type { PairConfig } from '../types/identity';
|
|
|
|
const STORAGE_KEY_PAIRS = 'userwallet_pairs';
|
|
|
|
/**
|
|
* Get stored pair configurations.
|
|
*/
|
|
export function getStoredPairs(): PairConfig[] {
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY_PAIRS);
|
|
if (stored === null) {
|
|
return [];
|
|
}
|
|
return JSON.parse(stored) as PairConfig[];
|
|
} catch (error) {
|
|
console.error('Error reading stored pairs:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store pair configurations.
|
|
*/
|
|
export function storePairs(pairs: PairConfig[]): void {
|
|
localStorage.setItem(STORAGE_KEY_PAIRS, JSON.stringify(pairs));
|
|
}
|
|
|
|
/**
|
|
* Create a new local pair.
|
|
* Returns words encoding the identity public key (not the pair UUID).
|
|
*/
|
|
export function createLocalPair(
|
|
membresParentsUuid: string[],
|
|
identityPublicKey: string,
|
|
): {
|
|
pair: PairConfig;
|
|
words: string[];
|
|
} {
|
|
const uuid = generateUuid();
|
|
const words = publicKeyToBip32Words(identityPublicKey);
|
|
const pair: PairConfig = {
|
|
uuid,
|
|
membres_parents_uuid: membresParentsUuid,
|
|
is_local: true,
|
|
can_sign: true,
|
|
publicKey: identityPublicKey,
|
|
};
|
|
const pairs = getStoredPairs();
|
|
pairs.push(pair);
|
|
storePairs(pairs);
|
|
return { pair, words };
|
|
}
|
|
|
|
/**
|
|
* Add a remote pair from BIP32 words.
|
|
* Words encode the remote identity public key (66 hex chars).
|
|
*/
|
|
export function addRemotePairFromWords(
|
|
words: string[],
|
|
membresParentsUuid: string[],
|
|
): PairConfig | null {
|
|
const publicKey = bip32WordsToPublicKey(words);
|
|
if (publicKey === null) {
|
|
return null;
|
|
}
|
|
const uuid = generateUuid();
|
|
const pair: PairConfig = {
|
|
uuid,
|
|
membres_parents_uuid: membresParentsUuid,
|
|
is_local: false,
|
|
can_sign: false,
|
|
publicKey,
|
|
};
|
|
const pairs = getStoredPairs();
|
|
pairs.push(pair);
|
|
storePairs(pairs);
|
|
return pair;
|
|
}
|
|
|
|
/**
|
|
* Generate BIP32 words for pairing without creating or storing a pair.
|
|
* Use when showing QR/URL for second device; user adds remote pair on both
|
|
* devices from these words.
|
|
*/
|
|
export function generatePairingWords(): string[] {
|
|
const uuid = generateUuid();
|
|
return uuidToBip32Words(uuid);
|
|
}
|
|
|
|
const EXPECTED_PAIRING_WORD_COUNT = 17; // 17 words for public key (33 bytes)
|
|
|
|
/**
|
|
* Parse and validate pairing words from user input (whitespace-separated).
|
|
* Returns word array if valid, null otherwise.
|
|
*/
|
|
export function parseAndValidatePairingWords(text: string): string[] | null {
|
|
const words = text
|
|
.toLowerCase()
|
|
.split(/\s+/)
|
|
.filter((w) => w.length > 0);
|
|
if (words.length !== EXPECTED_PAIRING_WORD_COUNT) {
|
|
return null;
|
|
}
|
|
const publicKey = bip32WordsToPublicKey(words);
|
|
return publicKey !== null ? words : null;
|
|
}
|
|
|
|
/**
|
|
* Check if pairing is satisfied (at least one pair available).
|
|
*/
|
|
export function isPairingSatisfied(): boolean {
|
|
const pairs = getStoredPairs();
|
|
return pairs.length > 0;
|
|
}
|
|
|
|
/**
|
|
* True if at least one stored pair is remote (is_local === false).
|
|
*/
|
|
export function hasRemotePair(): boolean {
|
|
const pairs = getStoredPairs();
|
|
return pairs.some((p) => !p.is_local);
|
|
}
|
|
|
|
/**
|
|
* Ensure a local pair exists for setup (1st or 2nd device). If none, create one.
|
|
* Returns the local pair's words (encoding public key) for QR/URL. Idempotent.
|
|
* Requires identity public key.
|
|
*/
|
|
export function ensureLocalPairForSetup(identityPublicKey: string): string[] {
|
|
const pairs = getStoredPairs();
|
|
const local = pairs.find((p) => p.is_local);
|
|
if (local !== undefined && local.publicKey !== undefined) {
|
|
return publicKeyToBip32Words(local.publicKey);
|
|
}
|
|
const { words } = createLocalPair([], identityPublicKey);
|
|
return words;
|
|
}
|
|
|
|
/**
|
|
* Get pairs for a specific member.
|
|
*/
|
|
export function getPairsForMember(membreUuid: string): PairConfig[] {
|
|
const pairs = getStoredPairs();
|
|
return pairs.filter((p) => p.membres_parents_uuid.includes(membreUuid));
|
|
}
|
|
|
|
/**
|
|
* Words for the local pair (encoding public key, BIP32-style). Null if no local pair or no public key.
|
|
* Does not create a pair.
|
|
*/
|
|
export function getLocalPairWords(): string[] | null {
|
|
const pairs = getStoredPairs();
|
|
const local = pairs.find((p) => p.is_local);
|
|
if (local === undefined || local.publicKey === undefined) {
|
|
return null;
|
|
}
|
|
return publicKeyToBip32Words(local.publicKey);
|
|
}
|
|
|
|
/**
|
|
* Sync check not available; use usePairingConnected() hook for async result.
|
|
* Kept for backward compatibility where only sync check was used.
|
|
*/
|
|
export function isPairingConnected(): boolean {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Remove a pair by UUID.
|
|
*/
|
|
export function removePair(pairUuid: string): void {
|
|
const pairs = getStoredPairs();
|
|
const filtered = pairs.filter((p) => p.uuid !== pairUuid);
|
|
storePairs(filtered);
|
|
}
|
|
|
|
/**
|
|
* Update a pair's public key (for ECDH encryption).
|
|
*/
|
|
export function updatePairPublicKey(pairUuid: string, publicKey: string): void {
|
|
const pairs = getStoredPairs();
|
|
const pair = pairs.find((p) => p.uuid === pairUuid);
|
|
if (pair === undefined) {
|
|
return;
|
|
}
|
|
pair.publicKey = publicKey;
|
|
storePairs(pairs);
|
|
}
|
|
|
|
/**
|
|
* Update a pair's persistent label (for display).
|
|
*/
|
|
export function updatePairLabel(pairUuid: string, label: string): void {
|
|
const pairs = getStoredPairs();
|
|
const pair = pairs.find((p) => p.uuid === pairUuid);
|
|
if (pair === undefined) {
|
|
return;
|
|
}
|
|
pair.label = label;
|
|
storePairs(pairs);
|
|
}
|
|
|
|
/**
|
|
* Returns all public keys for a pair. For local pair, pass identity private key to include
|
|
* derived keys (main + indices 0..derivedCount-1). For remote, returns publicKey + publicKeys.
|
|
*/
|
|
export function getPairPublicKeys(
|
|
pair: PairConfig,
|
|
identityPrivateKeyHex?: string,
|
|
derivedCount: number = 0,
|
|
): string[] {
|
|
if (pair.is_local && identityPrivateKeyHex !== undefined) {
|
|
return getDerivedPublicKeys(identityPrivateKeyHex, derivedCount);
|
|
}
|
|
const keys: string[] = [];
|
|
if (pair.publicKey !== undefined) {
|
|
keys.push(pair.publicKey);
|
|
}
|
|
if (pair.publicKeys !== undefined) {
|
|
for (const k of pair.publicKeys) {
|
|
if (k !== pair.publicKey && !keys.includes(k)) {
|
|
keys.push(k);
|
|
}
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the pair "owns" the given public key. Fast for local pair when identity
|
|
* private key is passed (main key O(1), then at most maxDerived derivations).
|
|
*/
|
|
export function pairContainsPublicKey(
|
|
pair: PairConfig,
|
|
publicKeyHex: string,
|
|
identityPrivateKeyHex?: string,
|
|
maxDerived: number = 0,
|
|
): boolean {
|
|
if (pair.is_local && identityPrivateKeyHex !== undefined) {
|
|
return publicKeyBelongsToIdentity(
|
|
identityPrivateKeyHex,
|
|
publicKeyHex,
|
|
maxDerived,
|
|
);
|
|
}
|
|
if (pair.publicKey === publicKeyHex) {
|
|
return true;
|
|
}
|
|
return pair.publicKeys?.includes(publicKeyHex) ?? false;
|
|
}
|
|
|
|
/**
|
|
* Add a derived public key to a pair (appends to publicKeys). Does not duplicate publicKey.
|
|
*/
|
|
export function addPairPublicKey(pairUuid: string, publicKeyHex: string): void {
|
|
const pairs = getStoredPairs();
|
|
const pair = pairs.find((p) => p.uuid === pairUuid);
|
|
if (pair === undefined) {
|
|
return;
|
|
}
|
|
const existing = pair.publicKey === publicKeyHex || pair.publicKeys?.includes(publicKeyHex);
|
|
if (existing) {
|
|
return;
|
|
}
|
|
const next = pair.publicKeys ?? [];
|
|
next.push(publicKeyHex);
|
|
pair.publicKeys = next;
|
|
storePairs(pairs);
|
|
}
|