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

148 lines
4.5 KiB
TypeScript

import {
etc as secpEtc,
getPublicKey,
sign,
Signature,
utils as secpUtils,
verify,
} from '@noble/secp256k1';
import { hmac } from '@noble/hashes/hmac';
import { sha256 } from '@noble/hashes/sha256';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
/** secp256k1 curve order (number of points on the curve). */
const SECP256K1_ORDER =
0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
// Require hmacSha256Sync for secp256k1 sign() (RFC6979). Set once at module load.
if (secpEtc.hmacSha256Sync === undefined) {
secpEtc.hmacSha256Sync = (k: Uint8Array, ...m: Uint8Array[]): Uint8Array =>
hmac(sha256, k, secpEtc.concatBytes(...m));
}
export interface KeyPair {
privateKey: string;
publicKey: string;
}
/**
* Generates a new secp256k1 key pair for authentication.
* The private key is kept secret, the public key is used for verification.
*/
export function generateKeyPair(): KeyPair {
const privateKey = secpUtils.randomPrivateKey();
const publicKey = getPublicKey(privateKey, true);
return {
privateKey: bytesToHex(privateKey),
publicKey: bytesToHex(publicKey),
};
}
/**
* Derives the secp256k1 public key (compressed, hex) from a raw hex private key.
* Input must be 64 hex characters (32 bytes). Throws if invalid.
*/
export function publicKeyFromPrivateKey(privateKeyHex: string): string {
const key = hexToBytes(privateKeyHex);
const pub = getPublicKey(key, true);
return bytesToHex(pub);
}
const DERIVE_DOMAIN = 'userwallet-derive-v1';
/**
* Derives a child key pair deterministically from a parent private key.
* Index must be >= 0. Same index always yields the same key pair.
* Uses HMAC-SHA256(parentKey, domain || index) then reduces to valid secp256k1 scalar.
*/
export function deriveChildKeyPair(
parentPrivateKeyHex: string,
index: number,
): KeyPair {
const parentBytes = hexToBytes(parentPrivateKeyHex);
const indexBytes = new TextEncoder().encode(`${DERIVE_DOMAIN}-${index}`);
const h = hmac(sha256, parentBytes, indexBytes);
const num = BigInt('0x' + bytesToHex(h));
const scalar = (num % (SECP256K1_ORDER - 1n)) + 1n;
const privHex = scalar.toString(16).padStart(64, '0');
const privateKeyBytes = hexToBytes(privHex);
const publicKey = bytesToHex(getPublicKey(privateKeyBytes, true));
return { privateKey: privHex, publicKey };
}
/**
* Returns the main public key plus derived public keys for indices 0..count-1.
* Index 0 is the first derived key, etc. Total length is 1 + count.
*/
export function getDerivedPublicKeys(
parentPrivateKeyHex: string,
count: number,
): string[] {
const main = publicKeyFromPrivateKey(parentPrivateKeyHex);
const out: string[] = [main];
for (let i = 0; i < count; i++) {
const { publicKey } = deriveChildKeyPair(parentPrivateKeyHex, i);
out.push(publicKey);
}
return out;
}
/**
* Returns true if the given public key is the main key or any derived key (indices 0..maxDerived-1).
* Fast: main key check O(1), then at most maxDerived derivations. Pass maxDerived to limit search.
*/
export function publicKeyBelongsToIdentity(
identityPrivateKeyHex: string,
publicKeyHex: string,
maxDerived: number = 0,
): boolean {
const main = publicKeyFromPrivateKey(identityPrivateKeyHex);
if (main === publicKeyHex) {
return true;
}
for (let i = 0; i < maxDerived; i++) {
const { publicKey } = deriveChildKeyPair(identityPrivateKeyHex, i);
if (publicKey === publicKeyHex) {
return true;
}
}
return false;
}
/**
* Signs a message with a private key using secp256k1.
* The message is hashed with SHA-256 before signing.
*/
export function signMessage(message: string, privateKeyHex: string): string {
const messageHash = sha256(message);
const privateKey = hexToBytes(privateKeyHex);
const sig = sign(messageHash, privateKey);
return bytesToHex(sig.toCompactRawBytes());
}
/**
* Verifies a signature against a message and public key.
* Returns false if verification fails or if any error occurs.
*/
export function verifySignature(
message: string,
signatureHex: string,
publicKeyHex: string,
): boolean {
try {
const messageHash = sha256(message);
const sig = Signature.fromCompact(hexToBytes(signatureHex));
const pub = hexToBytes(publicKeyHex);
return verify(sig, messageHash, pub);
} catch (error) {
console.error('Signature verification error:', error);
return false;
}
}
export function generateChallenge(): string {
const timestamp = Date.now();
const random = crypto.getRandomValues(new Uint8Array(16));
return `auth-challenge-${timestamp}-${bytesToHex(random)}`;
}