**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
148 lines
4.5 KiB
TypeScript
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)}`;
|
|
}
|