**Motivations:** - Ajouter dates manquantes dans hash_list.txt et compléter historique - Compléter blockTime manquants dans utxo_list.txt et compléter historique - Récupérer frais depuis transactions d'ancrage (OP_RETURN) et les stocker - Bouton UI pour déclencher récupération frais - Diagnostic Bloc Rewards (pourquoi ~4700 BTC au lieu de 50 BTC) **Root causes:** - hash_list.txt sans date (format ancien) - utxo_list.txt blockTime souvent vide - Frais absents du fichier (métadonnées OP_RETURN non stockées) - Pas de moyen de récupérer/compléter frais depuis UI **Correctifs:** - hash_list.txt : format étendu avec date (rétrocompatible) - utxo_list.txt : blockTime complété automatiquement lors écritures - fees_list.txt : nouveau fichier pour stocker frais - updateFeesFromAnchors() : récupère frais depuis OP_RETURN ancrages - Endpoint /api/utxo/fees/update pour déclencher récupération - Bouton "Récupérer les frais depuis les ancrages" dans section Frais (spinner) - Scripts batch : complete-hash-list-dates.js, complete-utxo-list-blocktime.js - Script diagnostic : diagnose-bloc-rewards.js (subsidy, coinbase, listunspent) **Evolutions:** - Frais chargés depuis fees_list.txt dans getUtxoList - Complétion automatique dates/blockTime lors écritures futures **Pages affectées:** - signet-dashboard/src/bitcoin-rpc.js - signet-dashboard/src/server.js - signet-dashboard/public/utxo-list.html - scripts/complete-hash-list-dates.js - scripts/complete-utxo-list-blocktime.js - scripts/diagnose-bloc-rewards.js - features/utxo-list-fees-update-and-historical-completion.md
180 lines
4.0 KiB
TypeScript
180 lines
4.0 KiB
TypeScript
import { getPublicKey, getSharedSecret } from '@noble/secp256k1';
|
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
|
|
/**
|
|
* Encrypt data with AES-GCM using a shared secret derived from ECDH.
|
|
*/
|
|
export async function encryptWithECDH(
|
|
data: string,
|
|
recipientPublicKey: string,
|
|
senderPrivateKey: string,
|
|
): Promise<{
|
|
encrypted: string;
|
|
iv: string;
|
|
publicKey: string;
|
|
}> {
|
|
const recipientPubKey = hexToBytes(recipientPublicKey);
|
|
const senderPrivKey = hexToBytes(senderPrivateKey);
|
|
|
|
const sharedSecret = getSharedSecret(senderPrivKey, recipientPubKey, true);
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
sharedSecret.slice(0, 32) as BufferSource,
|
|
{ name: 'HKDF' },
|
|
false,
|
|
['deriveBits', 'deriveKey'],
|
|
);
|
|
|
|
const derivedKey = await crypto.subtle.deriveKey(
|
|
{
|
|
name: 'HKDF',
|
|
hash: 'SHA-256',
|
|
salt: new Uint8Array(32),
|
|
info: new Uint8Array(0),
|
|
},
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['encrypt'],
|
|
);
|
|
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
const encoder = new TextEncoder();
|
|
const dataBytes = encoder.encode(data);
|
|
|
|
const encrypted = await crypto.subtle.encrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv,
|
|
},
|
|
derivedKey,
|
|
dataBytes,
|
|
);
|
|
|
|
return {
|
|
encrypted: bytesToHex(new Uint8Array(encrypted)),
|
|
iv: bytesToHex(iv),
|
|
publicKey: bytesToHex(getPublicKey(senderPrivKey, true)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decrypt data with AES-GCM using a shared secret derived from ECDH.
|
|
*/
|
|
export async function decryptWithECDH(
|
|
encrypted: string,
|
|
iv: string,
|
|
senderPublicKey: string,
|
|
recipientPrivateKey: string,
|
|
): Promise<string> {
|
|
const senderPubKey = hexToBytes(senderPublicKey);
|
|
const recipientPrivKey = hexToBytes(recipientPrivateKey);
|
|
|
|
const sharedSecret = getSharedSecret(recipientPrivKey, senderPubKey, true);
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
'raw',
|
|
sharedSecret.slice(0, 32) as BufferSource,
|
|
{ name: 'HKDF' },
|
|
false,
|
|
['deriveBits', 'deriveKey'],
|
|
);
|
|
|
|
const derivedKey = await crypto.subtle.deriveKey(
|
|
{
|
|
name: 'HKDF',
|
|
hash: 'SHA-256',
|
|
salt: new Uint8Array(32),
|
|
info: new Uint8Array(0),
|
|
},
|
|
keyMaterial,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
false,
|
|
['decrypt'],
|
|
);
|
|
|
|
const encryptedBytes = hexToBytes(encrypted);
|
|
const ivBytes = hexToBytes(iv);
|
|
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv: ivBytes as BufferSource,
|
|
},
|
|
derivedKey,
|
|
encryptedBytes as BufferSource,
|
|
);
|
|
|
|
const decoder = new TextDecoder();
|
|
return decoder.decode(decrypted);
|
|
}
|
|
|
|
/**
|
|
* Encrypt data for "publish to all" scenario.
|
|
* Uses a symmetric key that can be shared via ECDH with multiple recipients.
|
|
*/
|
|
export async function encryptForAll(
|
|
data: string,
|
|
encryptionKey: Uint8Array,
|
|
): Promise<{
|
|
encrypted: string;
|
|
iv: string;
|
|
}> {
|
|
const key = await crypto.subtle.importKey(
|
|
'raw',
|
|
encryptionKey as BufferSource,
|
|
{ name: 'AES-GCM' },
|
|
false,
|
|
['encrypt'],
|
|
);
|
|
|
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
const encoder = new TextEncoder();
|
|
const dataBytes = encoder.encode(data);
|
|
|
|
const encrypted = await crypto.subtle.encrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv: iv as BufferSource,
|
|
},
|
|
key,
|
|
dataBytes,
|
|
);
|
|
|
|
return {
|
|
encrypted: bytesToHex(new Uint8Array(encrypted)),
|
|
iv: bytesToHex(iv),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decrypt data encrypted with encryptForAll.
|
|
*/
|
|
export async function decryptForAll(
|
|
encrypted: string,
|
|
iv: string,
|
|
encryptionKey: Uint8Array,
|
|
): Promise<string> {
|
|
const key = await crypto.subtle.importKey(
|
|
'raw',
|
|
encryptionKey as BufferSource,
|
|
{ name: 'AES-GCM' },
|
|
false,
|
|
['decrypt'],
|
|
);
|
|
|
|
const encryptedBytes = hexToBytes(encrypted);
|
|
const ivBytes = hexToBytes(iv);
|
|
|
|
const decrypted = await crypto.subtle.decrypt(
|
|
{
|
|
name: 'AES-GCM',
|
|
iv: ivBytes as BufferSource,
|
|
},
|
|
key,
|
|
encryptedBytes as BufferSource,
|
|
);
|
|
|
|
const decoder = new TextDecoder();
|
|
return decoder.decode(decrypted);
|
|
}
|