@mostajs/enrollment 0.1.0 → 0.2.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/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/machine.d.ts +64 -0
- package/dist/machine.js +105 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
export declare const moduleInfo: {
|
|
31
31
|
readonly name: "@mostajs/enrollment";
|
|
32
|
-
readonly version: "0.
|
|
32
|
+
readonly version: "0.2.0";
|
|
33
33
|
readonly level: "N1";
|
|
34
34
|
};
|
|
35
35
|
export { generateCode, parseCode, normalizeCode, hashCode, verifyCode } from './code.js';
|
|
@@ -39,3 +39,5 @@ export type { EnrollmentServiceOptions, RequestOutcome } from './service.js';
|
|
|
39
39
|
export { createEnrollmentAgent, generateIdentity } from './agent.js';
|
|
40
40
|
export type { EnrollmentAgentOptions, AgentState, AgentIdentity } from './agent.js';
|
|
41
41
|
export type { EnrollmentState, EnrollmentRequest, EnrolledSite, Invitation, EnrollmentStore, ApprovalPayload, ReenrollmentAlert, } from './types.js';
|
|
42
|
+
export { readMachineId } from './machine.js';
|
|
43
|
+
export type { MachineId, MachineIdSource, ReadMachineIdOptions } from './machine.js';
|
package/dist/index.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* L'approbation est un CLIC humain — mais l'agent RE-SONDE en boucle. Le client installe et part
|
|
28
28
|
* travailler ; vous approuvez quand vous voulez ; la machine se rattache toute seule.
|
|
29
29
|
*/
|
|
30
|
-
export const moduleInfo = { name: '@mostajs/enrollment', version: '0.
|
|
30
|
+
export const moduleInfo = { name: '@mostajs/enrollment', version: '0.2.0', level: 'N1' };
|
|
31
31
|
export { generateCode, parseCode, normalizeCode, hashCode, verifyCode } from './code.js';
|
|
32
32
|
export { createEnrollmentService } from './service.js';
|
|
33
33
|
export { createEnrollmentAgent, generateIdentity } from './agent.js';
|
|
34
|
+
export { readMachineId } from './machine.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/enrollment — Lire l'identifiant de la MACHINE, sans dépendance native.
|
|
3
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
* Licence : AGPL-3.0-or-later
|
|
5
|
+
*
|
|
6
|
+
* ─── POURQUOI ICI ────────────────────────────────────────────────────────────
|
|
7
|
+
* L'agent exige un `machineId`, et TOUTE machine qui s'enrôle doit savoir le produire. Le laisser
|
|
8
|
+
* à la charge de chaque application, c'est garantir N implémentations divergentes — et une
|
|
9
|
+
* dépendance à `node-machine-id` (binaire natif) qui interdirait un `.exe` PUR-JS.
|
|
10
|
+
*
|
|
11
|
+
* ─── CE QUE CET IDENTIFIANT EST, ET N'EST PAS ────────────────────────────────
|
|
12
|
+
* Il **identifie**, il n'**authentifie** pas. Il est lisible par n'importe qui sur le poste et
|
|
13
|
+
* falsifiable par quiconque contrôle la machine. C'est la clé Ed25519 — privée, jamais transmise —
|
|
14
|
+
* qui authentifie. Le `machineId` sert à une seule chose : reconnaître « c'est le même poste » afin
|
|
15
|
+
* de LEVER L'ALERTE de ré-enrôlement quand la clé, elle, a changé.
|
|
16
|
+
*
|
|
17
|
+
* ─── ET IL EST HACHÉ ─────────────────────────────────────────────────────────
|
|
18
|
+
* On ne transmet JAMAIS le GUID brut de l'OS. Il est haché avec un `salt` propre à l'application :
|
|
19
|
+
* - la console n'apprend pas l'identifiant système du client (elle n'en a aucun besoin) ;
|
|
20
|
+
* - le même poste enrôlé dans deux applications donne deux identifiants DIFFÉRENTS — impossible
|
|
21
|
+
* de recouper les parcs de deux clients.
|
|
22
|
+
* Le hachage est stable : même poste + même salt ⇒ même identifiant, indéfiniment.
|
|
23
|
+
*/
|
|
24
|
+
/** D'où vient l'identifiant — utile pour DIRE au client ce qui se passe, notamment `aleatoire`. */
|
|
25
|
+
export type MachineIdSource = 'windows-registry' | 'linux-machine-id' | 'macos-ioplatform' | 'aleatoire';
|
|
26
|
+
export interface MachineId {
|
|
27
|
+
/** L'identifiant à transmettre : haché, stable, propre à l'application. */
|
|
28
|
+
id: string;
|
|
29
|
+
source: MachineIdSource;
|
|
30
|
+
/**
|
|
31
|
+
* Présent UNIQUEMENT quand l'OS n'a rien fourni et qu'une graine a dû être tirée au sort.
|
|
32
|
+
* **L'hôte DOIT la persister** et la repasser en `fallback` au démarrage suivant.
|
|
33
|
+
*
|
|
34
|
+
* Sans cela, `id` change à chaque lancement et la machine — pourtant déjà enrôlée — se
|
|
35
|
+
* représente comme un poste inconnu à chaque redémarrage. Le module ne peut pas persister à
|
|
36
|
+
* votre place (il ne choisit pas votre stockage) : il vous rend donc la graine, explicitement.
|
|
37
|
+
*/
|
|
38
|
+
seedToPersist?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ReadMachineIdOptions {
|
|
41
|
+
/**
|
|
42
|
+
* Sel propre à l'application (ex. `'ticketflow-server'`). Deux applications ⇒ deux identifiants
|
|
43
|
+
* pour le MÊME poste : aucun recoupement de parcs possible.
|
|
44
|
+
*/
|
|
45
|
+
salt: string;
|
|
46
|
+
/**
|
|
47
|
+
* Identifiant de repli, DÉJÀ PERSISTÉ par l'hôte, si l'OS n'en fournit aucun. Voir ci-dessous
|
|
48
|
+
* pourquoi il DOIT être persisté.
|
|
49
|
+
*/
|
|
50
|
+
fallback?: string | null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Lit l'identifiant de la machine, haché avec le sel de l'application.
|
|
54
|
+
*
|
|
55
|
+
* ─── LE REPLI ALÉATOIRE EST UN PIÈGE — LISEZ CECI ────────────────────────────
|
|
56
|
+
* Quand l'OS ne fournit rien (conteneur nu, registre verrouillé), on renvoie `source: 'aleatoire'`
|
|
57
|
+
* et un identifiant tiré au sort. **L'hôte DOIT le persister** et le repasser en `fallback` aux
|
|
58
|
+
* démarrages suivants. Sinon l'identifiant change à chaque lancement — et la machine, déjà
|
|
59
|
+
* enrôlée, se présente à chaque fois comme un poste inconnu.
|
|
60
|
+
*
|
|
61
|
+
* On ne le persiste pas nous-mêmes : ce module ne choisit pas votre stockage (fichier ? base ?
|
|
62
|
+
* `userData` d'Electron ?). Mais on vous DIT, par `source`, que la persistance est obligatoire.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readMachineId(opts: ReadMachineIdOptions): Promise<MachineId>;
|
package/dist/machine.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/enrollment — Lire l'identifiant de la MACHINE, sans dépendance native.
|
|
3
|
+
* @author Dr Hamid MADANI <drmdh@msn.com>
|
|
4
|
+
* Licence : AGPL-3.0-or-later
|
|
5
|
+
*
|
|
6
|
+
* ─── POURQUOI ICI ────────────────────────────────────────────────────────────
|
|
7
|
+
* L'agent exige un `machineId`, et TOUTE machine qui s'enrôle doit savoir le produire. Le laisser
|
|
8
|
+
* à la charge de chaque application, c'est garantir N implémentations divergentes — et une
|
|
9
|
+
* dépendance à `node-machine-id` (binaire natif) qui interdirait un `.exe` PUR-JS.
|
|
10
|
+
*
|
|
11
|
+
* ─── CE QUE CET IDENTIFIANT EST, ET N'EST PAS ────────────────────────────────
|
|
12
|
+
* Il **identifie**, il n'**authentifie** pas. Il est lisible par n'importe qui sur le poste et
|
|
13
|
+
* falsifiable par quiconque contrôle la machine. C'est la clé Ed25519 — privée, jamais transmise —
|
|
14
|
+
* qui authentifie. Le `machineId` sert à une seule chose : reconnaître « c'est le même poste » afin
|
|
15
|
+
* de LEVER L'ALERTE de ré-enrôlement quand la clé, elle, a changé.
|
|
16
|
+
*
|
|
17
|
+
* ─── ET IL EST HACHÉ ─────────────────────────────────────────────────────────
|
|
18
|
+
* On ne transmet JAMAIS le GUID brut de l'OS. Il est haché avec un `salt` propre à l'application :
|
|
19
|
+
* - la console n'apprend pas l'identifiant système du client (elle n'en a aucun besoin) ;
|
|
20
|
+
* - le même poste enrôlé dans deux applications donne deux identifiants DIFFÉRENTS — impossible
|
|
21
|
+
* de recouper les parcs de deux clients.
|
|
22
|
+
* Le hachage est stable : même poste + même salt ⇒ même identifiant, indéfiniment.
|
|
23
|
+
*/
|
|
24
|
+
import { execFile } from 'node:child_process';
|
|
25
|
+
import { readFile } from 'node:fs/promises';
|
|
26
|
+
import { hmacSha256, randomKey } from '@mostajs/crypto-box';
|
|
27
|
+
const run = (cmd, args) => new Promise((resolve, reject) => {
|
|
28
|
+
// execFile, PAS exec : aucun shell, donc aucune injection possible même si un jour un
|
|
29
|
+
// paramètre venait de l'extérieur.
|
|
30
|
+
execFile(cmd, args, { timeout: 5_000, windowsHide: true }, (err, stdout) => {
|
|
31
|
+
if (err)
|
|
32
|
+
reject(err);
|
|
33
|
+
else
|
|
34
|
+
resolve(String(stdout));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
/** Le GUID BRUT de l'OS. Interne : il ne doit jamais sortir d'ici sans être haché. */
|
|
38
|
+
async function guidNatif() {
|
|
39
|
+
try {
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
// MachineGuid : posé par Windows à l'installation, stable, lisible sans privilège.
|
|
42
|
+
const out = await run(`${process.env.SystemRoot ?? 'C:\\Windows'}\\System32\\reg.exe`, [
|
|
43
|
+
'QUERY', 'HKLM\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid',
|
|
44
|
+
]);
|
|
45
|
+
const m = /MachineGuid\s+REG_SZ\s+(\S+)/i.exec(out);
|
|
46
|
+
if (m)
|
|
47
|
+
return { brut: m[1], source: 'windows-registry' };
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === 'darwin') {
|
|
51
|
+
const out = await run('/usr/sbin/ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']);
|
|
52
|
+
const m = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out);
|
|
53
|
+
if (m)
|
|
54
|
+
return { brut: m[1], source: 'macos-ioplatform' };
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (process.platform === 'linux') {
|
|
58
|
+
// /etc/machine-id (systemd) ; le repli D-Bus couvre les distributions qui ne l'ont pas.
|
|
59
|
+
for (const f of ['/etc/machine-id', '/var/lib/dbus/machine-id']) {
|
|
60
|
+
try {
|
|
61
|
+
const brut = (await readFile(f, 'utf8')).trim();
|
|
62
|
+
if (brut)
|
|
63
|
+
return { brut, source: 'linux-machine-id' };
|
|
64
|
+
}
|
|
65
|
+
catch { /* fichier absent : on essaie le suivant */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Tout autre OS (AIX, BSD, SunOS…) : on ne devine PAS. Étiqueter `linux-machine-id` un
|
|
69
|
+
// identifiant qui ne vient pas de Linux serait un mensonge dans les journaux — et le jour où
|
|
70
|
+
// il faudra diagnostiquer une machine qui se ré-enrôle sans cesse, ce mensonge coûtera cher.
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Registre verrouillé par une GPO, conteneur sans /etc/machine-id, binaire absent… On ne
|
|
75
|
+
// fait pas échouer l'application pour ça : l'appelant recevra `aleatoire` et saura quoi dire.
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Lit l'identifiant de la machine, haché avec le sel de l'application.
|
|
81
|
+
*
|
|
82
|
+
* ─── LE REPLI ALÉATOIRE EST UN PIÈGE — LISEZ CECI ────────────────────────────
|
|
83
|
+
* Quand l'OS ne fournit rien (conteneur nu, registre verrouillé), on renvoie `source: 'aleatoire'`
|
|
84
|
+
* et un identifiant tiré au sort. **L'hôte DOIT le persister** et le repasser en `fallback` aux
|
|
85
|
+
* démarrages suivants. Sinon l'identifiant change à chaque lancement — et la machine, déjà
|
|
86
|
+
* enrôlée, se présente à chaque fois comme un poste inconnu.
|
|
87
|
+
*
|
|
88
|
+
* On ne le persiste pas nous-mêmes : ce module ne choisit pas votre stockage (fichier ? base ?
|
|
89
|
+
* `userData` d'Electron ?). Mais on vous DIT, par `source`, que la persistance est obligatoire.
|
|
90
|
+
*/
|
|
91
|
+
export async function readMachineId(opts) {
|
|
92
|
+
const natif = await guidNatif();
|
|
93
|
+
if (natif) {
|
|
94
|
+
// HMAC et pas SHA-256 nu : sans clé, un GUID Windows (espace connu et énumérable) se
|
|
95
|
+
// retrouverait par force brute à partir du haché. Le sel, secret ou non, coupe le dictionnaire.
|
|
96
|
+
return { id: hmacSha256(opts.salt, natif.brut), source: natif.source };
|
|
97
|
+
}
|
|
98
|
+
const repli = opts.fallback?.trim();
|
|
99
|
+
if (repli)
|
|
100
|
+
return { id: hmacSha256(opts.salt, repli), source: 'aleatoire' };
|
|
101
|
+
// Aucune graine fournie : on en tire une — et on la REND, pour que l'hôte la persiste. La
|
|
102
|
+
// garder pour nous produirait un identifiant neuf à chaque démarrage : le pire des deux mondes.
|
|
103
|
+
const graine = randomKey(32).toString('hex');
|
|
104
|
+
return { id: hmacSha256(opts.salt, graine), source: 'aleatoire', seedToPersist: graine };
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/enrollment",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Enrôler une machine chez un client distant, sans configuration : code dictable au téléphone (usage unique, 7 jours), identité Ed25519 générée sur la machine, approbation par un clic dans la console, re-tentative automatique. Compose crypto-box et scheduler.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|