ncantu 695aff4f85 api-relay DB migration, auth, service-login-verify PersistentNonceCache, UserWallet crypto settings
**Motivations:**
- Migrer api-relay vers base de données SQLite (production)
- Ajouter authentification API key pour endpoints POST (protection abus)
- PersistentNonceCache pour service-login-verify (IndexedDB/localStorage)
- Écran paramètres crypto avancés UserWallet
- Documenter options non implémentées (Merkle, évolutions api-relay)

**Root causes:**
- N/A (évolutions + correctifs)

**Correctifs:**
- N/A

**Evolutions:**
- api-relay: DatabaseStorageService (SQLite), StorageAdapter (compatibilité), ApiKeyService (génération/validation), auth middleware (Bearer/X-API-Key), endpoints admin (/admin/api-keys), migration script (migrate-to-db.ts), suppression saveToDisk périodique
- service-login-verify: PersistentNonceCache (IndexedDB avec fallback localStorage, TTL, cleanup), export dans index
- userwallet: CryptoSettingsScreen (hashAlgorithm, jsonCanonizationStrict, ecdhCurve, nonceTtlMs, timestampWindowMs), modifications LoginScreen, LoginForm, CreateIdentityScreen, ImportIdentityScreen, DataExportImportScreen, PairingDisplayScreen, RelaySettingsScreen, ServiceListScreen, MemberSelectionScreen, GlobalActionBar
- features: OPTIONS_NON_IMPLENTEES.md (analyse Merkle trees, évolutions api-relay)

**Pages affectées:**
- api-relay: package.json, index.ts, middleware/auth.ts, services/database.ts, services/storageAdapter.ts, services/apiKeyService.ts, scripts/migrate-to-db.ts
- service-login-verify: persistentNonceCache.ts, index.ts, tsconfig.json, dist/
- userwallet: App, CryptoSettingsScreen, LoginScreen, LoginForm, CreateIdentityScreen, ImportIdentityScreen, DataExportImportScreen, PairingDisplayScreen, RelaySettingsScreen, ServiceListScreen, MemberSelectionScreen, GlobalActionBar
- features: OPTIONS_NON_IMPLENTEES.md
- data: sync-utxos.log
2026-01-28 07:36:01 +01:00

105 lines
3.3 KiB
JavaScript

import { verifySignature } from './crypto.js';
const DEFAULT_TIMESTAMP_WINDOW_MS = 300000;
function verifyTimestamp(timestamp, windowMs) {
const now = Date.now();
const diff = Math.abs(now - timestamp);
return diff <= windowMs;
}
function verifySignaturesStrict(hashValue, signatures, allowedPubkeys) {
let valid = 0;
let unauthorized = 0;
for (const s of signatures) {
const messageToVerify = `${hashValue}-${s.nonce}`;
const cryptoOk = verifySignature(messageToVerify, s.signature, s.cle_publique);
if (!cryptoOk) {
continue;
}
if (allowedPubkeys.has(s.cle_publique)) {
valid++;
}
else {
unauthorized++;
}
}
return { valid, unauthorized };
}
/**
* Verify login proof: crypto, allowed pubkeys, timestamp window, nonce anti-replay.
* Service must provide allowedPubkeys (from validators) and a NonceCache.
*
* @param proof - Login proof from UserWallet
* @param ctx - Verification context (allowedPubkeys, nonceCache, timestampWindowMs)
* @returns Verification result with accept flag and optional reason
* @throws {Error} If proof structure is invalid (missing challenge, hash, signatures)
*/
export function verifyLoginProof(proof, ctx) {
// Validate proof structure
if (proof.challenge === undefined || typeof proof.challenge !== 'object') {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (typeof proof.challenge.hash !== 'string' ||
proof.challenge.hash.length === 0) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (typeof proof.challenge.nonce !== 'string' ||
proof.challenge.nonce.length === 0) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (typeof proof.challenge.timestamp !== 'number' ||
!Number.isFinite(proof.challenge.timestamp)) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (!Array.isArray(proof.signatures)) {
return {
accept: false,
reason: 'invalid_proof_structure',
};
}
if (ctx.allowedPubkeys.size === 0) {
return {
accept: false,
reason: 'validators_not_verifiable',
};
}
const windowMs = ctx.timestampWindowMs ?? DEFAULT_TIMESTAMP_WINDOW_MS;
if (!verifyTimestamp(proof.challenge.timestamp, windowMs)) {
return {
accept: false,
reason: 'timestamp_out_of_window',
};
}
if (!ctx.nonceCache.isValid(proof.challenge.nonce, proof.challenge.timestamp)) {
return {
accept: false,
reason: 'nonce_reused',
};
}
const hashValue = proof.challenge.hash;
const { valid, unauthorized } = verifySignaturesStrict(hashValue, proof.signatures, ctx.allowedPubkeys);
if (valid === 0) {
return {
accept: false,
reason: 'no_validator_signature',
};
}
if (unauthorized > 0) {
return {
accept: false,
reason: 'signature_cle_publique_not_authorized',
};
}
return { accept: true };
}