@mostajs/security 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog — @mostajs/security
2
+ ## [0.1.0] — 2026-06-18
3
+ ### Added
4
+ - Chiffrement au repos AES-256-GCM (`encryptAtRest`/`decryptAtRest`), `sha256`, `hmacSha256`, `randomKey`/`toKey`.
5
+ - `keyFromEnv` (clé via .env), `createCipher`, `encryptedStorage` (wrapper de composition pour @mostajs/storage).
6
+ - 7 tests + exemple §12 (chiffrement au repos d'un document, storage composé). EXTERNALISATION du crypto (§10 #4).
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # @mostajs/security
2
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later · **Statut** : 0.1.0 (7 tests verts) · Node-only
3
+ > Crypto réutilisable : **chiffrement au repos** (AES-256-GCM), sha256, HMAC, keyring `.env`. Composé par storage/ged/users/url.
4
+ ## Chiffrement au repos (compose storage, sans le modifier)
5
+ ```js
6
+ import { createCipher, encryptedStorage, keyFromEnv } from '@mostajs/security';
7
+ const secureStore = encryptedStorage(realStorageDriver, createCipher({ key: keyFromEnv(process.env) }));
8
+ const ref = await secureStore.put(bytes); // chiffré au repos
9
+ const clear = await secureStore.get(ref.id); // déchiffré à la lecture
10
+ ```
11
+ ## Lancer
12
+ ```bash
13
+ node test-scripts/unit/security.test.mjs && node examples/at-rest/run.mjs
14
+ ```
@@ -0,0 +1,19 @@
1
+ # @mostajs/security — DEVRULES de bout en bout (§4 + #3 condensés)
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Date** : 2026-06-18 · **Statut** : proposition à discuter (cas C)
4
+ **Priorité** : P0 transverse — **chiffrement au repos** réutilisable par `storage`, `ged`, `users` (biométrie), `url` (HMAC).
5
+
6
+ ## §4 Règle d'or / motivation
7
+ Le **chiffrement au repos** (spec ASSO-SEL §5, Hadhinat docs) était sur le point d'être **noyé dans `@mostajs/storage`**.
8
+ Décision (§10 #4, soupape anti-monolithe) : **EXTERNALISER** dans `@mostajs/security` (responsabilité unique : crypto),
9
+ réutilisé partout. Aujourd'hui la crypto est **dispersée** (`storage` calcule sha256, `url` fait HMAC) → on **consolide**.
10
+ - `storage` = octets (compose security pour chiffrer) · `ged`/`users` = composent security · `url` = HMAC via security.
11
+
12
+ ## §3 Périmètre & API
13
+ **Crypto au repos** : AES-256-GCM (authentifié → détection d'altération). **Hash** : sha256. **Signature** : HMAC-SHA256.
14
+ **Clés** : `keyFromEnv` (`MOSTA_ENCRYPTION_KEY`, via `@mostajs/config`/.env — jamais en dur ; rotation par versionnage de blob).
15
+ **API** : `encryptAtRest(data,key)->{v,iv,tag,ct}` · `decryptAtRest(blob,key)->Buffer` · `sha256(data)` · `hmacSha256(data,secret)` ·
16
+ `randomKey()` · `createCipher({key})->{encrypt,decrypt}` · `encryptedStorage(storage,cipher)` (wrapper qui chiffre au put / déchiffre au get — **storage inchangé**, composition cas B).
17
+ **Composition (§10)** : `@mostajs/config` (clé via .env, souveraineté/résidence pilotée par .env — §11.3 informatif).
18
+ **Jalons** : 0.1 AES-GCM+sha256+HMAC+keyring+wrapper storage · 0.2 rotation de clés (key id) · 1.0 14 livrables.
19
+ **Node-only** (node:crypto). §11.2 : secrets jamais commités ; §11.3 informatif : résidence des clés/données par .env.
@@ -0,0 +1,14 @@
1
+ // Exemple §12 — chiffrement au repos d'un document GED via @mostajs/security. node examples/at-rest/run.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { createCipher, encryptedStorage, randomKey, sha256 } from '../../src/index.js';
4
+ // driver storage mémoire (mime du vrai @mostajs/storage : { id, checksum, size })
5
+ const disk=new Map();
6
+ const storage={ async put(b){ const id='fs://'+disk.size; disk.set(id,b); return { id, checksum: sha256(b), size:b.length }; }, async get(id){ return disk.get(id); } };
7
+ // composition : storage + security (clé via .env en prod : keyFromEnv)
8
+ const secureStore=encryptedStorage(storage, createCipher({ key: randomKey() }));
9
+ const ref=await secureStore.put(Buffer.from('Compte rendu médical — patient X'));
10
+ // au repos : illisible en clair
11
+ assert.ok(!disk.get(ref.id).toString().includes('patient'), 'octets au repos chiffrés');
12
+ // lecture : déchiffré
13
+ assert.equal((await secureStore.get(ref.id)).toString(), 'Compte rendu médical — patient X');
14
+ console.log('✅ security — chiffrement au repos OK (storage composé, clé .env, AES-256-GCM)');
package/llms.txt ADDED
@@ -0,0 +1,15 @@
1
+ # @mostajs/security — fiche LLM
2
+ RÔLE
3
+ Crypto réutilisable de l'écosystème (responsabilité unique) : chiffrement AU REPOS (AES-256-GCM authentifié),
4
+ hash sha256, signature HMAC-SHA256, keyring via .env. Composé par storage (encryptedStorage), ged, users, url.
5
+ Node-only (node:crypto). EXTERNALISÉ pour éviter de noyer le crypto dans storage (§10 #4).
6
+ EXPORTS
7
+ encryptAtRest(data,key) -> { v,iv,tag,ct } (base64) ; decryptAtRest(blob,key) -> Buffer (throw si altéré/mauvaise clé)
8
+ sha256(data) -> 'sha256:hex' ; hmacSha256(data,secret) -> hex ; randomKey() -> Buffer(32) ; toKey(hex|base64|Buffer)
9
+ keyFromEnv(env?,name='MOSTA_ENCRYPTION_KEY') -> Buffer (jamais de clé en dur)
10
+ createCipher({key}) -> { encrypt(bytes), decrypt(blob) }
11
+ encryptedStorage(storageDriver, cipher) -> driver enveloppé (chiffre au put / déchiffre au get) — storage INCHANGÉ
12
+ PIÈGES
13
+ - Clé 32 octets (256-bit), via .env (souveraineté/résidence §11.3). AES-GCM = authentifié → toute altération throw.
14
+ - Le sha256 d'un FICHIER reste calculé par @mostajs/storage (checksum) ; security.sha256 = hash générique.
15
+ - Node-only ; ne pas embarquer côté navigateur.
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@mostajs/security",
3
+ "version": "0.1.0",
4
+ "description": "Crypto réutilisable : chiffrement au repos (AES-256-GCM authentifié), sha256, HMAC-SHA256, keyring .env, wrapper encryptedStorage. Composé par storage/ged/users/url.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./crypto": "./src/crypto.js"
12
+ },
13
+ "keywords": [
14
+ "mostajs",
15
+ "security",
16
+ "encryption",
17
+ "at-rest",
18
+ "aes-gcm",
19
+ "sha256",
20
+ "hmac"
21
+ ],
22
+ "scripts": {
23
+ "test": "node test-scripts/unit/security.test.mjs",
24
+ "example": "node examples/at-rest/run.mjs"
25
+ }
26
+ }
package/src/cipher.js ADDED
@@ -0,0 +1,18 @@
1
+ // @mostajs/security — cipher + wrapper storage (chiffrement au repos composable). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ import { encryptAtRest, decryptAtRest, toKey } from './crypto.js';
3
+ export function createCipher({ key }) {
4
+ const k = toKey(key);
5
+ return { encrypt: (b) => encryptAtRest(b, k), decrypt: (blob) => decryptAtRest(blob, k) };
6
+ }
7
+ /**
8
+ * Enveloppe un driver @mostajs/storage pour CHIFFRER au repos sans modifier storage (composition cas B).
9
+ * put : chiffre → JSON {iv,tag,ct} → délègue à storage ; get : récupère → déchiffre.
10
+ */
11
+ export function encryptedStorage(storage, cipher) {
12
+ const putFn = storage.put || storage.createFile || storage.save;
13
+ return {
14
+ async put(bytes, opts) { const payload = Buffer.from(JSON.stringify(cipher.encrypt(bytes))); return putFn.call(storage, payload, opts); },
15
+ async get(ref) { const raw = await storage.get(ref); const blob = JSON.parse(Buffer.from(raw).toString('utf8')); return cipher.decrypt(blob); },
16
+ _inner: storage,
17
+ };
18
+ }
package/src/crypto.js ADDED
@@ -0,0 +1,28 @@
1
+ // @mostajs/security — primitives crypto (node:crypto). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ import { randomBytes, createCipheriv, createDecipheriv, createHash, createHmac } from 'node:crypto';
3
+ const ALGO = 'aes-256-gcm';
4
+
5
+ /** Normalise une clé (Buffer 32o | hex 64 | base64) → Buffer 32 octets. */
6
+ export function toKey(k) {
7
+ if (Buffer.isBuffer(k)) { if (k.length !== 32) throw new Error('clé: 32 octets requis'); return k; }
8
+ if (typeof k === 'string') { const b = /^[0-9a-f]{64}$/i.test(k) ? Buffer.from(k, 'hex') : Buffer.from(k, 'base64'); if (b.length !== 32) throw new Error('clé: 32 octets (256-bit) requis'); return b; }
9
+ throw new Error('clé invalide');
10
+ }
11
+ export const randomKey = () => randomBytes(32);
12
+
13
+ /** Chiffrement au repos AES-256-GCM (authentifié). → { v, iv, tag, ct } (base64). */
14
+ export function encryptAtRest(data, key) {
15
+ const k = toKey(key); const iv = randomBytes(12);
16
+ const c = createCipheriv(ALGO, k, iv);
17
+ const ct = Buffer.concat([c.update(Buffer.from(data)), c.final()]);
18
+ return { v: 1, iv: iv.toString('base64'), tag: c.getAuthTag().toString('base64'), ct: ct.toString('base64') };
19
+ }
20
+ /** Déchiffrement ; throw si altéré (auth GCM) ou mauvaise clé. → Buffer. */
21
+ export function decryptAtRest(blob, key) {
22
+ const k = toKey(key);
23
+ const d = createDecipheriv(ALGO, k, Buffer.from(blob.iv, 'base64'));
24
+ d.setAuthTag(Buffer.from(blob.tag, 'base64'));
25
+ return Buffer.concat([d.update(Buffer.from(blob.ct, 'base64')), d.final()]);
26
+ }
27
+ export const sha256 = (data) => 'sha256:' + createHash('sha256').update(Buffer.from(data)).digest('hex');
28
+ export const hmacSha256 = (data, secret) => createHmac('sha256', secret).update(Buffer.from(data)).digest('hex');
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // @mostajs/security — point d'entrée. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ export { encryptAtRest, decryptAtRest, sha256, hmacSha256, randomKey, toKey } from './crypto.js';
3
+ export { keyFromEnv } from './keyring.js';
4
+ export { createCipher, encryptedStorage } from './cipher.js';
package/src/keyring.js ADDED
@@ -0,0 +1,8 @@
1
+ // @mostajs/security — clé via .env (compose @mostajs/config). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ import { toKey } from './crypto.js';
3
+ /** Lit la clé de chiffrement au repos depuis l'env (jamais en dur). */
4
+ export function keyFromEnv(env = (typeof process !== 'undefined' ? process.env : {}), name = 'MOSTA_ENCRYPTION_KEY') {
5
+ const v = env[name];
6
+ if (!v) throw new Error(`${name} absent — définir la clé (32 octets hex/base64) dans .env (souveraineté/résidence §11.3)`);
7
+ return toKey(v);
8
+ }
@@ -0,0 +1,38 @@
1
+ // @mostajs/security — tests (DEVRULES §5). node test-scripts/unit/security.test.mjs
2
+ import assert from 'node:assert/strict';
3
+ import { encryptAtRest, decryptAtRest, sha256, hmacSha256, randomKey, createCipher, encryptedStorage, keyFromEnv } from '../../src/index.js';
4
+ let pass=0; const test=async(n,f)=>{await f();pass++;console.log(' ✓',n);};
5
+
6
+ await test('chiffrement au repos : aller-retour', ()=>{
7
+ const key=randomKey(); const blob=encryptAtRest(Buffer.from('dossier médical confidentiel'),key);
8
+ assert.ok(blob.iv && blob.tag && blob.ct);
9
+ assert.equal(decryptAtRest(blob,key).toString(),'dossier médical confidentiel');
10
+ });
11
+ await test('détection d\'altération (GCM) → throw', ()=>{
12
+ const key=randomKey(); const blob=encryptAtRest(Buffer.from('x'),key);
13
+ const tampered={...blob, ct:Buffer.from('zzzz').toString('base64')};
14
+ assert.throws(()=>decryptAtRest(tampered,key));
15
+ });
16
+ await test('mauvaise clé → throw', ()=>{
17
+ const blob=encryptAtRest(Buffer.from('x'),randomKey());
18
+ assert.throws(()=>decryptAtRest(blob,randomKey()));
19
+ });
20
+ await test('clé invalide rejetée', ()=>{ assert.throws(()=>encryptAtRest('x','tropcourt')); });
21
+ await test('sha256 + hmac déterministes', ()=>{
22
+ assert.equal(sha256('abc'), sha256('abc'));
23
+ assert.equal(hmacSha256('msg','secret'), hmacSha256('msg','secret'));
24
+ assert.notEqual(hmacSha256('msg','a'), hmacSha256('msg','b'));
25
+ });
26
+ await test('keyFromEnv : absente → throw, présente → ok', ()=>{
27
+ assert.throws(()=>keyFromEnv({}), /absent/);
28
+ const k=randomKey().toString('hex'); assert.ok(keyFromEnv({ MOSTA_ENCRYPTION_KEY:k }));
29
+ });
30
+ await test('encryptedStorage : chiffre au repos, déchiffre à la lecture (storage inchangé)', async()=>{
31
+ const blobs=new Map(); const raw=new Map();
32
+ const storage={ async put(b){ const id='f'+blobs.size; raw.set(id,b); return { id, checksum: sha256(b), size:b.length }; }, async get(id){ return raw.get(id); } };
33
+ const enc=encryptedStorage(storage, createCipher({key:randomKey()}));
34
+ const res=await enc.put(Buffer.from('secret au repos'));
35
+ assert.ok(!raw.get(res.id).toString().includes('secret'), 'au repos = chiffré (pas de clair)');
36
+ assert.equal((await enc.get(res.id)).toString(),'secret au repos','déchiffré à la lecture');
37
+ });
38
+ console.log(`\n✅ @mostajs/security — ${pass} tests OK`);