@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 +29 -0
- package/README.md +84 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +146 -0
- package/llms.txt +36 -0
- package/package.json +23 -0
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.
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|