@mostajs/crypto-box 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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (c) 2026 Dr Hamid MADANI <drmdh@msn.com>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ COMMERCIAL LICENSE
20
+
21
+ For organizations that cannot comply with the AGPL open-source requirements,
22
+ a commercial license is available. Contact: drmdh@msn.com
23
+
24
+ The commercial license allows you to:
25
+ - Use the software in proprietary/closed-source projects
26
+ - Modify without publishing your source code
27
+ - Get priority support and SLA
28
+
29
+ Contact: Dr Hamid MADANI <drmdh@msn.com>
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @mostajs/crypto-box
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
4
+ **Niveau** : N0 (primitif) · **Zéro dépendance** (`node:crypto` uniquement)
5
+
6
+ Les primitives cryptographiques de l'écosystème. **Une seule maison.**
7
+
8
+ ## Pourquoi ce module existe
9
+
10
+ En consolidant les URLs signées (13/07/2026), on a trouvé la **même comparaison de signature
11
+ implémentée deux fois** : « best effort » (boucle JS) dans `@mostajs/url` — le **kernel**,
12
+ utilisé partout — et correcte (`timingSafeEqual`) dans `@mostajs/storage`, une simple feuille.
13
+ **Le durcissement n'avait jamais circulé.**
14
+
15
+ C'est ça, le vrai coût d'un doublon cryptographique : ce n'est pas la place qu'il prend, c'est
16
+ que **les corrections ne remontent pas**. Une seule maison = un seul endroit à durcir, un seul
17
+ endroit à auditer.
18
+
19
+ ## Ce que ce module n'est PAS
20
+
21
+ Ce n'est **pas** une « bibliothèque de crypto générique ». C'est une **enveloppe mince et
22
+ opinionée** sur `node:crypto` :
23
+
24
+ - pas de choix d'algorithme,
25
+ - pas de mode dangereux,
26
+ - **pas d'IV fourni par l'appelant**.
27
+
28
+ Un module de crypto qui offre des options est la manière classique d'écrire du chiffrement
29
+ cassé. Le périmètre est dicté par la **demande réelle** — on l'enrichira au prochain besoin
30
+ constaté, pas avant.
31
+
32
+ ## API
33
+
34
+ | Fonction | Réclamée par |
35
+ |---|---|
36
+ | `sha256` · `sha256Bytes` | `blob-store` (SigV4, checksums) |
37
+ | `hmacSha256` · `hmacSha256Bytes` | `@mostajs/url` (URLs signées), SigV4 |
38
+ | `equalsConstantTime` | **toute** comparaison de secret, signature ou jeton |
39
+ | `aeadEncrypt` · `aeadDecrypt` | `@mostajs/backup` (archives chiffrées) |
40
+ | `deriveKey` · `randomKey` | `@mostajs/backup` (clé depuis une phrase) |
41
+
42
+ ```js
43
+ import { deriveKey, aeadEncrypt, aeadDecrypt } from '@mostajs/crypto-box';
44
+
45
+ // Le client ne retient qu'une phrase. Le sel se conserve à côté du chiffré (il n'est pas secret).
46
+ const { key, salt } = await deriveKey('la phrase du client');
47
+ const chiffre = aeadEncrypt(key, archive);
48
+
49
+ // Plus tard, ailleurs :
50
+ const { key: k2 } = await deriveKey('la phrase du client', { salt });
51
+ const restaure = aeadDecrypt(k2, chiffre); // lève si la clé est fausse OU si UN BIT a changé
52
+ ```
53
+
54
+ **AES-256-GCM** — chiffrement *authentifié* : confidentialité **et** intégrité. Le format est
55
+ autoportant (`MJSC1 | iv | tag | chiffré`) : rien d'autre à conserver que la clé.
56
+ **Dérivation par scrypt** (lente et gourmande à dessein) : un simple SHA-256 d'une phrase se
57
+ casse par force brute à des millions d'essais par seconde.
58
+
59
+ ## Ce dont ce module ne répond pas
60
+
61
+ La **garde de la clé** est une décision produit, pas une affaire de module. Pour les
62
+ sauvegardes TicketFlow : la clé reste **chez le client**, la console ne stocke que du chiffré
63
+ qu'elle **ne peut pas lire**. Corollaire assumé, à contractualiser : **clé perdue = archive
64
+ irrécupérable.**
65
+
66
+ ## Tests
67
+
68
+ ```bash
69
+ npm test # 18 tests mjs-unit
70
+ ```
71
+
72
+ En cryptographie, un test qui prouve que « ça marche » ne prouve presque rien : **du
73
+ chiffrement cassé chiffre et déchiffre parfaitement**. Ces tests prouvent ce qui **tue** :
74
+
75
+ - **un seul bit altéré** fait échouer le déchiffrement (tag GCM) — un déchiffrement qui
76
+ « réussirait » sur des octets corrompus rendrait une sauvegarde silencieusement fausse ;
77
+ - **l'IV n'est jamais réutilisé** (deux chiffrés du même clair diffèrent) ;
78
+ - une **clé fausse** échoue, jamais de sortie silencieuse ;
79
+ - `equalsConstantTime` **ne plante pas** sur des longueurs différentes — `timingSafeEqual` de
80
+ Node lève, et un appel naïf tomberait sur une signature tronquée, c'est-à-dire sur une
81
+ entrée d'attaquant.
82
+
83
+ Vecteurs **vérifiés indépendamment** : SHA-256 (coreutils), HMAC (**RFC 4231**, cas 1). Jamais
84
+ recopiés depuis le module — un test qui compare la sortie à elle-même ne teste rien.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @mostajs/crypto-box — Les primitives cryptographiques de l'écosystème. Une seule maison.
3
+ * @author Dr Hamid MADANI <drmdh@msn.com>
4
+ * Licence : AGPL-3.0-or-later
5
+ * Niveau : N0 (primitif) · ZÉRO dépendance (`node:crypto` uniquement)
6
+ *
7
+ * ─── POURQUOI CE MODULE EXISTE ───────────────────────────────────────────────
8
+ * Le 13/07/2026, en consolidant les URLs signées, on a trouvé la MÊME comparaison de
9
+ * signature implémentée deux fois : « best effort » (boucle JS) dans `@mostajs/url` — le
10
+ * KERNEL, utilisé partout — et correcte (`timingSafeEqual`) dans `@mostajs/storage`, une
11
+ * simple feuille. Le durcissement n'avait jamais circulé.
12
+ *
13
+ * Voilà le vrai coût d'un doublon cryptographique : ce n'est pas la place qu'il prend,
14
+ * c'est que **les corrections ne remontent pas**. Une seule maison = un seul endroit à
15
+ * durcir, un seul endroit à auditer.
16
+ *
17
+ * ─── CE QUE CE MODULE N'EST PAS ──────────────────────────────────────────────
18
+ * Ce n'est PAS une « bibliothèque de crypto générique ». C'est une **enveloppe mince et
19
+ * opinionée** sur `node:crypto` : pas de choix d'algorithme, pas de mode dangereux, pas
20
+ * d'IV fourni par l'appelant. Un module de crypto qui offre des options est la manière
21
+ * classique d'écrire du chiffrement cassé.
22
+ *
23
+ * Le périmètre est dicté par la DEMANDE RÉELLE, pas par la spéculation :
24
+ * - `hmacSha256` + `equalsConstantTime` → réclamés par @mostajs/url (URLs signées)
25
+ * - `sha256` → réclamé par blob-store (SigV4, checksums)
26
+ * - `aeadEncrypt` / `aeadDecrypt` → réclamés par @mostajs/backup (archives chiffrées)
27
+ * - `deriveKey` → réclamé par @mostajs/backup (clé depuis un secret)
28
+ * On l'enrichira au prochain besoin RÉEL. Pas avant.
29
+ *
30
+ * ─── CE DONT CE MODULE NE RÉPOND PAS ─────────────────────────────────────────
31
+ * La GARDE de la clé n'est pas son affaire — c'est une décision produit. Pour les
32
+ * sauvegardes TicketFlow : la clé reste chez le client, la console ne stocke que du chiffré
33
+ * qu'elle ne peut pas lire. Corollaire assumé : **clé perdue = archive irrécupérable.**
34
+ */
35
+ export declare const moduleInfo: {
36
+ readonly name: "@mostajs/crypto-box";
37
+ readonly version: "0.1.0";
38
+ readonly level: "N0";
39
+ };
40
+ export type Bytes = Uint8Array | Buffer;
41
+ type Encoding = 'hex' | 'base64url';
42
+ /** SHA-256. Défaut : hex (convention des checksums et de SigV4). */
43
+ export declare function sha256(data: Bytes | string, encoding?: Encoding): string;
44
+ /** SHA-256 brut — pour les chaînages (dérivation de clé SigV4, par ex.). */
45
+ export declare function sha256Bytes(data: Bytes | string): Buffer;
46
+ /** HMAC-SHA256. Défaut : hex. */
47
+ export declare function hmacSha256(key: Bytes | string, data: Bytes | string, encoding?: Encoding): string;
48
+ /** HMAC-SHA256 brut — pour les chaînages (SigV4 dérive sa clé par HMAC successifs). */
49
+ export declare function hmacSha256Bytes(key: Bytes | string, data: Bytes | string): Buffer;
50
+ /**
51
+ * Compare deux valeurs en TEMPS CONSTANT. **Toute** comparaison de secret, de signature ou
52
+ * de jeton doit passer par ici — jamais par `===`, jamais par une boucle maison.
53
+ *
54
+ * C'est LA fonction dont l'absence d'un foyer unique a causé la divergence du 13/07 : le
55
+ * kernel comparait « best effort » quand une feuille faisait correctement.
56
+ *
57
+ * La différence de longueur est renvoyée sans comparaison — elle ne fuit rien : la longueur
58
+ * d'un HMAC-SHA256 est publique et constante.
59
+ */
60
+ export declare function equalsConstantTime(a: Bytes | string, b: Bytes | string): boolean;
61
+ export interface DeriveKeyOptions {
62
+ /** Sel — 16 octets aléatoires recommandés. Généré si absent (et RETOURNÉ : conservez-le). */
63
+ salt?: Bytes;
64
+ /** Coût CPU/mémoire (puissance de 2). Défaut 2^17 — ~100 ms, coûteux à brute-forcer. */
65
+ cost?: number;
66
+ }
67
+ /**
68
+ * Dérive une clé AES-256 (32 octets) depuis une phrase secrète, par **scrypt**.
69
+ *
70
+ * Pourquoi scrypt et pas un simple SHA-256 de la phrase : un hachage est INSTANTANÉ, donc
71
+ * une phrase faible se casse par force brute à des millions d'essais par seconde. scrypt est
72
+ * délibérément **lent et gourmand en mémoire** — c'est le but.
73
+ *
74
+ * Le `salt` est RETOURNÉ : sans lui, la clé est irrécupérable. Il n'est pas secret et doit
75
+ * être stocké à côté du chiffré (c'est ce que fait `aeadEncrypt`).
76
+ */
77
+ export declare function deriveKey(passphrase: string, opts?: DeriveKeyOptions): Promise<{
78
+ key: Buffer;
79
+ salt: Buffer;
80
+ }>;
81
+ /** Secret aléatoire (défaut 32 octets) — clés, sels, jetons. */
82
+ export declare function randomKey(bytes?: number): Buffer;
83
+ /**
84
+ * Chiffre en **AES-256-GCM** (chiffrement AUTHENTIFIÉ : confidentialité ET intégrité).
85
+ *
86
+ * Le format de sortie est **autoportant** — `MJSC1 | iv(12) | tag(16) | chiffré` : il n'y a
87
+ * rien d'autre à conserver que la clé. Sans cela, l'appelant devrait gérer lui-même l'IV et
88
+ * le tag, et un seul oubli rend le déchiffrement impossible… ou pire, l'IV est réutilisé.
89
+ *
90
+ * L'IV est TIRÉ AU HASARD À CHAQUE APPEL et n'est jamais fourni par l'appelant. Réutiliser un
91
+ * IV en GCM est catastrophique — cela révèle le XOR des clairs et permet de forger des
92
+ * messages. C'est la raison pour laquelle cette API n'offre pas l'option.
93
+ */
94
+ export declare function aeadEncrypt(key: Bytes, plaintext: Bytes | string): Buffer;
95
+ /**
96
+ * Déchiffre un blob produit par `aeadEncrypt`.
97
+ *
98
+ * Lève si la clé est fausse OU si le chiffré a été **altéré d'un seul bit** : c'est le tag
99
+ * GCM qui l'impose. Un déchiffrement qui « réussit » sur des octets corrompus est exactement
100
+ * ce qu'on ne veut pas d'une sauvegarde.
101
+ */
102
+ export declare function aeadDecrypt(key: Bytes, blob: Bytes): Buffer;
103
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @mostajs/crypto-box — Les primitives cryptographiques de l'écosystème. Une seule maison.
3
+ * @author Dr Hamid MADANI <drmdh@msn.com>
4
+ * Licence : AGPL-3.0-or-later
5
+ * Niveau : N0 (primitif) · ZÉRO dépendance (`node:crypto` uniquement)
6
+ *
7
+ * ─── POURQUOI CE MODULE EXISTE ───────────────────────────────────────────────
8
+ * Le 13/07/2026, en consolidant les URLs signées, on a trouvé la MÊME comparaison de
9
+ * signature implémentée deux fois : « best effort » (boucle JS) dans `@mostajs/url` — le
10
+ * KERNEL, utilisé partout — et correcte (`timingSafeEqual`) dans `@mostajs/storage`, une
11
+ * simple feuille. Le durcissement n'avait jamais circulé.
12
+ *
13
+ * Voilà le vrai coût d'un doublon cryptographique : ce n'est pas la place qu'il prend,
14
+ * c'est que **les corrections ne remontent pas**. Une seule maison = un seul endroit à
15
+ * durcir, un seul endroit à auditer.
16
+ *
17
+ * ─── CE QUE CE MODULE N'EST PAS ──────────────────────────────────────────────
18
+ * Ce n'est PAS une « bibliothèque de crypto générique ». C'est une **enveloppe mince et
19
+ * opinionée** sur `node:crypto` : pas de choix d'algorithme, pas de mode dangereux, pas
20
+ * d'IV fourni par l'appelant. Un module de crypto qui offre des options est la manière
21
+ * classique d'écrire du chiffrement cassé.
22
+ *
23
+ * Le périmètre est dicté par la DEMANDE RÉELLE, pas par la spéculation :
24
+ * - `hmacSha256` + `equalsConstantTime` → réclamés par @mostajs/url (URLs signées)
25
+ * - `sha256` → réclamé par blob-store (SigV4, checksums)
26
+ * - `aeadEncrypt` / `aeadDecrypt` → réclamés par @mostajs/backup (archives chiffrées)
27
+ * - `deriveKey` → réclamé par @mostajs/backup (clé depuis un secret)
28
+ * On l'enrichira au prochain besoin RÉEL. Pas avant.
29
+ *
30
+ * ─── CE DONT CE MODULE NE RÉPOND PAS ─────────────────────────────────────────
31
+ * La GARDE de la clé n'est pas son affaire — c'est une décision produit. Pour les
32
+ * sauvegardes TicketFlow : la clé reste chez le client, la console ne stocke que du chiffré
33
+ * qu'elle ne peut pas lire. Corollaire assumé : **clé perdue = archive irrécupérable.**
34
+ */
35
+ import { createHash, createHmac, timingSafeEqual as nodeTimingSafeEqual, randomBytes, createCipheriv, createDecipheriv, scrypt as nodeScrypt, } from 'node:crypto';
36
+ export const moduleInfo = { name: '@mostajs/crypto-box', version: '0.1.0', level: 'N0' };
37
+ const enc = (buf, encoding) => encoding === 'hex' ? buf.toString('hex') : buf.toString('base64url');
38
+ // ─── Empreintes ──────────────────────────────────────────────────────────────
39
+ /** SHA-256. Défaut : hex (convention des checksums et de SigV4). */
40
+ export function sha256(data, encoding = 'hex') {
41
+ return enc(createHash('sha256').update(data).digest(), encoding);
42
+ }
43
+ /** SHA-256 brut — pour les chaînages (dérivation de clé SigV4, par ex.). */
44
+ export function sha256Bytes(data) {
45
+ return createHash('sha256').update(data).digest();
46
+ }
47
+ /** HMAC-SHA256. Défaut : hex. */
48
+ export function hmacSha256(key, data, encoding = 'hex') {
49
+ return enc(createHmac('sha256', key).update(data).digest(), encoding);
50
+ }
51
+ /** HMAC-SHA256 brut — pour les chaînages (SigV4 dérive sa clé par HMAC successifs). */
52
+ export function hmacSha256Bytes(key, data) {
53
+ return createHmac('sha256', key).update(data).digest();
54
+ }
55
+ // ─── Comparaison ─────────────────────────────────────────────────────────────
56
+ /**
57
+ * Compare deux valeurs en TEMPS CONSTANT. **Toute** comparaison de secret, de signature ou
58
+ * de jeton doit passer par ici — jamais par `===`, jamais par une boucle maison.
59
+ *
60
+ * C'est LA fonction dont l'absence d'un foyer unique a causé la divergence du 13/07 : le
61
+ * kernel comparait « best effort » quand une feuille faisait correctement.
62
+ *
63
+ * La différence de longueur est renvoyée sans comparaison — elle ne fuit rien : la longueur
64
+ * d'un HMAC-SHA256 est publique et constante.
65
+ */
66
+ export function equalsConstantTime(a, b) {
67
+ const bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);
68
+ const bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);
69
+ if (bufA.length !== bufB.length)
70
+ return false;
71
+ if (bufA.length === 0)
72
+ return true;
73
+ return nodeTimingSafeEqual(bufA, bufB);
74
+ }
75
+ /**
76
+ * Dérive une clé AES-256 (32 octets) depuis une phrase secrète, par **scrypt**.
77
+ *
78
+ * Pourquoi scrypt et pas un simple SHA-256 de la phrase : un hachage est INSTANTANÉ, donc
79
+ * une phrase faible se casse par force brute à des millions d'essais par seconde. scrypt est
80
+ * délibérément **lent et gourmand en mémoire** — c'est le but.
81
+ *
82
+ * Le `salt` est RETOURNÉ : sans lui, la clé est irrécupérable. Il n'est pas secret et doit
83
+ * être stocké à côté du chiffré (c'est ce que fait `aeadEncrypt`).
84
+ */
85
+ export function deriveKey(passphrase, opts = {}) {
86
+ const salt = opts.salt ? Buffer.from(opts.salt) : randomBytes(16);
87
+ const cost = opts.cost ?? 2 ** 17;
88
+ return new Promise((resolve, reject) => {
89
+ // `maxmem` doit suivre le coût, sinon Node refuse (« memory limit exceeded »).
90
+ nodeScrypt(passphrase, salt, 32, { N: cost, r: 8, p: 1, maxmem: 256 * cost * 8 }, (err, key) => {
91
+ if (err)
92
+ reject(err);
93
+ else
94
+ resolve({ key: key, salt });
95
+ });
96
+ });
97
+ }
98
+ /** Secret aléatoire (défaut 32 octets) — clés, sels, jetons. */
99
+ export function randomKey(bytes = 32) {
100
+ return randomBytes(bytes);
101
+ }
102
+ // ─── Chiffrement authentifié (AEAD) ──────────────────────────────────────────
103
+ const MAGIC = Buffer.from('MJSC1'); // 5 octets — repère de format, versionné
104
+ const IV_LEN = 12; // GCM : 96 bits, la taille recommandée
105
+ const TAG_LEN = 16;
106
+ /**
107
+ * Chiffre en **AES-256-GCM** (chiffrement AUTHENTIFIÉ : confidentialité ET intégrité).
108
+ *
109
+ * Le format de sortie est **autoportant** — `MJSC1 | iv(12) | tag(16) | chiffré` : il n'y a
110
+ * rien d'autre à conserver que la clé. Sans cela, l'appelant devrait gérer lui-même l'IV et
111
+ * le tag, et un seul oubli rend le déchiffrement impossible… ou pire, l'IV est réutilisé.
112
+ *
113
+ * L'IV est TIRÉ AU HASARD À CHAQUE APPEL et n'est jamais fourni par l'appelant. Réutiliser un
114
+ * IV en GCM est catastrophique — cela révèle le XOR des clairs et permet de forger des
115
+ * messages. C'est la raison pour laquelle cette API n'offre pas l'option.
116
+ */
117
+ export function aeadEncrypt(key, plaintext) {
118
+ if (key.length !== 32)
119
+ throw new Error('crypto-box : la clé AES-256 doit faire 32 octets (voir deriveKey)');
120
+ const iv = randomBytes(IV_LEN);
121
+ const cipher = createCipheriv('aes-256-gcm', Buffer.from(key), iv);
122
+ const body = Buffer.concat([cipher.update(plaintext), cipher.final()]);
123
+ return Buffer.concat([MAGIC, iv, cipher.getAuthTag(), body]);
124
+ }
125
+ /**
126
+ * Déchiffre un blob produit par `aeadEncrypt`.
127
+ *
128
+ * Lève si la clé est fausse OU si le chiffré a été **altéré d'un seul bit** : c'est le tag
129
+ * GCM qui l'impose. Un déchiffrement qui « réussit » sur des octets corrompus est exactement
130
+ * ce qu'on ne veut pas d'une sauvegarde.
131
+ */
132
+ export function aeadDecrypt(key, blob) {
133
+ if (key.length !== 32)
134
+ throw new Error('crypto-box : la clé AES-256 doit faire 32 octets');
135
+ const buf = Buffer.from(blob);
136
+ if (buf.length < MAGIC.length + IV_LEN + TAG_LEN)
137
+ throw new Error('crypto-box : blob tronqué');
138
+ if (!buf.subarray(0, MAGIC.length).equals(MAGIC))
139
+ throw new Error('crypto-box : format inconnu (pas un blob MJSC1)');
140
+ const iv = buf.subarray(MAGIC.length, MAGIC.length + IV_LEN);
141
+ const tag = buf.subarray(MAGIC.length + IV_LEN, MAGIC.length + IV_LEN + TAG_LEN);
142
+ const body = buf.subarray(MAGIC.length + IV_LEN + TAG_LEN);
143
+ const decipher = createDecipheriv('aes-256-gcm', Buffer.from(key), iv);
144
+ decipher.setAuthTag(tag);
145
+ return Buffer.concat([decipher.update(body), decipher.final()]); // `final()` vérifie le tag
146
+ }
package/llms.txt ADDED
@@ -0,0 +1,36 @@
1
+ # @mostajs/crypto-box — 0.1.0
2
+ Auteur : Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later · Niveau N0 · ZÉRO dépendance
3
+
4
+ RÔLE
5
+ LES primitives cryptographiques de l'écosystème — une seule maison, un seul endroit à durcir.
6
+ Enveloppe MINCE et opinionée sur node:crypto. PAS une bibliothèque de crypto : aucun choix
7
+ d'algorithme, aucun mode dangereux, aucun IV fourni par l'appelant.
8
+
9
+ MOTIF
10
+ La même comparaison de signature existait 2x : « best effort » dans @mostajs/url (le KERNEL)
11
+ et correcte dans @mostajs/storage (une feuille). Le durcissement n'avait pas circulé.
12
+
13
+ API
14
+ sha256(data, 'hex'|'base64url') -> string
15
+ sha256Bytes(data) -> Buffer (chaînages)
16
+ hmacSha256(key, data, enc) -> string
17
+ hmacSha256Bytes(key, data) -> Buffer (chaînage SigV4)
18
+ equalsConstantTime(a, b) -> boolean TOUTE comparaison de secret passe ici
19
+ deriveKey(passphrase, {salt?, cost?}) -> {key(32o), salt(16o)} scrypt, N=2^17 par défaut
20
+ randomKey(bytes=32) -> Buffer
21
+ aeadEncrypt(key32, plaintext) -> Buffer AES-256-GCM, format MJSC1|iv|tag|chiffré
22
+ aeadDecrypt(key32, blob) -> Buffer LÈVE si clé fausse OU 1 bit altéré
23
+
24
+ GARANTIES
25
+ - AEAD = confidentialité ET intégrité. Un bit modifié → échec (tag GCM).
26
+ - IV aléatoire à chaque appel, jamais fourni par l'appelant (réutilisation d'IV en GCM =
27
+ catastrophe : révèle le XOR des clairs, permet de forger).
28
+ - Format autoportant : rien à conserver hors la clé.
29
+ - equalsConstantTime ne plante pas sur longueurs différentes (timingSafeEqual de Node lève).
30
+
31
+ HORS PÉRIMÈTRE
32
+ La garde de la clé = décision produit. TicketFlow : clé chez le CLIENT, la console ne stocke
33
+ que du chiffré illisible pour elle. Clé perdue = archive irrécupérable (à contractualiser).
34
+
35
+ TESTS
36
+ 18 tests mjs-unit. Vecteurs vérifiés indépendamment (sha256 coreutils, HMAC RFC 4231 cas 1).
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@mostajs/crypto-box",
3
+ "version": "0.1.0",
4
+ "description": "Les primitives cryptographiques de l'écosystème, une seule maison : sha256, hmac, comparaison à temps constant, AES-256-GCM authentifié, dérivation de clé scrypt. Enveloppe MINCE et opinionée sur node:crypto — pas une bibliothèque de crypto. Primitive N0, zéro dépendance.",
5
+ "license": "AGPL-3.0-or-later",
6
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" }
10
+ },
11
+ "files": ["dist", "README.md", "llms.txt", "LICENSE"],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "test": "bash test-scripts/run-tests.sh",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": ["crypto", "aes-256-gcm", "aead", "hmac", "scrypt", "timing-safe", "zero-dep", "mostajs"],
18
+ "devDependencies": {
19
+ "@mostajs/mjs-unit": "^0.3.1",
20
+ "@types/node": "^20",
21
+ "typescript": "^5.6.0"
22
+ }
23
+ }