ncantu 937646cc45 Daily backup to git cron, backup/restore scripts, docs
**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
2026-02-04 03:07:57 +01:00

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