From 695aff4f858d13a36350f5f8db1bf0202f882505 Mon Sep 17 00:00:00 2001 From: ncantu Date: Wed, 28 Jan 2026 07:36:01 +0100 Subject: [PATCH] api-relay DB migration, auth, service-login-verify PersistentNonceCache, UserWallet crypto settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 --- api-relay/package-lock.json | 410 ++++++++++++++- api-relay/package.json | 5 +- api-relay/src/index.ts | 80 ++- api-relay/src/middleware/auth.ts | 58 +++ api-relay/src/scripts/migrate-to-db.ts | 82 +++ api-relay/src/services/apiKeyService.ts | 57 +++ api-relay/src/services/database.ts | 479 ++++++++++++++++++ api-relay/src/services/storageAdapter.ts | 88 ++++ data/sync-utxos.log | 126 ++--- features/OPTIONS_NON_IMPLENTEES.md | 221 ++++++++ service-login-verify/dist/index.d.ts | 1 + service-login-verify/dist/index.d.ts.map | 2 +- service-login-verify/dist/index.js | 1 + .../dist/persistentNonceCache.d.ts | 36 ++ .../dist/persistentNonceCache.d.ts.map | 1 + .../dist/persistentNonceCache.js | 107 ++++ service-login-verify/dist/types.d.ts | 2 +- service-login-verify/dist/types.d.ts.map | 2 +- .../dist/verifyLoginProof.d.ts | 5 + .../dist/verifyLoginProof.d.ts.map | 2 +- service-login-verify/dist/verifyLoginProof.js | 39 ++ service-login-verify/src/index.ts | 1 + .../src/persistentNonceCache.ts | 123 +++++ service-login-verify/tsconfig.json | 2 +- userwallet/src/App.tsx | 2 + .../src/components/CreateIdentityScreen.tsx | 24 +- .../src/components/CryptoSettingsScreen.tsx | 216 ++++++++ .../src/components/DataExportImportScreen.tsx | 110 ++-- userwallet/src/components/GlobalActionBar.tsx | 5 +- .../src/components/ImportIdentityScreen.tsx | 32 +- userwallet/src/components/LoginForm.tsx | 36 +- userwallet/src/components/LoginScreen.tsx | 84 +-- .../src/components/MemberSelectionScreen.tsx | 4 +- .../src/components/PairingDisplayScreen.tsx | 128 ++--- .../src/components/RelaySettingsScreen.tsx | 43 +- .../src/components/ServiceListScreen.tsx | 6 +- 36 files changed, 2303 insertions(+), 317 deletions(-) create mode 100644 api-relay/src/middleware/auth.ts create mode 100644 api-relay/src/scripts/migrate-to-db.ts create mode 100644 api-relay/src/services/apiKeyService.ts create mode 100644 api-relay/src/services/database.ts create mode 100644 api-relay/src/services/storageAdapter.ts create mode 100644 features/OPTIONS_NON_IMPLENTEES.md create mode 100644 service-login-verify/dist/persistentNonceCache.d.ts create mode 100644 service-login-verify/dist/persistentNonceCache.d.ts.map create mode 100644 service-login-verify/dist/persistentNonceCache.js create mode 100644 service-login-verify/src/persistentNonceCache.ts create mode 100644 userwallet/src/components/CryptoSettingsScreen.tsx diff --git a/api-relay/package-lock.json b/api-relay/package-lock.json index ab57a4b..a919018 100644 --- a/api-relay/package-lock.json +++ b/api-relay/package-lock.json @@ -8,6 +8,7 @@ "name": "userwallet-api-relay", "version": "1.0.0", "dependencies": { + "better-sqlite3": "^11.10.0", "bloom-filters": "^3.0.4", "compression": "^1.8.1", "cors": "^2.8.5", @@ -18,6 +19,7 @@ }, "devDependencies": { "@eslint/js": "^9.0.0", + "@types/better-sqlite3": "^7.6.13", "@types/compression": "^1.8.1", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -677,6 +679,16 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1168,12 +1180,63 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/bintrees": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", "license": "MIT" }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/bloom-filters": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz", @@ -1255,6 +1318,30 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1320,6 +1407,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1493,6 +1586,30 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1519,6 +1636,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1574,6 +1700,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1891,6 +2026,15 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -2044,6 +2188,12 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2147,6 +2297,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2228,6 +2384,12 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -2409,6 +2571,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2464,6 +2646,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", @@ -2741,6 +2929,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -2757,12 +2957,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2779,6 +3000,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2834,7 +3067,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -3008,6 +3240,32 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3060,6 +3318,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3136,6 +3404,44 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -3268,7 +3574,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3432,6 +3737,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3469,6 +3819,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3508,6 +3867,34 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tdigest": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", @@ -3591,6 +3978,18 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3670,6 +4069,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3718,7 +4123,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/xxhashjs": { diff --git a/api-relay/package.json b/api-relay/package.json index 9c376ee..3d23617 100644 --- a/api-relay/package.json +++ b/api-relay/package.json @@ -9,9 +9,11 @@ "build": "tsc", "start": "node dist/index.js", "lint": "eslint . --ext ts --report-unused-disable-directives --max-warnings 0", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "migrate": "tsx src/scripts/migrate-to-db.ts" }, "dependencies": { + "better-sqlite3": "^11.10.0", "bloom-filters": "^3.0.4", "compression": "^1.8.1", "cors": "^2.8.5", @@ -22,6 +24,7 @@ }, "devDependencies": { "@eslint/js": "^9.0.0", + "@types/better-sqlite3": "^7.6.13", "@types/compression": "^1.8.1", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", diff --git a/api-relay/src/index.ts b/api-relay/src/index.ts index b446d72..29f2ce0 100644 --- a/api-relay/src/index.ts +++ b/api-relay/src/index.ts @@ -1,6 +1,8 @@ import express from 'express'; import http from 'http'; -import { StorageService } from './services/storage.js'; +import { DatabaseStorageService } from './services/database.js'; +import { StorageAdapter } from './services/storageAdapter.js'; +import { ApiKeyService } from './services/apiKeyService.js'; import { RelayService } from './services/relay.js'; import { createMessagesRouter } from './routes/messages.js'; import { createSignaturesRouter } from './routes/signatures.js'; @@ -13,6 +15,7 @@ import { getBodyLimit, getRequestTimeoutMs, } from './middleware/index.js'; +import { createAuthMiddleware } from './middleware/auth.js'; import { logger } from './lib/logger.js'; const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3019; @@ -31,8 +34,22 @@ async function main(): Promise { registerMiddleware(app); app.use(express.json({ limit: getBodyLimit() })); - const storage = new StorageService(STORAGE_PATH); - await storage.initialize(); + // Initialize database + const dbStorage = new DatabaseStorageService(STORAGE_PATH); + await dbStorage.initialize(); + + // Create adapter for compatibility + const storage = new StorageAdapter(dbStorage); + + // Initialize API key service + const apiKeyService = new ApiKeyService(dbStorage); + + // Register authentication middleware if required + if (REQUIRE_API_KEY) { + app.use( + createAuthMiddleware((key) => apiKeyService.validateApiKey(key)), + ); + } const relay = new RelayService(storage, PEER_RELAYS); @@ -43,42 +60,51 @@ async function main(): Promise { app.use('/metrics', createMetricsRouter(storage)); app.use('/bloom', createBloomRouter(storage)); - let saveIntervalId: ReturnType | null = null; - if (SAVE_INTERVAL_SECONDS > 0) { - saveIntervalId = setInterval(() => { - storage.saveToDisk().catch((err) => { - logger.error({ err }, 'Periodic save failed'); - }); - }, SAVE_INTERVAL_SECONDS * 1000); - } + // API key management endpoint (admin only, should be protected in production) + app.post('/admin/api-keys', (req, res) => { + const { description } = req.body as { description?: string }; + const { key, prefix } = apiKeyService.generateApiKey(description); + res.status(201).json({ key, prefix, description }); + }); + + app.get('/admin/api-keys', (_req, res) => { + const keys = apiKeyService.listApiKeys(); + res.json(keys); + }); + + app.delete('/admin/api-keys/:prefix', (req, res) => { + const { prefix } = req.params; + const deleted = apiKeyService.deleteApiKey(prefix); + if (deleted) { + res.status(204).send(); + } else { + res.status(404).json({ error: 'API key not found' }); + } + }); const server = http.createServer(app); server.timeout = getRequestTimeoutMs(); server.listen(PORT, HOST, () => { - logger.info( - { - host: HOST, - port: PORT, - storagePath: STORAGE_PATH, - peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none', - saveIntervalSeconds: SAVE_INTERVAL_SECONDS > 0 ? SAVE_INTERVAL_SECONDS : null, - }, - 'Relay server listening', - ); + logger.info( + { + host: HOST, + port: PORT, + storagePath: STORAGE_PATH, + peerRelays: PEER_RELAYS.length > 0 ? PEER_RELAYS : 'none', + requireApiKey: REQUIRE_API_KEY, + }, + 'Relay server listening', + ); }); const shutdown = async (): Promise => { logger.info('Shutting down...'); - if (saveIntervalId !== null) { - clearInterval(saveIntervalId); - saveIntervalId = null; - } try { - await storage.saveToDisk(); + dbStorage.close(); process.exit(0); } catch (err) { - logger.error({ err }, 'Error saving storage on shutdown'); + logger.error({ err }, 'Error closing database on shutdown'); process.exit(1); } }; diff --git a/api-relay/src/middleware/auth.ts b/api-relay/src/middleware/auth.ts new file mode 100644 index 0000000..2c2524f --- /dev/null +++ b/api-relay/src/middleware/auth.ts @@ -0,0 +1,58 @@ +import type { Request, Response, NextFunction } from 'express'; +import { logger } from '../lib/logger.js'; + +/** + * Middleware to authenticate POST requests using API key. + * API key should be provided in the Authorization header as: "Bearer " + * or in the X-API-Key header. + */ +export function createAuthMiddleware( + validateApiKey: (key: string) => boolean, +): (req: Request, res: Response, next: NextFunction) => void { + return (req: Request, res: Response, next: NextFunction): void => { + // Only require auth for POST requests + if (req.method !== 'POST') { + next(); + return; + } + + const authHeader = req.headers.authorization; + const apiKeyHeader = req.headers['x-api-key'] as string | undefined; + + let apiKey: string | undefined; + + if (authHeader !== undefined && authHeader.startsWith('Bearer ')) { + apiKey = authHeader.slice(7); + } else if (apiKeyHeader !== undefined) { + apiKey = apiKeyHeader; + } + + if (apiKey === undefined || apiKey.length === 0) { + logger.warn( + { + method: req.method, + url: req.url, + ip: req.ip, + }, + 'POST request without API key', + ); + res.status(401).json({ error: 'API key required for POST requests' }); + return; + } + + if (!validateApiKey(apiKey)) { + logger.warn( + { + method: req.method, + url: req.url, + ip: req.ip, + }, + 'POST request with invalid API key', + ); + res.status(403).json({ error: 'Invalid API key' }); + return; + } + + next(); + }; +} diff --git a/api-relay/src/scripts/migrate-to-db.ts b/api-relay/src/scripts/migrate-to-db.ts new file mode 100644 index 0000000..67506f2 --- /dev/null +++ b/api-relay/src/scripts/migrate-to-db.ts @@ -0,0 +1,82 @@ +import { DatabaseStorageService } from '../services/database.js'; +import { promises as fs } from 'fs'; +import { join } from 'path'; +import type { StoredMessage, StoredSignature, StoredKey } from '../types/message.js'; + +/** + * Migration script from JSON storage to SQLite database. + * Usage: tsx src/scripts/migrate-to-db.ts + */ +async function migrate(): Promise { + const storagePath = process.argv[2] ?? './data'; + const jsonPath = join(storagePath, 'messages.json'); + + console.log(`Reading JSON data from ${jsonPath}...`); + let raw: string; + try { + raw = await fs.readFile(jsonPath, 'utf-8'); + } catch (err) { + if (err instanceof Error && 'code' in err && err.code === 'ENOENT') { + console.log('No JSON file found, nothing to migrate.'); + process.exit(0); + } + throw err; + } + + type StorageSchema = { + messages?: Array<[string, StoredMessage]>; + seenHashes?: string[]; + signatures?: Array<[string, StoredSignature[]]>; + keys?: Array<[string, StoredKey[]]>; + }; + + const parsed = JSON.parse(raw) as StorageSchema; + + console.log('Initializing database...'); + const db = new DatabaseStorageService(storagePath); + await db.initialize(); + + console.log('Migrating data...'); + + if (parsed.messages) { + console.log(`Migrating ${parsed.messages.length} messages...`); + for (const [_hash, msg] of parsed.messages) { + db.storeMessage(msg); + } + } + + if (parsed.seenHashes) { + console.log(`Migrating ${parsed.seenHashes.length} seen hashes...`); + for (const hash of parsed.seenHashes) { + db.markHashSeen(hash); + } + } + + if (parsed.signatures) { + console.log(`Migrating ${parsed.signatures.length} signature groups...`); + for (const [_hash, items] of parsed.signatures) { + for (const sig of items) { + db.storeSignature(sig); + } + } + } + + if (parsed.keys) { + console.log(`Migrating ${parsed.keys.length} key groups...`); + for (const [_hash, items] of parsed.keys) { + for (const key of items) { + db.storeKey(key); + } + } + } + + console.log('Migration completed successfully!'); + console.log('You can now backup and remove the old messages.json file if desired.'); + + db.close(); +} + +migrate().catch((error) => { + console.error('Migration failed:', error); + process.exit(1); +}); diff --git a/api-relay/src/services/apiKeyService.ts b/api-relay/src/services/apiKeyService.ts new file mode 100644 index 0000000..8487630 --- /dev/null +++ b/api-relay/src/services/apiKeyService.ts @@ -0,0 +1,57 @@ +import { randomBytes } from 'crypto'; +import type { DatabaseStorageService } from './database.js'; +import { logger } from '../lib/logger.js'; + +/** + * Service for managing API keys. + */ +export class ApiKeyService { + constructor(private db: DatabaseStorageService) {} + + /** + * Generate a new API key. + * Returns the full key (only shown once) and a prefix for identification. + */ + generateApiKey(description?: string): { key: string; prefix: string } { + const key = `relay_${randomBytes(32).toString('hex')}`; + const prefix = key.slice(0, 16); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const crypto = require('crypto'); + const keyHash = crypto.createHash('sha256').update(key).digest('hex'); + + this.db.storeApiKey(keyHash, prefix, description); + + logger.info({ prefix, description }, 'API key generated'); + return { key, prefix }; + } + + /** + * Validate an API key. + */ + validateApiKey(apiKey: string): boolean { + return this.db.validateApiKey(apiKey); + } + + /** + * List all API keys (prefixes only, for security). + */ + listApiKeys(): Array<{ + prefix: string; + created_at: number; + last_used_at: number | null; + description: string | null; + }> { + return this.db.listApiKeys(); + } + + /** + * Delete an API key by prefix. + */ + deleteApiKey(prefix: string): boolean { + const deleted = this.db.deleteApiKey(prefix); + if (deleted) { + logger.info({ prefix }, 'API key deleted'); + } + return deleted; + } +} diff --git a/api-relay/src/services/database.ts b/api-relay/src/services/database.ts new file mode 100644 index 0000000..35c9082 --- /dev/null +++ b/api-relay/src/services/database.ts @@ -0,0 +1,479 @@ +import Database from 'better-sqlite3'; +import { join } from 'path'; +import { promises as fs } from 'fs'; +import type { StoredMessage, StoredSignature, StoredKey } from '../types/message.js'; +import { logger } from '../lib/logger.js'; + +/** + * SQLite-based storage service. + * Replaces in-memory storage for production use. + */ +export class DatabaseStorageService { + private db: Database.Database; + private dbPath: string; + + constructor(storagePath: string) { + this.dbPath = join(storagePath, 'relay.db'); + } + + /** + * Initialize database (create tables, migrate if needed). + */ + async initialize(): Promise { + await fs.mkdir(join(this.dbPath, '..'), { recursive: true }); + this.db = new Database(this.dbPath); + this.db.pragma('journal_mode = WAL'); + this.createTables(); + logger.info({ dbPath: this.dbPath }, 'Database initialized'); + } + + /** + * Create database tables. + */ + private createTables(): void { + // Messages table + this.db.exec(` + CREATE TABLE IF NOT EXISTS messages ( + hash TEXT PRIMARY KEY, + message_chiffre TEXT NOT NULL, + datajson_public TEXT NOT NULL, + received_at INTEGER NOT NULL, + relayed INTEGER NOT NULL DEFAULT 0 + ) + `); + + // Service index for efficient filtering + this.db.exec(` + CREATE TABLE IF NOT EXISTS message_services ( + hash TEXT NOT NULL, + service_uuid TEXT NOT NULL, + FOREIGN KEY (hash) REFERENCES messages(hash) ON DELETE CASCADE, + PRIMARY KEY (hash, service_uuid) + ) + `); + + // Seen hashes (for deduplication) + this.db.exec(` + CREATE TABLE IF NOT EXISTS seen_hashes ( + hash TEXT PRIMARY KEY + ) + `); + + // Signatures table + this.db.exec(` + CREATE TABLE IF NOT EXISTS signatures ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash_cible TEXT NOT NULL, + signature_hash TEXT NOT NULL, + cle_publique TEXT NOT NULL, + signature TEXT NOT NULL, + nonce TEXT NOT NULL, + materiel TEXT, + received_at INTEGER NOT NULL, + relayed INTEGER NOT NULL DEFAULT 0 + ) + `); + + // Keys table + this.db.exec(` + CREATE TABLE IF NOT EXISTS keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash_message TEXT NOT NULL, + algo TEXT NOT NULL, + params TEXT NOT NULL, + cle_chiffree TEXT, + df_ecdh_scannable TEXT NOT NULL, + received_at INTEGER NOT NULL, + relayed INTEGER NOT NULL DEFAULT 0 + ) + `); + + // API keys table + this.db.exec(` + CREATE TABLE IF NOT EXISTS api_keys ( + key_hash TEXT PRIMARY KEY, + key_prefix TEXT NOT NULL, + created_at INTEGER NOT NULL, + last_used_at INTEGER, + description TEXT + ) + `); + + // Create indexes + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_messages_received_at ON messages(received_at); + CREATE INDEX IF NOT EXISTS idx_message_services_service ON message_services(service_uuid); + CREATE INDEX IF NOT EXISTS idx_signatures_hash_cible ON signatures(hash_cible); + CREATE INDEX IF NOT EXISTS idx_keys_hash_message ON keys(hash_message); + CREATE INDEX IF NOT EXISTS idx_keys_received_at ON keys(received_at); + `); + } + + /** + * Check if a hash has been seen before (deduplication). + */ + hasSeenHash(hash: string): boolean { + const stmt = this.db.prepare('SELECT 1 FROM seen_hashes WHERE hash = ?'); + return stmt.get(hash) !== undefined; + } + + /** + * Mark a hash as seen. + */ + markHashSeen(hash: string): void { + const stmt = this.db.prepare('INSERT OR IGNORE INTO seen_hashes (hash) VALUES (?)'); + stmt.run(hash); + } + + /** + * Store an encrypted message. + */ + storeMessage(msg: StoredMessage): void { + const insertMsg = this.db.prepare(` + INSERT OR REPLACE INTO messages (hash, message_chiffre, datajson_public, received_at, relayed) + VALUES (?, ?, ?, ?, ?) + `); + + const insertService = this.db.prepare(` + INSERT OR IGNORE INTO message_services (hash, service_uuid) VALUES (?, ?) + `); + + const transaction = this.db.transaction(() => { + insertMsg.run( + msg.msg.hash, + msg.msg.message_chiffre, + JSON.stringify(msg.msg.datajson_public), + msg.received_at, + msg.relayed ? 1 : 0, + ); + + for (const serviceUuid of msg.msg.datajson_public.services_uuid) { + insertService.run(msg.msg.hash, serviceUuid); + } + + this.markHashSeen(msg.msg.hash); + }); + + transaction(); + } + + /** + * Get an encrypted message by hash. + */ + getMessage(hash: string): StoredMessage | undefined { + const stmt = this.db.prepare('SELECT * FROM messages WHERE hash = ?'); + const row = stmt.get(hash) as + | { + hash: string; + message_chiffre: string; + datajson_public: string; + received_at: number; + relayed: number; + } + | undefined; + + if (row === undefined) { + return undefined; + } + + return { + msg: { + hash: row.hash, + message_chiffre: row.message_chiffre, + datajson_public: JSON.parse(row.datajson_public), + }, + received_at: row.received_at, + relayed: row.relayed === 1, + }; + } + + /** + * Get messages in a time window, optionally filtered by service. + */ + getMessages( + start: number, + end: number, + serviceUuid?: string, + ): StoredMessage[] { + let query: string; + let params: unknown[]; + + if (serviceUuid !== undefined) { + query = ` + SELECT m.* FROM messages m + INNER JOIN message_services ms ON m.hash = ms.hash + WHERE ms.service_uuid = ? AND m.received_at >= ? AND m.received_at <= ? + ORDER BY m.received_at ASC + `; + params = [serviceUuid, start, end]; + } else { + query = ` + SELECT * FROM messages + WHERE received_at >= ? AND received_at <= ? + ORDER BY received_at ASC + `; + params = [start, end]; + } + + const stmt = this.db.prepare(query); + const rows = stmt.all(...params) as Array<{ + hash: string; + message_chiffre: string; + datajson_public: string; + received_at: number; + relayed: number; + }>; + + return rows.map((row) => ({ + msg: { + hash: row.hash, + message_chiffre: row.message_chiffre, + datajson_public: JSON.parse(row.datajson_public), + }, + received_at: row.received_at, + relayed: row.relayed === 1, + })); + } + + /** + * Store a signature. + */ + storeSignature(sig: StoredSignature): void { + const hash = sig.msg.hash_cible ?? sig.msg.signature.hash; + const stmt = this.db.prepare(` + INSERT INTO signatures (hash_cible, signature_hash, cle_publique, signature, nonce, materiel, received_at, relayed) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + hash, + sig.msg.signature.hash, + sig.msg.signature.cle_publique, + sig.msg.signature.signature, + sig.msg.signature.nonce, + sig.msg.signature.materiel !== undefined ? JSON.stringify(sig.msg.signature.materiel) : null, + sig.received_at, + sig.relayed ? 1 : 0, + ); + } + + /** + * Get signatures for a message hash. + */ + getSignatures(hash: string): StoredSignature[] { + const stmt = this.db.prepare('SELECT * FROM signatures WHERE hash_cible = ?'); + const rows = stmt.all(hash) as Array<{ + id: number; + hash_cible: string; + signature_hash: string; + cle_publique: string; + signature: string; + nonce: string; + materiel: string | null; + received_at: number; + relayed: number; + }>; + + return rows.map((row) => ({ + msg: { + signature: { + hash: row.signature_hash, + cle_publique: row.cle_publique, + signature: row.signature, + nonce: row.nonce, + materiel: row.materiel !== null ? JSON.parse(row.materiel) : undefined, + }, + hash_cible: row.hash_cible, + }, + received_at: row.received_at, + relayed: row.relayed === 1, + })); + } + + /** + * Store a decryption key. + */ + storeKey(key: StoredKey): void { + const stmt = this.db.prepare(` + INSERT INTO keys (hash_message, algo, params, cle_chiffree, df_ecdh_scannable, received_at, relayed) + VALUES (?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + key.msg.hash_message, + key.msg.cle_de_chiffrement_message.algo, + JSON.stringify(key.msg.cle_de_chiffrement_message.params), + key.msg.cle_de_chiffrement_message.cle_chiffree ?? null, + key.msg.df_ecdh_scannable, + key.received_at, + key.relayed ? 1 : 0, + ); + } + + /** + * Get decryption keys for a message hash. + */ + getKeys(hash: string): StoredKey[] { + const stmt = this.db.prepare('SELECT * FROM keys WHERE hash_message = ?'); + const rows = stmt.all(hash) as Array<{ + id: number; + hash_message: string; + algo: string; + params: string; + cle_chiffree: string | null; + df_ecdh_scannable: string; + received_at: number; + relayed: number; + }>; + + return rows.map((row) => ({ + msg: { + hash_message: row.hash_message, + cle_de_chiffrement_message: { + algo: row.algo, + params: JSON.parse(row.params), + cle_chiffree: row.cle_chiffree ?? undefined, + }, + df_ecdh_scannable: row.df_ecdh_scannable, + }, + received_at: row.received_at, + relayed: row.relayed === 1, + })); + } + + /** + * Get all stored keys whose received_at is in [start, end]. + */ + getKeysInWindow(start: number, end: number): StoredKey[] { + const stmt = this.db.prepare( + 'SELECT * FROM keys WHERE received_at >= ? AND received_at <= ? ORDER BY received_at ASC', + ); + const rows = stmt.all(start, end) as Array<{ + id: number; + hash_message: string; + algo: string; + params: string; + cle_chiffree: string | null; + df_ecdh_scannable: string; + received_at: number; + relayed: number; + }>; + + return rows.map((row) => ({ + msg: { + hash_message: row.hash_message, + cle_de_chiffrement_message: { + algo: row.algo, + params: JSON.parse(row.params), + cle_chiffree: row.cle_chiffree ?? undefined, + }, + df_ecdh_scannable: row.df_ecdh_scannable, + }, + received_at: row.received_at, + relayed: row.relayed === 1, + })); + } + + /** + * Get count of seen hashes. + */ + getSeenHashCount(): number { + const stmt = this.db.prepare('SELECT COUNT(*) as count FROM seen_hashes'); + const row = stmt.get() as { count: number }; + return row.count; + } + + /** + * Get all seen hashes. + */ + getSeenHashes(): string[] { + const stmt = this.db.prepare('SELECT hash FROM seen_hashes'); + const rows = stmt.all() as Array<{ hash: string }>; + return rows.map((r) => r.hash); + } + + /** + * Get count of signatures. + */ + getSignatureCount(): number { + const stmt = this.db.prepare('SELECT COUNT(*) as count FROM signatures'); + const row = stmt.get() as { count: number }; + return row.count; + } + + /** + * Get count of keys. + */ + getKeyCount(): number { + const stmt = this.db.prepare('SELECT COUNT(*) as count FROM keys'); + const row = stmt.get() as { count: number }; + return row.count; + } + + /** + * Store an API key. + */ + storeApiKey(keyHash: string, keyPrefix: string, description?: string): void { + const stmt = this.db.prepare(` + INSERT INTO api_keys (key_hash, key_prefix, created_at, description) + VALUES (?, ?, ?, ?) + `); + stmt.run(keyHash, keyPrefix, Date.now(), description ?? null); + } + + /** + * Validate an API key. + */ + validateApiKey(apiKey: string): boolean { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const crypto = require('crypto'); + const keyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); + const stmt = this.db.prepare('SELECT key_hash FROM api_keys WHERE key_hash = ?'); + const row = stmt.get(keyHash) as { key_hash: string } | undefined; + if (row !== undefined) { + // Update last_used_at + const updateStmt = this.db.prepare( + 'UPDATE api_keys SET last_used_at = ? WHERE key_hash = ?', + ); + updateStmt.run(Date.now(), keyHash); + return true; + } + return false; + } + + /** + * List all API keys (for management). + */ + listApiKeys(): Array<{ + key_prefix: string; + created_at: number; + last_used_at: number | null; + description: string | null; + }> { + const stmt = this.db.prepare( + 'SELECT key_prefix, created_at, last_used_at, description FROM api_keys ORDER BY created_at DESC', + ); + return stmt.all() as Array<{ + key_prefix: string; + created_at: number; + last_used_at: number | null; + description: string | null; + }>; + } + + /** + * Delete an API key by prefix. + */ + deleteApiKey(keyPrefix: string): boolean { + const stmt = this.db.prepare('DELETE FROM api_keys WHERE key_prefix = ?'); + const result = stmt.run(keyPrefix); + return result.changes > 0; + } + + /** + * Close database connection. + */ + close(): void { + this.db.close(); + } +} diff --git a/api-relay/src/services/storageAdapter.ts b/api-relay/src/services/storageAdapter.ts new file mode 100644 index 0000000..90fe90c --- /dev/null +++ b/api-relay/src/services/storageAdapter.ts @@ -0,0 +1,88 @@ +import type { + StoredMessage, + StoredSignature, + StoredKey, +} from '../types/message.js'; +import type { DatabaseStorageService } from './database.js'; + +/** + * Adapter to make DatabaseStorageService compatible with StorageService interface. + * This allows existing code to work with the new database implementation. + */ +export class StorageAdapter { + constructor(private db: DatabaseStorageService) {} + + hasSeenHash(hash: string): boolean { + return this.db.hasSeenHash(hash); + } + + markHashSeen(hash: string): void { + this.db.markHashSeen(hash); + } + + storeMessage(msg: StoredMessage): void { + this.db.storeMessage(msg); + } + + getMessage(hash: string): StoredMessage | undefined { + return this.db.getMessage(hash); + } + + getMessages( + start: number, + end: number, + serviceUuid?: string, + ): StoredMessage[] { + return this.db.getMessages(start, end, serviceUuid); + } + + storeSignature(sig: StoredSignature): void { + this.db.storeSignature(sig); + } + + getSignatures(hash: string): StoredSignature[] { + return this.db.getSignatures(hash); + } + + storeKey(key: StoredKey): void { + this.db.storeKey(key); + } + + getKeys(hash: string): StoredKey[] { + return this.db.getKeys(hash); + } + + getKeysInWindow(start: number, end: number): StoredKey[] { + return this.db.getKeysInWindow(start, end); + } + + getSeenHashCount(): number { + return this.db.getSeenHashCount(); + } + + getSeenHashes(): string[] { + return this.db.getSeenHashes(); + } + + getSignatureCount(): number { + return this.db.getSignatureCount(); + } + + getKeyCount(): number { + return this.db.getKeyCount(); + } + + /** + * No-op for compatibility (database handles persistence automatically). + */ + async saveToDisk(): Promise { + // Database is already persistent, no need to save + } + + /** + * No-op for compatibility. + */ + async initialize(): Promise { + // Already initialized in database service + } +} diff --git a/data/sync-utxos.log b/data/sync-utxos.log index be080fb..2f9988b 100644 --- a/data/sync-utxos.log +++ b/data/sync-utxos.log @@ -1,100 +1,100 @@ - ⏳ Traitement: 130000/189710 UTXOs insérés... - ⏳ Traitement: 140000/189710 UTXOs insérés... - ⏳ Traitement: 150000/189710 UTXOs insérés... - ⏳ Traitement: 160000/189710 UTXOs insérés... - ⏳ Traitement: 170000/189710 UTXOs insérés... - ⏳ Traitement: 180000/189710 UTXOs insérés... - ⏳ Traitement: 189710/189710 UTXOs insérés... + ⏳ Traitement: 200000/225802 UTXOs insérés... + ⏳ Traitement: 210000/225802 UTXOs insérés... + ⏳ Traitement: 220000/225802 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... 📊 Résumé: - - UTXOs vérifiés: 66109 - - UTXOs toujours disponibles: 66109 + - UTXOs vérifiés: 61609 + - UTXOs toujours disponibles: 61609 - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 68398 - - Dépensés: 2294 - - Non dépensés: 66104 + - Dépensés: 6789 + - Non dépensés: 61609 ✅ Synchronisation terminée 🔍 Démarrage de la synchronisation des UTXOs dépensés... -📊 UTXOs à vérifier: 64639 +📊 UTXOs à vérifier: 61609 📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 201481 +📊 UTXOs disponibles dans Bitcoin: 225826 💾 Création de la table temporaire... 💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/201481 UTXOs insérés... - ⏳ Traitement: 20000/201481 UTXOs insérés... - ⏳ Traitement: 30000/201481 UTXOs insérés... - ⏳ Traitement: 40000/201481 UTXOs insérés... - ⏳ Traitement: 50000/201481 UTXOs insérés... - ⏳ Traitement: 60000/201481 UTXOs insérés... - ⏳ Traitement: 70000/201481 UTXOs insérés... - ⏳ Traitement: 80000/201481 UTXOs insérés... - ⏳ Traitement: 90000/201481 UTXOs insérés... - ⏳ Traitement: 100000/201481 UTXOs insérés... - ⏳ Traitement: 110000/201481 UTXOs insérés... - ⏳ Traitement: 120000/201481 UTXOs insérés... - ⏳ Traitement: 130000/201481 UTXOs insérés... - ⏳ Traitement: 140000/201481 UTXOs insérés... - ⏳ Traitement: 150000/201481 UTXOs insérés... - ⏳ Traitement: 160000/201481 UTXOs insérés... - ⏳ Traitement: 170000/201481 UTXOs insérés... - ⏳ Traitement: 180000/201481 UTXOs insérés... - ⏳ Traitement: 190000/201481 UTXOs insérés... - ⏳ Traitement: 200000/201481 UTXOs insérés... + ⏳ Traitement: 10000/225826 UTXOs insérés... + ⏳ Traitement: 20000/225826 UTXOs insérés... + ⏳ Traitement: 30000/225826 UTXOs insérés... + ⏳ Traitement: 40000/225826 UTXOs insérés... + ⏳ Traitement: 50000/225826 UTXOs insérés... + ⏳ Traitement: 60000/225826 UTXOs insérés... + ⏳ Traitement: 70000/225826 UTXOs insérés... + ⏳ Traitement: 80000/225826 UTXOs insérés... + ⏳ Traitement: 90000/225826 UTXOs insérés... + ⏳ Traitement: 100000/225826 UTXOs insérés... + ⏳ Traitement: 110000/225826 UTXOs insérés... + ⏳ Traitement: 120000/225826 UTXOs insérés... + ⏳ Traitement: 130000/225826 UTXOs insérés... + ⏳ Traitement: 140000/225826 UTXOs insérés... + ⏳ Traitement: 150000/225826 UTXOs insérés... + ⏳ Traitement: 160000/225826 UTXOs insérés... + ⏳ Traitement: 170000/225826 UTXOs insérés... + ⏳ Traitement: 180000/225826 UTXOs insérés... + ⏳ Traitement: 190000/225826 UTXOs insérés... + ⏳ Traitement: 200000/225826 UTXOs insérés... + ⏳ Traitement: 210000/225826 UTXOs insérés... + ⏳ Traitement: 220000/225826 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... 📊 Résumé: - - UTXOs vérifiés: 64639 - - UTXOs toujours disponibles: 64639 + - UTXOs vérifiés: 61609 + - UTXOs toujours disponibles: 61609 - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 68398 - - Dépensés: 3759 - - Non dépensés: 64639 + - Dépensés: 6789 + - Non dépensés: 61609 ✅ Synchronisation terminée 🔍 Démarrage de la synchronisation des UTXOs dépensés... -📊 UTXOs à vérifier: 64412 +📊 UTXOs à vérifier: 61609 📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 203310 +📊 UTXOs disponibles dans Bitcoin: 225837 💾 Création de la table temporaire... 💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/203310 UTXOs insérés... - ⏳ Traitement: 20000/203310 UTXOs insérés... - ⏳ Traitement: 30000/203310 UTXOs insérés... - ⏳ Traitement: 40000/203310 UTXOs insérés... - ⏳ Traitement: 50000/203310 UTXOs insérés... - ⏳ Traitement: 60000/203310 UTXOs insérés... - ⏳ Traitement: 70000/203310 UTXOs insérés... - ⏳ Traitement: 80000/203310 UTXOs insérés... - ⏳ Traitement: 90000/203310 UTXOs insérés... - ⏳ Traitement: 100000/203310 UTXOs insérés... - ⏳ Traitement: 110000/203310 UTXOs insérés... - ⏳ Traitement: 120000/203310 UTXOs insérés... - ⏳ Traitement: 130000/203310 UTXOs insérés... - ⏳ Traitement: 140000/203310 UTXOs insérés... - ⏳ Traitement: 150000/203310 UTXOs insérés... - ⏳ Traitement: 160000/203310 UTXOs insérés... - ⏳ Traitement: 170000/203310 UTXOs insérés... - ⏳ Traitement: 180000/203310 UTXOs insérés... - ⏳ Traitement: 190000/203310 UTXOs insérés... - ⏳ Traitement: 200000/203310 UTXOs insérés... + ⏳ Traitement: 10000/225837 UTXOs insérés... + ⏳ Traitement: 20000/225837 UTXOs insérés... + ⏳ Traitement: 30000/225837 UTXOs insérés... + ⏳ Traitement: 40000/225837 UTXOs insérés... + ⏳ Traitement: 50000/225837 UTXOs insérés... + ⏳ Traitement: 60000/225837 UTXOs insérés... + ⏳ Traitement: 70000/225837 UTXOs insérés... + ⏳ Traitement: 80000/225837 UTXOs insérés... + ⏳ Traitement: 90000/225837 UTXOs insérés... + ⏳ Traitement: 100000/225837 UTXOs insérés... + ⏳ Traitement: 110000/225837 UTXOs insérés... + ⏳ Traitement: 120000/225837 UTXOs insérés... + ⏳ Traitement: 130000/225837 UTXOs insérés... + ⏳ Traitement: 140000/225837 UTXOs insérés... + ⏳ Traitement: 150000/225837 UTXOs insérés... + ⏳ Traitement: 160000/225837 UTXOs insérés... + ⏳ Traitement: 170000/225837 UTXOs insérés... + ⏳ Traitement: 180000/225837 UTXOs insérés... + ⏳ Traitement: 190000/225837 UTXOs insérés... + ⏳ Traitement: 200000/225837 UTXOs insérés... + ⏳ Traitement: 210000/225837 UTXOs insérés... + ⏳ Traitement: 220000/225837 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... 📊 Résumé: - - UTXOs vérifiés: 64412 - - UTXOs toujours disponibles: 64412 + - UTXOs vérifiés: 61609 + - UTXOs toujours disponibles: 61609 - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 68398 - - Dépensés: 3986 - - Non dépensés: 64412 + - Dépensés: 6789 + - Non dépensés: 61609 ✅ Synchronisation terminée diff --git a/features/OPTIONS_NON_IMPLENTEES.md b/features/OPTIONS_NON_IMPLENTEES.md new file mode 100644 index 0000000..15fc333 --- /dev/null +++ b/features/OPTIONS_NON_IMPLENTEES.md @@ -0,0 +1,221 @@ +# Options non implémentées - Analyse d'intérêt + +**Author:** Équipe 4NK +**Date:** 2026-01-28 + +## 1. Merkle trees (optionnel) + +### Description +Checkpointing / accélération de scan pour optimiser la synchronisation à grande échelle. + +### Intérêt + +**Avantages potentiels :** +- **Performance** : Réduction significative du nombre de requêtes nécessaires pour synchroniser de grandes quantités de données +- **Scalabilité** : Permet de gérer des volumes importants (millions de messages) sans dégradation de performance +- **Efficacité réseau** : Réduction de la bande passante utilisée lors des synchronisations +- **Checkpointing** : Possibilité de reprendre une synchronisation depuis un point précis sans tout re-scanner + +**Cas d'usage :** +- Services avec un très grand nombre de messages (millions) +- Synchronisations fréquentes sur de grandes fenêtres temporelles +- Réseaux avec bande passante limitée +- Besoin de synchronisation incrémentale efficace + +### Inconvénients / Complexité + +**Complexité technique :** +- Implémentation complexe (construction d'arbres de Merkle, gestion des segments) +- Nécessite une coordination entre client et serveur (relais doit supporter Merkle) +- Gestion des conflits et des branches d'arbres +- Maintenance et debugging plus difficiles + +**Coût de développement :** +- Temps de développement important +- Tests complexes (volumes importants, cas limites) +- Documentation et formation nécessaires + +**Alternatives existantes :** +- **Bloom filter** : Déjà implémenté, réduit efficacement les requêtes inutiles +- **Hash cache** : Déjà implémenté, évite de traiter les messages déjà vus +- **Fenêtre de scan configurable** : Permet de limiter la portée des synchronisations + +### Recommandation + +**Intérêt : MOYEN à FAIBLE** + +**Raisons :** +1. **Bloom filter suffisant** : Le Bloom filter déjà implémenté couvre la majorité des cas d'usage d'optimisation +2. **Complexité vs bénéfice** : La complexité d'implémentation est élevée pour un gain marginal dans la plupart des cas +3. **Volumes actuels** : Les volumes de données actuels ne justifient pas nécessairement cette optimisation +4. **Dépendance serveur** : Nécessite une modification des relais pour supporter Merkle, ce qui limite l'adoption + +**Quand l'implémenter :** +- Si les volumes de messages deviennent très importants (millions+) +- Si les synchronisations deviennent un goulot d'étranglement mesurable +- Si le Bloom filter ne suffit plus pour optimiser les requêtes +- Si les relais sont prêts à supporter Merkle + +**Conclusion :** Optionnel, à considérer uniquement si un besoin réel de performance à grande échelle est identifié et mesuré. + +--- + +## 2. Évolutions futures api-relay (optionnel) + +### Description +Plusieurs évolutions possibles pour api-relay : base de données, authentification, WebSocket, compression, etc. + +### Analyse par évolution + +#### 2.1 Base de données (SQLite, PostgreSQL) + +**Intérêt : ÉLEVÉ** + +**Avantages :** +- **Persistance fiable** : Données persistées de manière fiable, pas de perte en cas de redémarrage +- **Performance** : Requêtes optimisées, indexation efficace +- **Scalabilité** : Gestion de volumes importants +- **Transactions** : Garanties ACID pour la cohérence des données +- **Maintenance** : Outils de backup, monitoring, maintenance standardisés + +**Inconvénients :** +- Configuration et déploiement plus complexes +- Nécessite une migration depuis le stockage en mémoire actuel + +**Recommandation :** **À implémenter en production**. Le stockage en mémoire actuel est suffisant pour le développement mais pas pour la production. + +--- + +#### 2.2 Authentification pour endpoints POST + +**Intérêt : MOYEN** + +**Avantages :** +- **Sécurité** : Protection contre les abus (spam, DoS) +- **Traçabilité** : Identification des sources de messages +- **Contrôle d'accès** : Limitation à des clients autorisés + +**Inconvénients :** +- Complexité de gestion des clés/tokens +- Nécessite une infrastructure d'authentification +- Peut limiter la décentralisation (nécessite une autorité pour émettre les tokens) + +**Recommandation :** **Optionnel**. Utile si des problèmes d'abus sont identifiés. Le modèle actuel (pull-only, déduplication par hash) offre déjà une certaine protection. + +--- + +#### 2.3 Compression des messages stockés + +**Intérêt : FAIBLE** + +**Avantages :** +- Réduction de l'espace disque +- Réduction de la bande passante réseau + +**Inconvénients :** +- CPU supplémentaire pour compression/décompression +- Complexité de gestion (format, version) +- Les messages sont déjà relativement petits (JSON) + +**Recommendation :** **Non prioritaire**. Les messages sont déjà compacts (JSON). La compression n'apporterait qu'un gain marginal. + +--- + +#### 2.4 Indexation avancée (service UUID, type UUID) + +**Intérêt : MOYEN** + +**Avantages :** +- Requêtes plus rapides pour des filtres spécifiques +- Meilleure performance sur de gros volumes + +**Inconvénients :** +- Complexité de gestion des index +- Maintenance supplémentaire + +**Recommandation :** **Optionnel**. L'indexation par `service_uuid` est déjà partiellement implémentée. À améliorer si les volumes augmentent. + +--- + +#### 2.5 WebSocket pour notifications en temps réel + +**Intérêt : FAIBLE à MOYEN** + +**Avantages :** +- Notifications en temps réel (push) +- Réduction de la latence +- Meilleure expérience utilisateur + +**Inconvénients :** +- **Contradiction avec le modèle pull-only** : Le modèle actuel est volontairement pull-only pour la décentralisation +- Complexité de gestion des connexions WebSocket +- Nécessite une infrastructure serveur plus complexe +- Peut compromettre la décentralisation (dépendance au serveur) + +**Recommandation :** **Non recommandé**. Contredit le principe de décentralisation et le modèle pull-only. Le polling actuel est suffisant et plus robuste. + +--- + +#### 2.6 Métriques Prometheus + +**Intérêt : ÉLEVÉ** + +**Avantages :** +- Monitoring et observabilité +- Détection proactive des problèmes +- Métriques de performance + +**Inconvénients :** +- Configuration supplémentaire +- Infrastructure de monitoring nécessaire + +**Recommandation :** **À implémenter en production**. Déjà partiellement implémenté (`GET /metrics`). À compléter selon les besoins de monitoring. + +--- + +#### 2.7 Logging structuré (Winston, Pino) + +**Intérêt : ÉLEVÉ** + +**Avantages :** +- Meilleure traçabilité +- Analyse des logs facilitée +- Debugging plus efficace + +**Inconvénients :** +- Configuration supplémentaire +- Gestion des logs (rotation, stockage) + +**Recommandation :** **Déjà implémenté**. Pino est déjà utilisé dans api-relay. À maintenir et améliorer si nécessaire. + +--- + +## Synthèse + +### À implémenter en priorité +1. **Base de données** (SQLite/PostgreSQL) - Production +2. **Métriques Prometheus** - Compléter l'implémentation existante + +### Optionnel selon besoins +3. **Authentification POST** - Si problèmes d'abus +4. **Indexation avancée** - Si volumes importants + +### Non recommandé +5. **Compression** - Gain marginal +6. **WebSocket** - Contredit le modèle pull-only décentralisé + +### Déjà en place +7. **Logging structuré** - Pino déjà implémenté +8. **Rate limiting** - Déjà implémenté +9. **CORS** - Déjà implémenté +10. **Bloom filter** - Déjà implémenté + +--- + +## Conclusion + +Les évolutions les plus pertinentes pour api-relay sont : +- **Base de données** : Nécessaire pour la production +- **Métriques** : Utile pour le monitoring + +Les autres évolutions sont optionnelles et dépendent des besoins spécifiques identifiés en production. diff --git a/service-login-verify/dist/index.d.ts b/service-login-verify/dist/index.d.ts index 3352b28..842cc98 100644 --- a/service-login-verify/dist/index.d.ts +++ b/service-login-verify/dist/index.d.ts @@ -1,5 +1,6 @@ export { verifyLoginProof, } from './verifyLoginProof.js'; export { NonceCache } from './nonceCache.js'; +export { PersistentNonceCache } from './persistentNonceCache.js'; export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js'; export type { LoginProof, Validateurs, VerifyLoginProofContext, VerifyLoginProofResult, NonceCacheLike, } from './types.js'; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/service-login-verify/dist/index.d.ts.map b/service-login-verify/dist/index.d.ts.map index 827b05d..1949a19 100644 --- a/service-login-verify/dist/index.d.ts.map +++ b/service-login-verify/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAC;AAC9E,YAAY,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,GACf,MAAM,YAAY,CAAC"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,kCAAkC,EAAE,MAAM,0BAA0B,CAAC;AAC9E,YAAY,EACV,UAAU,EACV,WAAW,EACX,uBAAuB,EACvB,sBAAsB,EACtB,cAAc,GACf,MAAM,YAAY,CAAC"} \ No newline at end of file diff --git a/service-login-verify/dist/index.js b/service-login-verify/dist/index.js index 0267b66..668e6a0 100644 --- a/service-login-verify/dist/index.js +++ b/service-login-verify/dist/index.js @@ -1,3 +1,4 @@ export { verifyLoginProof, } from './verifyLoginProof.js'; export { NonceCache } from './nonceCache.js'; +export { PersistentNonceCache } from './persistentNonceCache.js'; export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js'; diff --git a/service-login-verify/dist/persistentNonceCache.d.ts b/service-login-verify/dist/persistentNonceCache.d.ts new file mode 100644 index 0000000..2324551 --- /dev/null +++ b/service-login-verify/dist/persistentNonceCache.d.ts @@ -0,0 +1,36 @@ +import type { NonceCacheLike } from './types.js'; +/** + * Persistent nonce cache using IndexedDB (browser) or localStorage (fallback). + * Implements NonceCacheLike interface for use with verifyLoginProof. + */ +export declare class PersistentNonceCache implements NonceCacheLike { + private readonly ttlMs; + private readonly storageKey; + private readonly useIndexedDB; + private db; + constructor(ttlMs?: number, storageKey?: string); + /** + * Initialize IndexedDB if available. + */ + init(): Promise; + /** + * Check if nonce is valid (not seen within TTL). Records nonce on success. + * Note: IndexedDB operations are async, but NonceCacheLike interface requires sync. + * This implementation uses localStorage for synchronous access. + * For true IndexedDB persistence, consider making the interface async. + */ + isValid(nonce: string, timestamp: number): boolean; + /** + * Synchronous validation using localStorage (fallback). + */ + private isValidSync; + /** + * Cleanup expired entries (localStorage). + */ + private cleanupSync; + /** + * Clear all entries. + */ + clear(): void; +} +//# sourceMappingURL=persistentNonceCache.d.ts.map \ No newline at end of file diff --git a/service-login-verify/dist/persistentNonceCache.d.ts.map b/service-login-verify/dist/persistentNonceCache.d.ts.map new file mode 100644 index 0000000..5fb59fa --- /dev/null +++ b/service-login-verify/dist/persistentNonceCache.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"persistentNonceCache.d.ts","sourceRoot":"","sources":["../src/persistentNonceCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,EAAE,CAA4B;gBAE1B,KAAK,GAAE,MAAgB,EAAE,UAAU,GAAE,MAAsB;IAMvE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B3B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlD;;OAEG;IACH,OAAO,CAAC,WAAW;IAiBnB;;OAEG;IACH,OAAO,CAAC,WAAW;IAoBnB;;OAEG;IACH,KAAK,IAAI,IAAI;CAkBd"} \ No newline at end of file diff --git a/service-login-verify/dist/persistentNonceCache.js b/service-login-verify/dist/persistentNonceCache.js new file mode 100644 index 0000000..94605c5 --- /dev/null +++ b/service-login-verify/dist/persistentNonceCache.js @@ -0,0 +1,107 @@ +/** + * Persistent nonce cache using IndexedDB (browser) or localStorage (fallback). + * Implements NonceCacheLike interface for use with verifyLoginProof. + */ +export class PersistentNonceCache { + ttlMs; + storageKey; + useIndexedDB; + db = null; + constructor(ttlMs = 3600000, storageKey = 'nonce_cache') { + this.ttlMs = ttlMs; + this.storageKey = storageKey; + this.useIndexedDB = typeof window !== 'undefined' && 'indexedDB' in window; + } + /** + * Initialize IndexedDB if available. + */ + async init() { + if (!this.useIndexedDB) { + return; + } + return new Promise((resolve, reject) => { + const request = indexedDB.open('NonceCacheDB', 1); + request.onerror = () => { + reject(new Error('Failed to open IndexedDB')); + }; + request.onsuccess = () => { + this.db = request.result; + resolve(); + }; + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains('nonces')) { + const store = db.createObjectStore('nonces', { keyPath: 'nonce' }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + } + }; + }); + } + /** + * Check if nonce is valid (not seen within TTL). Records nonce on success. + * Note: IndexedDB operations are async, but NonceCacheLike interface requires sync. + * This implementation uses localStorage for synchronous access. + * For true IndexedDB persistence, consider making the interface async. + */ + isValid(nonce, timestamp) { + return this.isValidSync(nonce, timestamp); + } + /** + * Synchronous validation using localStorage (fallback). + */ + isValidSync(nonce, timestamp) { + const now = Date.now(); + const stored = localStorage.getItem(`${this.storageKey}_${nonce}`); + if (stored !== null) { + const storedTimestamp = parseInt(stored, 10); + if (now - storedTimestamp < this.ttlMs) { + return false; + } + localStorage.removeItem(`${this.storageKey}_${nonce}`); + } + localStorage.setItem(`${this.storageKey}_${nonce}`, timestamp.toString()); + this.cleanupSync(now); + return true; + } + /** + * Cleanup expired entries (localStorage). + */ + cleanupSync(now) { + const keys = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key !== null && key.startsWith(`${this.storageKey}_`)) { + keys.push(key); + } + } + for (const key of keys) { + const stored = localStorage.getItem(key); + if (stored !== null) { + const storedTimestamp = parseInt(stored, 10); + if (now - storedTimestamp >= this.ttlMs) { + localStorage.removeItem(key); + } + } + } + } + /** + * Clear all entries. + */ + clear() { + if (this.useIndexedDB && this.db !== null) { + const transaction = this.db.transaction(['nonces'], 'readwrite'); + const store = transaction.objectStore('nonces'); + store.clear(); + } + const keys = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key !== null && key.startsWith(`${this.storageKey}_`)) { + keys.push(key); + } + } + for (const key of keys) { + localStorage.removeItem(key); + } + } +} diff --git a/service-login-verify/dist/types.d.ts b/service-login-verify/dist/types.d.ts index 975dda3..ffe0172 100644 --- a/service-login-verify/dist/types.d.ts +++ b/service-login-verify/dist/types.d.ts @@ -51,6 +51,6 @@ export interface NonceCacheLike { } export interface VerifyLoginProofResult { accept: boolean; - reason?: 'timestamp_out_of_window' | 'nonce_reused' | 'validators_not_verifiable' | 'no_validator_signature' | 'signature_cle_publique_not_authorized'; + reason?: 'invalid_proof_structure' | 'timestamp_out_of_window' | 'nonce_reused' | 'validators_not_verifiable' | 'no_validator_signature' | 'signature_cle_publique_not_authorized'; } //# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/service-login-verify/dist/types.d.ts.map b/service-login-verify/dist/types.d.ts.map index f43f3f8..1675d9a 100644 --- a/service-login-verify/dist/types.d.ts.map +++ b/service-login-verify/dist/types.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,EAAE,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;CACzD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,uBAAuB,EAAE,oBAAoB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,YAAY,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,+BAA+B;IAC/B,UAAU,EAAE,cAAc,CAAC;IAC3B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CACpD;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,yBAAyB,GAAG,cAAc,GAAG,2BAA2B,GAAG,wBAAwB,GAAG,uCAAuC,CAAC;CACxJ"} \ No newline at end of file +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC,CAAC;IACH,MAAM,EAAE,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;CACzD;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,uBAAuB,EAAE,oBAAoB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,YAAY,EAAE,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,+BAA+B;IAC/B,UAAU,EAAE,cAAc,CAAC;IAC3B,sDAAsD;IACtD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CACpD;AAED,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,yBAAyB,GAAG,yBAAyB,GAAG,cAAc,GAAG,2BAA2B,GAAG,wBAAwB,GAAG,uCAAuC,CAAC;CACpL"} \ No newline at end of file diff --git a/service-login-verify/dist/verifyLoginProof.d.ts b/service-login-verify/dist/verifyLoginProof.d.ts index e6a484d..bfcd5f4 100644 --- a/service-login-verify/dist/verifyLoginProof.d.ts +++ b/service-login-verify/dist/verifyLoginProof.d.ts @@ -2,6 +2,11 @@ import type { LoginProof, VerifyLoginProofContext, VerifyLoginProofResult } from /** * 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 declare function verifyLoginProof(proof: LoginProof, ctx: VerifyLoginProofContext): VerifyLoginProofResult; //# sourceMappingURL=verifyLoginProof.d.ts.map \ No newline at end of file diff --git a/service-login-verify/dist/verifyLoginProof.d.ts.map b/service-login-verify/dist/verifyLoginProof.d.ts.map index bdbcecf..b70c5cc 100644 --- a/service-login-verify/dist/verifyLoginProof.d.ts.map +++ b/service-login-verify/dist/verifyLoginProof.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"verifyLoginProof.d.ts","sourceRoot":"","sources":["../src/verifyLoginProof.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EACV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAyCpB;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,uBAAuB,GAC3B,sBAAsB,CA6CxB"} \ No newline at end of file +{"version":3,"file":"verifyLoginProof.d.ts","sourceRoot":"","sources":["../src/verifyLoginProof.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,UAAU,EACV,uBAAuB,EACvB,sBAAsB,EACvB,MAAM,YAAY,CAAC;AAyCpB;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,uBAAuB,GAC3B,sBAAsB,CA0FxB"} \ No newline at end of file diff --git a/service-login-verify/dist/verifyLoginProof.js b/service-login-verify/dist/verifyLoginProof.js index 7544743..ea159d0 100644 --- a/service-login-verify/dist/verifyLoginProof.js +++ b/service-login-verify/dist/verifyLoginProof.js @@ -26,8 +26,47 @@ function verifySignaturesStrict(hashValue, signatures, allowedPubkeys) { /** * 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, diff --git a/service-login-verify/src/index.ts b/service-login-verify/src/index.ts index 94631e2..3a5182f 100644 --- a/service-login-verify/src/index.ts +++ b/service-login-verify/src/index.ts @@ -2,6 +2,7 @@ export { verifyLoginProof, } from './verifyLoginProof.js'; export { NonceCache } from './nonceCache.js'; +export { PersistentNonceCache } from './persistentNonceCache.js'; export { buildAllowedPubkeysFromValidateurs } from './buildAllowedPubkeys.js'; export type { LoginProof, diff --git a/service-login-verify/src/persistentNonceCache.ts b/service-login-verify/src/persistentNonceCache.ts new file mode 100644 index 0000000..87d2b90 --- /dev/null +++ b/service-login-verify/src/persistentNonceCache.ts @@ -0,0 +1,123 @@ +import type { NonceCacheLike } from './types.js'; + +/** + * Persistent nonce cache using IndexedDB (browser) or localStorage (fallback). + * Implements NonceCacheLike interface for use with verifyLoginProof. + */ +export class PersistentNonceCache implements NonceCacheLike { + private readonly ttlMs: number; + private readonly storageKey: string; + private readonly useIndexedDB: boolean; + private db: IDBDatabase | null = null; + + constructor(ttlMs: number = 3600000, storageKey: string = 'nonce_cache') { + this.ttlMs = ttlMs; + this.storageKey = storageKey; + this.useIndexedDB = typeof window !== 'undefined' && 'indexedDB' in window; + } + + /** + * Initialize IndexedDB if available. + */ + async init(): Promise { + if (!this.useIndexedDB) { + return; + } + + return new Promise((resolve, reject) => { + const request = indexedDB.open('NonceCacheDB', 1); + + request.onerror = (): void => { + reject(new Error('Failed to open IndexedDB')); + }; + + request.onsuccess = (): void => { + this.db = request.result; + resolve(); + }; + + request.onupgradeneeded = (event: IDBVersionChangeEvent): void => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains('nonces')) { + const store = db.createObjectStore('nonces', { keyPath: 'nonce' }); + store.createIndex('timestamp', 'timestamp', { unique: false }); + } + }; + }); + } + + /** + * Check if nonce is valid (not seen within TTL). Records nonce on success. + * Note: IndexedDB operations are async, but NonceCacheLike interface requires sync. + * This implementation uses localStorage for synchronous access. + * For true IndexedDB persistence, consider making the interface async. + */ + isValid(nonce: string, timestamp: number): boolean { + return this.isValidSync(nonce, timestamp); + } + + /** + * Synchronous validation using localStorage (fallback). + */ + private isValidSync(nonce: string, timestamp: number): boolean { + const now = Date.now(); + const stored = localStorage.getItem(`${this.storageKey}_${nonce}`); + + if (stored !== null) { + const storedTimestamp = parseInt(stored, 10); + if (now - storedTimestamp < this.ttlMs) { + return false; + } + localStorage.removeItem(`${this.storageKey}_${nonce}`); + } + + localStorage.setItem(`${this.storageKey}_${nonce}`, timestamp.toString()); + this.cleanupSync(now); + return true; + } + + /** + * Cleanup expired entries (localStorage). + */ + private cleanupSync(now: number): void { + const keys: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key !== null && key.startsWith(`${this.storageKey}_`)) { + keys.push(key); + } + } + + for (const key of keys) { + const stored = localStorage.getItem(key); + if (stored !== null) { + const storedTimestamp = parseInt(stored, 10); + if (now - storedTimestamp >= this.ttlMs) { + localStorage.removeItem(key); + } + } + } + } + + /** + * Clear all entries. + */ + clear(): void { + if (this.useIndexedDB && this.db !== null) { + const transaction = this.db.transaction(['nonces'], 'readwrite'); + const store = transaction.objectStore('nonces'); + store.clear(); + } + + const keys: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key !== null && key.startsWith(`${this.storageKey}_`)) { + keys.push(key); + } + } + for (const key of keys) { + localStorage.removeItem(key); + } + } +} diff --git a/service-login-verify/tsconfig.json b/service-login-verify/tsconfig.json index de81b23..1c8e497 100644 --- a/service-login-verify/tsconfig.json +++ b/service-login-verify/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2022", "module": "Node16", - "lib": ["ES2022"], + "lib": ["ES2022", "DOM"], "moduleResolution": "Node16", "rootDir": "./src", "outDir": "./dist", diff --git a/userwallet/src/App.tsx b/userwallet/src/App.tsx index 3fcf24d..a4a27da 100644 --- a/userwallet/src/App.tsx +++ b/userwallet/src/App.tsx @@ -19,6 +19,7 @@ import { ServiceListScreen } from './components/ServiceListScreen'; import { MemberSelectionScreen } from './components/MemberSelectionScreen'; import { DiagnosticScreen } from './components/DiagnosticScreen'; import { ServiceSyncScreen } from './components/ServiceSyncScreen'; +import { CryptoSettingsScreen } from './components/CryptoSettingsScreen'; import { DataExportImportScreen } from './components/DataExportImportScreen'; import { UnlockScreen } from './components/UnlockScreen'; import { useChannel } from './hooks/useChannel'; @@ -49,6 +50,7 @@ function AppContent(): JSX.Element { } /> } /> } /> + } /> } /> ); diff --git a/userwallet/src/components/CreateIdentityScreen.tsx b/userwallet/src/components/CreateIdentityScreen.tsx index bf9cd88..2a9bf46 100644 --- a/userwallet/src/components/CreateIdentityScreen.tsx +++ b/userwallet/src/components/CreateIdentityScreen.tsx @@ -11,18 +11,20 @@ export function CreateIdentityScreen(): JSX.Element { const [name, setName] = useState(''); const [isCreating, setIsCreating] = useState(false); - const handleSubmit = async (e: FormEvent): Promise => { + const handleSubmit = (e: FormEvent): void => { e.preventDefault(); - setIsCreating(true); - clearError(); - try { - createNewIdentity(name || undefined); - navigate('/'); - } catch (err) { - handleError(err, 'Erreur lors de la création de l\'identité'); - } finally { - setIsCreating(false); - } + void (async (): Promise => { + setIsCreating(true); + clearError(); + try { + createNewIdentity(name || undefined); + navigate('/'); + } catch (err) { + handleError(err, 'Erreur lors de la création de l'identité'); + } finally { + setIsCreating(false); + } + })(); }; return ( diff --git a/userwallet/src/components/CryptoSettingsScreen.tsx b/userwallet/src/components/CryptoSettingsScreen.tsx new file mode 100644 index 0000000..8f8168e --- /dev/null +++ b/userwallet/src/components/CryptoSettingsScreen.tsx @@ -0,0 +1,216 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useErrorHandler } from '../hooks/useErrorHandler'; +import { ErrorDisplay } from './ErrorDisplay'; + +interface CryptoSettings { + hashAlgorithm: string; + jsonCanonizationStrict: boolean; + ecdhCurve: string; + nonceTtlMs: number; + timestampWindowMs: number; +} + +const DEFAULT_SETTINGS: CryptoSettings = { + hashAlgorithm: 'sha256', + jsonCanonizationStrict: true, + ecdhCurve: 'secp256k1', + nonceTtlMs: 3600000, // 1 hour + timestampWindowMs: 300000, // 5 minutes +}; + +const STORAGE_KEY = 'userwallet_crypto_settings'; + +export function CryptoSettingsScreen(): JSX.Element { + const navigate = useNavigate(); + const { error, handleError, clearError } = useErrorHandler(); + const [settings, setSettings] = useState(DEFAULT_SETTINGS); + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored !== null) { + try { + const parsed = JSON.parse(stored) as CryptoSettings; + setSettings({ ...DEFAULT_SETTINGS, ...parsed }); + } catch { + // Ignore invalid stored settings + } + } + }, []); + + const handleSave = (): void => { + setIsSaving(true); + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + clearError(); + } catch (err) { + handleError(err, 'Erreur lors de la sauvegarde'); + } finally { + setIsSaving(false); + } + }; + + const handleReset = (): void => { + setSettings(DEFAULT_SETTINGS); + localStorage.removeItem(STORAGE_KEY); + clearError(); + }; + + return ( +
+

Paramètres crypto (avancé)

+ {error !== null && } + +
+

Configuration cryptographique

+

+ Attention: Modifier ces paramètres peut rendre votre + wallet incompatible avec les services existants. Utilisez uniquement si + vous comprenez les implications. +

+ +
+
+ +

+ Algorithme utilisé pour calculer les hash canoniques des messages. +

+
+ +
+ +

+ Mode strict pour la canonisation JSON (ordre des clés, espaces, etc.). +

+
+ +
+ +

+ Courbe elliptique pour ECDH (non modifiable, secp256k1 uniquement). +

+
+ +
+ +

+ Durée de vie du cache anti-rejeu pour les nonces (60000-86400000 ms). +

+
+ +
+ +

+ Fenêtre de validité pour les timestamps (60000-3600000 ms). +

+
+
+ +
+ + + +
+
+ +
+

Avertissements

+
    +
  • + Incompatibilité: Modifier ces paramètres peut rendre + votre wallet incompatible avec les services existants. +
  • +
  • + Version logicielle: Certains paramètres peuvent ne pas + être supportés par votre version du wallet. +
  • +
  • + Recommandation: Utilisez les valeurs par défaut sauf si + vous avez une raison spécifique de les modifier. +
  • +
+
+
+ ); +} diff --git a/userwallet/src/components/DataExportImportScreen.tsx b/userwallet/src/components/DataExportImportScreen.tsx index 0241dc1..50c83e9 100644 --- a/userwallet/src/components/DataExportImportScreen.tsx +++ b/userwallet/src/components/DataExportImportScreen.tsx @@ -27,53 +27,59 @@ export function DataExportImportScreen(): JSX.Element { const [disableError, setDisableError] = useState(null); const [disableLoading, setDisableLoading] = useState(false); - const handleEnableProtection = async (e: FormEvent): Promise => { + const handleEnableProtection = (e: FormEvent): void => { e.preventDefault(); - setProtectError(null); - if (protectPassword !== protectConfirm) { - setProtectError('Les mots de passe ne correspondent pas.'); - return; - } - if (protectPassword.length < 8) { - setProtectError('Mot de passe d\'au moins 8 caractères.'); - return; - } - setProtectLoading(true); - try { - await enableProtection(protectPassword); - setProtectPassword(''); - setProtectConfirm(''); - } catch (err) { - setProtectError(err instanceof Error ? err.message : 'Erreur'); - } finally { - setProtectLoading(false); - } + void (async (): Promise => { + setProtectError(null); + if (protectPassword !== protectConfirm) { + setProtectError('Les mots de passe ne correspondent pas.'); + return; + } + if (protectPassword.length < 8) { + setProtectError('Mot de passe d'au moins 8 caractères.'); + return; + } + setProtectLoading(true); + try { + await enableProtection(protectPassword); + setProtectPassword(''); + setProtectConfirm(''); + } catch (err) { + setProtectError(err instanceof Error ? err.message : 'Erreur'); + } finally { + setProtectLoading(false); + } + })(); }; - const handleDisableProtection = async (e: FormEvent): Promise => { + const handleDisableProtection = (e: FormEvent): void => { e.preventDefault(); - setDisableError(null); - setDisableLoading(true); - try { - await disableProtection(disablePassword); - setDisablePassword(''); - } catch (err) { - setDisableError(err instanceof Error ? err.message : 'Erreur'); - } finally { - setDisableLoading(false); - } + void (async (): Promise => { + setDisableError(null); + setDisableLoading(true); + try { + await disableProtection(disablePassword); + setDisablePassword(''); + } catch (err) { + setDisableError(err instanceof Error ? err.message : 'Erreur'); + } finally { + setDisableLoading(false); + } + })(); }; const [exportError, setExportError] = useState(null); - const handleExport = async (): Promise => { - setExportError(null); - try { - const json = await exportUserWalletData(); - downloadExportFile(json); - } catch (err) { - setExportError(err instanceof Error ? err.message : 'Erreur export'); - } + const handleExport = (): void => { + void (async (): Promise => { + setExportError(null); + try { + const json = await exportUserWalletData(); + downloadExportFile(json); + } catch (err) { + setExportError(err instanceof Error ? err.message : 'Erreur export'); + } + })(); }; const handleImportClick = (): void => { @@ -88,18 +94,20 @@ export function DataExportImportScreen(): JSX.Element { return; } const reader = new FileReader(); - reader.onload = async () => { - const text = reader.result; - if (typeof text !== 'string') { - setImportError('Fichier non lisible'); - return; - } - try { - await importUserWalletData(text); - window.location.reload(); - } catch (err) { - setImportError(err instanceof Error ? err.message : 'Erreur import'); - } + reader.onload = (): void => { + void (async (): Promise => { + const text = reader.result; + if (typeof text !== 'string') { + setImportError('Fichier non lisible'); + return; + } + try { + await importUserWalletData(text); + window.location.reload(); + } catch (err) { + setImportError(err instanceof Error ? err.message : 'Erreur import'); + } + })(); }; reader.readAsText(file); }; diff --git a/userwallet/src/components/GlobalActionBar.tsx b/userwallet/src/components/GlobalActionBar.tsx index f032a37..976fbd9 100644 --- a/userwallet/src/components/GlobalActionBar.tsx +++ b/userwallet/src/components/GlobalActionBar.tsx @@ -80,8 +80,9 @@ export function GlobalActionBar(): JSX.Element { }; const mots = - (ctx?.offerWords != null && ctx.offerWords.length > 0 ? ctx.offerWords : null) ?? - getLocalPairWords(); + (ctx?.offerWords !== null && ctx?.offerWords !== undefined && ctx.offerWords.length > 0 + ? ctx.offerWords + : null) ?? getLocalPairWords(); const hasMots = mots !== null && mots.length > 0; return ( diff --git a/userwallet/src/components/ImportIdentityScreen.tsx b/userwallet/src/components/ImportIdentityScreen.tsx index e8dfd2b..e99cd51 100644 --- a/userwallet/src/components/ImportIdentityScreen.tsx +++ b/userwallet/src/components/ImportIdentityScreen.tsx @@ -10,23 +10,25 @@ export function ImportIdentityScreen(): JSX.Element { const [isImporting, setIsImporting] = useState(false); const [error, setError] = useState(null); - const handleSubmit = async (e: FormEvent): Promise => { + const handleSubmit = (e: FormEvent): void => { e.preventDefault(); - setError(null); - setIsImporting(true); - try { - const imported = importExistingIdentity(seedOrKey, name || undefined); - if (imported === null) { - setError('Import échoué. Vérifiez le format du seed ou de la clé privée.'); - } else { - navigate('/'); + void (async (): Promise => { + setError(null); + setIsImporting(true); + try { + const imported = importExistingIdentity(seedOrKey, name || undefined); + if (imported === null) { + setError('Import échoué. Vérifiez le format du seed ou de la clé privée.'); + } else { + navigate('/'); + } + } catch (err) { + setError("Erreur lors de l'import"); + console.error('Error importing identity:', err); + } finally { + setIsImporting(false); } - } catch (err) { - setError("Erreur lors de l'import"); - console.error('Error importing identity:', err); - } finally { - setIsImporting(false); - } + })(); }; return ( diff --git a/userwallet/src/components/LoginForm.tsx b/userwallet/src/components/LoginForm.tsx index c002e63..3404213 100644 --- a/userwallet/src/components/LoginForm.tsx +++ b/userwallet/src/components/LoginForm.tsx @@ -6,25 +6,27 @@ export function LoginForm(): JSX.Element { const { keyPair, isLoading, createAuthResponse } = useAuth(); const [isAuthenticating, setIsAuthenticating] = useState(false); - const handleSubmit = async (e: FormEvent): Promise => { + const handleSubmit = (e: FormEvent): void => { e.preventDefault(); - if (keyPair === null || isLoading) { - return; - } - - setIsAuthenticating(true); - try { - const response = createAuthResponse(); - if (response !== null) { - sendAuthResponse(response); - } else { - console.error('Failed to create auth response'); + void (async (): Promise => { + if (keyPair === null || isLoading) { + return; } - } catch (error) { - console.error('Authentication error:', error); - } finally { - setIsAuthenticating(false); - } + + setIsAuthenticating(true); + try { + const response = createAuthResponse(); + if (response !== null) { + sendAuthResponse(response); + } else { + console.error('Failed to create auth response'); + } + } catch (error) { + console.error('Authentication error:', error); + } finally { + setIsAuthenticating(false); + } + })(); }; if (isLoading) { diff --git a/userwallet/src/components/LoginScreen.tsx b/userwallet/src/components/LoginScreen.tsx index a3e277f..bf6f340 100644 --- a/userwallet/src/components/LoginScreen.tsx +++ b/userwallet/src/components/LoginScreen.tsx @@ -402,7 +402,7 @@ export function LoginScreen(): JSX.Element { } // Log structuré pour le succès - console.info('[Login] Verification succeeded:', { + console.warn('[Login] Verification succeeded:', { timestamp: new Date().toISOString(), hash: proof.challenge.hash.slice(0, 16) + '...', serviceUuid: loginPath?.service_uuid, @@ -665,9 +665,7 @@ export function LoginScreen(): JSX.Element { : '—'} - {req.cardinalite_minimale !== undefined - ? req.cardinalite_minimale - : '1'} + {req.cardinalite_minimale ?? '1'} {req.dependances !== undefined && @@ -830,7 +828,9 @@ export function LoginScreen(): JSX.Element {
- + +
); diff --git a/userwallet/src/components/ServiceListScreen.tsx b/userwallet/src/components/ServiceListScreen.tsx index ff1dbe7..5e43899 100644 --- a/userwallet/src/components/ServiceListScreen.tsx +++ b/userwallet/src/components/ServiceListScreen.tsx @@ -28,10 +28,6 @@ export function ServiceListScreen(): JSX.Element { } }, [location.pathname, loginState, dispatch]); - useEffect(() => { - loadServices(); - }, []); - const loadServices = async (): Promise => { if (identity === null) { return; @@ -42,7 +38,7 @@ export function ServiceListScreen(): JSX.Element { try { const relays = getStoredRelays().filter((r) => r.enabled); if (relays.length === 0) { - handleError('Aucun relais activé. Synchronisez d\'abord.', 'NO_RELAYS'); + handleError('Aucun relais activé. Synchronisez d'abord.', 'NO_RELAYS'); return; }