**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
105 lines
3.3 KiB
JavaScript
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 };
|
|
}
|