ths-csprng 1.0.0 → 1.0.2
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/README.md +88 -46
- package/npm-shrinkwrap.json +31 -0
- package/package.json +15 -3
- package/src/index.js +45 -53
- package/src/index.mjs +117 -0
- package/src/util.js +28 -20
- package/src/util.mjs +176 -0
package/README.md
CHANGED
|
@@ -1,90 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# THS (Temporal-Hardening Solution)
|
|
2
|
+
|
|
3
|
+
THS is a **security-hardened CSPRNG wrapper** designed to protect cryptographic seeds against historical state reconstruction and hardware backdoors. It replaces single-point-in-time entropy with a temporal sequence of "metabolic" system noise, forcing adversaries to perform massive physical work to recover keys.
|
|
4
|
+
|
|
5
|
+
<br />
|
|
6
|
+
|
|
7
|
+
---
|
|
4
8
|
|
|
5
9
|
<br />
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
### 🚀 Quick Start
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
```bash
|
|
14
|
+
npm install ths-csprng
|
|
15
|
+
```
|
|
10
16
|
|
|
11
17
|
<br />
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
#### ESM (Node.js 18+, Vite, Bun)
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
```javascript
|
|
22
|
+
import THS from 'ths-csprng';
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
// Standard 256-bit secure random buffer
|
|
25
|
+
const entropy = await THS.random(32);
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
// High-security hardened key (Computational work required to reconstruct)
|
|
28
|
+
const masterKey = await THS.random(64, {
|
|
29
|
+
layers: 512, // 512 temporal slices
|
|
30
|
+
harden: 3, // Heavy Argon2id hardening
|
|
31
|
+
mem: 65536 // 64MB memory cost
|
|
32
|
+
});
|
|
33
|
+
```
|
|
20
34
|
|
|
21
|
-
|
|
35
|
+
#### CommonJS
|
|
22
36
|
|
|
23
|
-
|
|
37
|
+
```javascript
|
|
38
|
+
const THS = require('ths-csprng');
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
(async () => {
|
|
41
|
+
const entropy = await THS.random(32);
|
|
42
|
+
console.log(entropy.toString('hex'));
|
|
43
|
+
})();
|
|
44
|
+
```
|
|
28
45
|
|
|
29
46
|
<br />
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
---
|
|
32
49
|
|
|
33
|
-
|
|
50
|
+
### 💎 Hardening Levels
|
|
34
51
|
|
|
35
|
-
|
|
52
|
+
| Level | Mode | Description |
|
|
53
|
+
| :--- | :--- | :--- |
|
|
54
|
+
| **0** | **Basic** | High-resolution hardware timing (`hrtime`) per slice. |
|
|
55
|
+
| **1** | **Entropy** | Level 0 + supplemental CSPRNG entropy injection. |
|
|
56
|
+
| **2** | **Internal** | Level 1 + metabolic collection (Internal state snapshots). |
|
|
57
|
+
| **3** | **Heavy** | Level 2 + **Argon2id** memory-hardened processing. |
|
|
36
58
|
|
|
37
59
|
<br />
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
### ⚙️ Full Configuration
|
|
40
64
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
```javascript
|
|
66
|
+
const bytes = await THS.random(32, {
|
|
67
|
+
layers: 128, // Temporal slices (2 to 65536)
|
|
68
|
+
harden: 2, // 0–3 (hardening depth)
|
|
69
|
+
trng: false, // Force /dev/urandom usage
|
|
70
|
+
buffer: true, // Return Node.js Buffer
|
|
71
|
+
|
|
72
|
+
// Argon2id Specifics (Required for level 3)
|
|
73
|
+
mem: 16384, // Memory cost in KB
|
|
74
|
+
passes: 3, // Iterations
|
|
75
|
+
parallelism: 4 // Threads
|
|
76
|
+
});
|
|
77
|
+
```
|
|
44
78
|
|
|
45
79
|
<br />
|
|
46
80
|
|
|
47
81
|
---
|
|
48
82
|
|
|
49
|
-
|
|
83
|
+
### 🧬 Advanced API
|
|
50
84
|
|
|
51
|
-
|
|
85
|
+
#### Direct Utils
|
|
52
86
|
|
|
53
|
-
```
|
|
54
|
-
|
|
87
|
+
```javascript
|
|
88
|
+
// ESM
|
|
89
|
+
import Utils from 'ths-csprng/utils';
|
|
90
|
+
// CJS
|
|
91
|
+
const Utils = require('ths-csprng/utils');
|
|
92
|
+
|
|
93
|
+
// Get a raw metabolic snapshot (*T=n*)
|
|
94
|
+
const snap = await Utils.snapshot(2);
|
|
95
|
+
|
|
96
|
+
// Direct access to the internal TRNG/CSPRNG source
|
|
97
|
+
const raw = await THS.raw(32);
|
|
55
98
|
```
|
|
56
99
|
|
|
57
|
-
<br />
|
|
58
100
|
|
|
59
|
-
|
|
101
|
+
#### Browser Support
|
|
60
102
|
|
|
61
103
|
```javascript
|
|
62
|
-
|
|
104
|
+
const buf = new Uint8Array(32);
|
|
105
|
+
THS.fillRandom(buf);
|
|
106
|
+
```
|
|
63
107
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
memoryH: 1, // Run Argon2id at once
|
|
74
|
-
trng: true // Use randomness from /dev/urandom directly (Unix-like only)
|
|
75
|
-
});
|
|
76
|
-
})();
|
|
108
|
+
|
|
109
|
+
#### Convenient Aliases
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import { randomBytes } from 'ths-csprng';
|
|
113
|
+
|
|
114
|
+
await randomBytes(32);
|
|
115
|
+
await THS.rand(32);
|
|
116
|
+
await THS.rnd(32);
|
|
77
117
|
```
|
|
78
118
|
|
|
79
119
|
<br />
|
|
80
120
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Most people **don't need this**.
|
|
121
|
+
---
|
|
84
122
|
|
|
85
|
-
|
|
123
|
+
### 🛡️ Threat Model
|
|
124
|
+
THS is designed for high-stakes environments where an attacker might:
|
|
125
|
+
1. **Reconstruct historical states** of a machine to guess seeds.
|
|
126
|
+
2. **Exploit VM Snapshots** where cloned instances produce identical "random" output.
|
|
127
|
+
3. **Bypass Hardware Backdoors** by mixing high-variance "metabolic" noise into the entropy pool.
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
**Note:** Hardening Level 3 is computationally expensive. Use Level 0 or 1 for high-frequency needs like UUIDs or nonces.
|
|
88
130
|
|
|
89
131
|
<br />
|
|
90
132
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ths-csprng",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "ths-csprng",
|
|
9
|
+
"version": "1.0.2",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@noble/hashes": "^2.2.0"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"node_modules/@noble/hashes": {
|
|
19
|
+
"version": "2.2.0",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
|
|
21
|
+
"integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">= 20.19.0"
|
|
25
|
+
},
|
|
26
|
+
"funding": {
|
|
27
|
+
"url": "https://paulmillr.com/funding/"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ths-csprng",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Temporal-Hardening Solution: A CSPRNG wrapper designed to resist historical state reconstruction and hardware backdoors.",
|
|
5
|
-
"type": "module",
|
|
6
5
|
"main": "./src/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md",
|
|
10
|
+
"npm-shrinkwrap.json"
|
|
11
|
+
],
|
|
7
12
|
"exports": {
|
|
8
|
-
".":
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./src/index.mjs",
|
|
15
|
+
"require": "./src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./utils": {
|
|
18
|
+
"import": "./src/util.mjs",
|
|
19
|
+
"require": "./src/util.js"
|
|
20
|
+
}
|
|
9
21
|
},
|
|
10
22
|
"author": "Aries Harbinger",
|
|
11
23
|
"license": "Apache-2.0",
|
package/src/index.js
CHANGED
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const Utils = require('./util.js');
|
|
20
|
+
const { kmac256 } = require('@noble/hashes/sha3-addons.js');
|
|
21
|
+
const { webcrypto } = require('node:crypto');
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Temporal-Hardening Solution - Applied in Random Number Generator
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
const THS = Object.freeze({
|
|
28
28
|
/**
|
|
29
29
|
* Generates cryptographically secure random bytes with temporal hardening.
|
|
30
30
|
*/
|
|
@@ -33,10 +33,12 @@ export const THS = {
|
|
|
33
33
|
layers = 128, // number of timeline slices (iterations)
|
|
34
34
|
harden = 2, // levels: 0 (hrtime), 1 (+entropy), 2 (+internal), 3 (+Argon2id)
|
|
35
35
|
trng = false, // attempts to read /dev/urandom, fallback to randomBytes()
|
|
36
|
-
|
|
36
|
+
buffer = true, // return as buffer
|
|
37
37
|
|
|
38
38
|
memoryH = 1, // number of Argon2id runs (requires harden=3 level)
|
|
39
39
|
mem = 16384, // memory hardness of Argon2id (default: 16MB)
|
|
40
|
+
passes = 3, // (t) passes for Argon2id (default: 3)
|
|
41
|
+
parallelism = 4, // (p) parallelism for Argon2id (default: 4)
|
|
40
42
|
label = 'THS-Default-v1' // kmac label
|
|
41
43
|
|
|
42
44
|
} = {}) {
|
|
@@ -56,69 +58,59 @@ export const THS = {
|
|
|
56
58
|
if ('number' !== typeof mem || mem < 1024)
|
|
57
59
|
throw new Error(`Memory set [${mem}] for Argon2id must be a valid number and not be less than 1024 or 1KB.`);
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const moments = [];
|
|
62
|
-
let seed;
|
|
61
|
+
const seed = await Utils.getRandom(32, trng);
|
|
62
|
+
let k;
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
else {
|
|
71
|
-
const h = createHash('sha3-512') // Structurally immune to length-extension attacks
|
|
72
|
-
.update(moment)
|
|
73
|
-
.digest();
|
|
65
|
+
k = kmac256.create(seed, {
|
|
66
|
+
personalization: Utils.toBytes(label),
|
|
67
|
+
dkLen: length
|
|
68
|
+
});
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
for (let i = 0; i < layers; i++) {
|
|
71
|
+
const moment = await Utils.snapshot(harden, memoryH, mem, passes, parallelism, i, trng);
|
|
72
|
+
k.update(moment), moment.fill(0);
|
|
78
73
|
|
|
79
|
-
// Supplemental entropy injected into the moment collection
|
|
80
74
|
if (harden >= 1) {
|
|
75
|
+
// Supplemental entropy injected into the moment collection
|
|
81
76
|
const sup = await Utils.getRandom(12, trng);
|
|
82
|
-
|
|
77
|
+
k.update(sup), sup.fill(0);
|
|
83
78
|
}
|
|
84
79
|
|
|
85
80
|
if (i < layers - 1) await Utils.jitter(trng);
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
const out = k.digest();
|
|
84
|
+
if (!buffer) return out;
|
|
90
85
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
personalization: Utils.toBytes(label),
|
|
94
|
-
dkLen: length
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return Buffer.from(result);
|
|
86
|
+
const b = Buffer.from(out);
|
|
87
|
+
return out.fill(0), b;
|
|
98
88
|
}
|
|
99
89
|
|
|
100
|
-
catch (e) { throw e }
|
|
101
|
-
|
|
102
|
-
finally {
|
|
103
|
-
// Cleanup sensitive memory
|
|
104
|
-
seed && seed.fill(0);
|
|
90
|
+
catch (e) { throw e; }
|
|
105
91
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
92
|
+
finally {
|
|
93
|
+
seed.fill(0);
|
|
94
|
+
k = null;
|
|
109
95
|
}
|
|
110
96
|
},
|
|
111
97
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
async raw(len)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
98
|
+
rand(len, o) { return THS.random(len, o) },
|
|
99
|
+
rnd (len, o) { return THS.random(len, o) },
|
|
100
|
+
|
|
101
|
+
// Direct use of /dev/urandom (fallback to randomBytes)
|
|
102
|
+
async raw(len) { return await Utils.getRandom(len, true) },
|
|
103
|
+
|
|
104
|
+
// Direct Webcrypto access
|
|
105
|
+
fillRandom(arr) {
|
|
106
|
+
return (
|
|
107
|
+
(typeof window !== 'undefined' && window.crypto)
|
|
108
|
+
? window.crypto
|
|
109
|
+
: webcrypto
|
|
110
|
+
).getRandomValues(arr)
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
module.exports = THS;
|
|
115
|
+
module.exports.THS = THS;
|
|
116
|
+
module.exports.randomBytes = (len, o) => THS.random(len, o);
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THS (Temporal-Hardening Solution)
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2026 Aries Harbinger
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import Utils from './util.mjs';
|
|
20
|
+
import { kmac256 } from '@noble/hashes/sha3-addons.js';
|
|
21
|
+
import { createHash, webcrypto } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Temporal-Hardening Solution - Applied in Random Number Generator
|
|
26
|
+
*/
|
|
27
|
+
export const THS = Object.freeze({
|
|
28
|
+
/**
|
|
29
|
+
* Generates cryptographically secure random bytes with temporal hardening.
|
|
30
|
+
*/
|
|
31
|
+
async random(length = 32, {
|
|
32
|
+
|
|
33
|
+
layers = 128, // number of timeline slices (iterations)
|
|
34
|
+
harden = 2, // levels: 0 (hrtime), 1 (+entropy), 2 (+internal), 3 (+Argon2id)
|
|
35
|
+
trng = false, // attempts to read /dev/urandom, fallback to randomBytes()
|
|
36
|
+
buffer = true, // return as buffer
|
|
37
|
+
|
|
38
|
+
memoryH = 1, // number of Argon2id runs (requires harden=3 level)
|
|
39
|
+
mem = 16384, // memory hardness of Argon2id (default: 16MB)
|
|
40
|
+
passes = 3, // (t) passes for Argon2id (default: 3)
|
|
41
|
+
parallelism = 4, // (p) parallelism for Argon2id (default: 4)
|
|
42
|
+
label = 'THS-Default-v1' // kmac label
|
|
43
|
+
|
|
44
|
+
} = {}) {
|
|
45
|
+
|
|
46
|
+
if (length <= 0)
|
|
47
|
+
return Buffer.alloc(0);
|
|
48
|
+
|
|
49
|
+
if (layers < 2 || layers > 65536)
|
|
50
|
+
throw new Error(`Layer count [${layers}] is invalid. Requires a temporal span of 2-65536 cycles to ensure metabolic variance.`);
|
|
51
|
+
|
|
52
|
+
if (harden < 0 || harden > 3)
|
|
53
|
+
throw new Error(`Hardening level [${harden}] is unknown. Use 0 (Basic) through 3 (Heavy).`);
|
|
54
|
+
|
|
55
|
+
if (harden >= 3 && (memoryH < 1 || memoryH > layers))
|
|
56
|
+
throw new Error(`Memory hardening cycle count [${memoryH}] exceeds available temporal layers [${layers}].`);
|
|
57
|
+
|
|
58
|
+
if ('number' !== typeof mem || mem < 1024)
|
|
59
|
+
throw new Error(`Memory set [${mem}] for Argon2id must be a valid number and not be less than 1024 or 1KB.`);
|
|
60
|
+
|
|
61
|
+
const seed = await Utils.getRandom(32, trng);
|
|
62
|
+
let k;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
k = kmac256.create(seed, {
|
|
66
|
+
personalization: Utils.toBytes(label),
|
|
67
|
+
dkLen: length
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < layers; i++) {
|
|
71
|
+
const moment = await Utils.snapshot(harden, memoryH, mem, passes, parallelism, i, trng);
|
|
72
|
+
k.update(moment), moment.fill(0);
|
|
73
|
+
|
|
74
|
+
if (harden >= 1) {
|
|
75
|
+
// Supplemental entropy injected into the moment collection
|
|
76
|
+
const sup = await Utils.getRandom(12, trng);
|
|
77
|
+
k.update(sup), sup.fill(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (i < layers - 1) await Utils.jitter(trng);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const out = k.digest();
|
|
84
|
+
if (!buffer) return out;
|
|
85
|
+
|
|
86
|
+
const b = Buffer.from(out);
|
|
87
|
+
return out.fill(0), b;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
catch (e) { throw e; }
|
|
91
|
+
|
|
92
|
+
finally {
|
|
93
|
+
seed.fill(0);
|
|
94
|
+
k = null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
rand(len, o) { return THS.random(len, o) },
|
|
99
|
+
rnd (len, o) { return THS.random(len, o) },
|
|
100
|
+
|
|
101
|
+
// Direct use of /dev/urandom (fallback to randomBytes)
|
|
102
|
+
async raw(len) { return await Utils.getRandom(len, true) },
|
|
103
|
+
|
|
104
|
+
// Direct Webcrypto access
|
|
105
|
+
fillRandom(arr) {
|
|
106
|
+
return (
|
|
107
|
+
(typeof window !== 'undefined' && window.crypto)
|
|
108
|
+
? window.crypto
|
|
109
|
+
: webcrypto
|
|
110
|
+
).getRandomValues(arr)
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Must use await
|
|
115
|
+
export const randomBytes = (len, o) => THS.random(len, o);
|
|
116
|
+
|
|
117
|
+
export default THS;
|
package/src/util.js
CHANGED
|
@@ -16,31 +16,30 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const fs = require('node:fs/promises');
|
|
20
|
+
const { promisify } = require('node:util');
|
|
21
|
+
const { randomBytes, timingSafeEqual, argon2: _argon2 } = require('node:crypto');
|
|
22
22
|
|
|
23
23
|
const argon2 = promisify(_argon2);
|
|
24
24
|
const defined_past = process.hrtime.bigint();
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const Utils = Object.freeze({
|
|
28
28
|
/**
|
|
29
29
|
* Captures a high-resolution snapshot of the Node.js metabolic state.
|
|
30
30
|
* Uses concatenation to prevent entropy collisions.
|
|
31
31
|
*/
|
|
32
|
-
async snapshot(h, mh, mem, s, useUrandom) {
|
|
32
|
+
async snapshot(h, mh, mem, t, p, s, useUrandom) {
|
|
33
33
|
// Level 1: Minimal data
|
|
34
34
|
if (h <= 1) return this.bigIntToBuffer(process.hrtime.bigint());
|
|
35
35
|
|
|
36
36
|
const u = process.resourceUsage();
|
|
37
37
|
const m = process.memoryUsage();
|
|
38
38
|
|
|
39
|
-
// Primary metabolic collection (Machine State)
|
|
40
|
-
// We use an array to preserve individual entropy of each metric
|
|
39
|
+
// Level 2: Primary metabolic collection (Machine State)
|
|
41
40
|
const metrics = [
|
|
42
41
|
process.hrtime.bigint(), // CPU Nanoseconds
|
|
43
|
-
BigInt(Math.round(process.uptime() * 1_000_000)),
|
|
42
|
+
BigInt(Math.round(process.uptime() * 1_000_000)),
|
|
44
43
|
BigInt(u.userCPUTime), // User-space CPU usage
|
|
45
44
|
BigInt(u.systemCPUTime), // Kernel-space CPU usage
|
|
46
45
|
BigInt(u.minorPageFault), // Soft memory faults
|
|
@@ -55,7 +54,7 @@ export const Utils = {
|
|
|
55
54
|
BigInt(Math.round(performance.now() * 1_000_000)),
|
|
56
55
|
defined_past, // Process start anchor
|
|
57
56
|
BigInt(Date.now()), // Wall clock (Temporal anchor)
|
|
58
|
-
BigInt(s)
|
|
57
|
+
BigInt(s || 0) // Sequential salt
|
|
59
58
|
].map(n => this.bigIntToBuffer(n));
|
|
60
59
|
|
|
61
60
|
// Map metrics to uniquely structured buffers
|
|
@@ -71,25 +70,33 @@ export const Utils = {
|
|
|
71
70
|
if (h <= 2 || s >= mh) return b;
|
|
72
71
|
|
|
73
72
|
// Level 3: Argon2id Memory-Hardening
|
|
74
|
-
|
|
73
|
+
let nonce, memHard;
|
|
75
74
|
|
|
76
75
|
try {
|
|
77
|
-
|
|
76
|
+
nonce = await this.getRandom(16, useUrandom);
|
|
77
|
+
memHard = await argon2('argon2id', {
|
|
78
78
|
nonce,
|
|
79
79
|
message: b,
|
|
80
80
|
memory: mem,
|
|
81
|
-
passes:
|
|
82
|
-
parallelism:
|
|
81
|
+
passes: t,
|
|
82
|
+
parallelism: p,
|
|
83
83
|
tagLength: 64
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const result = Buffer.concat([memHard, b]);
|
|
87
|
+
if (h <= 3) return result;
|
|
88
|
+
|
|
89
|
+
throw new Error(`Hardening level [${h}] is unknown. Use 0 (Basic) through 3 (Heavy).`);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
catch (e) { throw e }
|
|
92
|
-
|
|
92
|
+
catch (e) { throw e; }
|
|
93
|
+
|
|
94
|
+
finally {
|
|
95
|
+
// wipe result
|
|
96
|
+
if (memHard) memHard.fill(0);
|
|
97
|
+
if (nonce) nonce.fill(0);
|
|
98
|
+
b.fill(0);
|
|
99
|
+
}
|
|
93
100
|
},
|
|
94
101
|
|
|
95
102
|
/**
|
|
@@ -164,6 +171,7 @@ export const Utils = {
|
|
|
164
171
|
if (typeof input === 'string') return new TextEncoder().encode(input);
|
|
165
172
|
return new TextEncoder().encode(JSON.stringify(input) || '');
|
|
166
173
|
}
|
|
167
|
-
};
|
|
174
|
+
});
|
|
168
175
|
|
|
169
|
-
|
|
176
|
+
module.exports = Utils;
|
|
177
|
+
module.exports.Utils = Utils;
|
package/src/util.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THS (Temporal-Hardening Solution)
|
|
3
|
+
*
|
|
4
|
+
* Copyright 2026 Aries Harbinger
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import { promisify } from 'node:util';
|
|
21
|
+
import { randomBytes, timingSafeEqual, argon2 as _argon2 } from 'node:crypto';
|
|
22
|
+
|
|
23
|
+
const argon2 = promisify(_argon2);
|
|
24
|
+
const defined_past = process.hrtime.bigint();
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export const Utils = Object.freeze({
|
|
28
|
+
/**
|
|
29
|
+
* Captures a high-resolution snapshot of the Node.js metabolic state.
|
|
30
|
+
* Uses concatenation to prevent entropy collisions.
|
|
31
|
+
*/
|
|
32
|
+
async snapshot(h, mh, mem, t, p, s, useUrandom) {
|
|
33
|
+
// Level 1: Minimal data
|
|
34
|
+
if (h <= 1) return this.bigIntToBuffer(process.hrtime.bigint());
|
|
35
|
+
|
|
36
|
+
const u = process.resourceUsage();
|
|
37
|
+
const m = process.memoryUsage();
|
|
38
|
+
|
|
39
|
+
// Level 2: Primary metabolic collection (Machine State)
|
|
40
|
+
const metrics = [
|
|
41
|
+
process.hrtime.bigint(), // CPU Nanoseconds
|
|
42
|
+
BigInt(Math.round(process.uptime() * 1_000_000)),
|
|
43
|
+
BigInt(u.userCPUTime), // User-space CPU usage
|
|
44
|
+
BigInt(u.systemCPUTime), // Kernel-space CPU usage
|
|
45
|
+
BigInt(u.minorPageFault), // Soft memory faults
|
|
46
|
+
BigInt(u.majorPageFault), // Hard I/O faults
|
|
47
|
+
BigInt(u.voluntaryContextSwitches),
|
|
48
|
+
BigInt(u.involuntaryContextSwitches),
|
|
49
|
+
BigInt(m.heapUsed), // V8 actual usage
|
|
50
|
+
BigInt(m.heapTotal), // V8 allocated total
|
|
51
|
+
BigInt(m.external), // C++ object memory
|
|
52
|
+
BigInt(m.arrayBuffers || 0), // Raw buffer memory
|
|
53
|
+
BigInt(m.rss), // Resident Set Size (Physical RAM)
|
|
54
|
+
BigInt(Math.round(performance.now() * 1_000_000)),
|
|
55
|
+
defined_past, // Process start anchor
|
|
56
|
+
BigInt(Date.now()), // Wall clock (Temporal anchor)
|
|
57
|
+
BigInt(s || 0) // Sequential salt
|
|
58
|
+
].map(n => this.bigIntToBuffer(n));
|
|
59
|
+
|
|
60
|
+
// Map metrics to uniquely structured buffers
|
|
61
|
+
const b = Buffer.concat([
|
|
62
|
+
|
|
63
|
+
// Inject 128 bytes of raw, uninitialized system memory
|
|
64
|
+
// (May contain fragments of previous internal function calls/pointers)
|
|
65
|
+
Buffer.allocUnsafe(128),
|
|
66
|
+
|
|
67
|
+
...metrics
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
if (h <= 2 || s >= mh) return b;
|
|
71
|
+
|
|
72
|
+
// Level 3: Argon2id Memory-Hardening
|
|
73
|
+
let nonce, memHard;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
nonce = await this.getRandom(16, useUrandom);
|
|
77
|
+
memHard = await argon2('argon2id', {
|
|
78
|
+
nonce,
|
|
79
|
+
message: b,
|
|
80
|
+
memory: mem,
|
|
81
|
+
passes: t,
|
|
82
|
+
parallelism: p,
|
|
83
|
+
tagLength: 64
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = Buffer.concat([memHard, b]);
|
|
87
|
+
if (h <= 3) return result;
|
|
88
|
+
|
|
89
|
+
throw new Error(`Hardening level [${h}] is unknown. Use 0 (Basic) through 3 (Heavy).`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
catch (e) { throw e; }
|
|
93
|
+
|
|
94
|
+
finally {
|
|
95
|
+
// wipe result
|
|
96
|
+
if (memHard) memHard.fill(0);
|
|
97
|
+
if (nonce) nonce.fill(0);
|
|
98
|
+
b.fill(0);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Attempts to harvest raw entropy from the OS device.
|
|
104
|
+
* Fallback to hardware-backed Node.js randomBytes on failure.
|
|
105
|
+
*/
|
|
106
|
+
async getRandom(len, useUrandom) {
|
|
107
|
+
if (!useUrandom) return randomBytes(len);
|
|
108
|
+
|
|
109
|
+
let fd;
|
|
110
|
+
try {
|
|
111
|
+
fd = await fs.open('/dev/urandom', 'r');
|
|
112
|
+
const buffer = Buffer.allocUnsafe(len);
|
|
113
|
+
const { bytesRead } = await fd.read(buffer, 0, len, 0);
|
|
114
|
+
|
|
115
|
+
// If we didn't read enough bytes for some reason, fallback
|
|
116
|
+
return (bytesRead < len)
|
|
117
|
+
? randomBytes(len)
|
|
118
|
+
: buffer;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
catch (e) { return randomBytes(len); }
|
|
122
|
+
finally { if (fd) await fd.close(); }
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Micro-architectural noise generator.
|
|
127
|
+
*/
|
|
128
|
+
async jitter(useUrandom) {
|
|
129
|
+
const b = await this.getRandom(64, useUrandom);
|
|
130
|
+
timingSafeEqual(b, b);
|
|
131
|
+
b.fill(0);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Optimized BigInt to Buffer conversion with 16-bit Length Header.
|
|
136
|
+
* Prepends a 2-byte length header (Big-Endian) followed by the data.
|
|
137
|
+
*/
|
|
138
|
+
bigIntToBuffer(n) {
|
|
139
|
+
if (typeof n !== 'bigint') n = BigInt(n);
|
|
140
|
+
if (n < 0n) throw new RangeError('Negative values not supported');
|
|
141
|
+
|
|
142
|
+
// Fast path for zero: [Length: 0x0001] [Value: 0x00]
|
|
143
|
+
if (n === 0n) {
|
|
144
|
+
const buf = Buffer.allocUnsafe(3);
|
|
145
|
+
buf.writeUInt16BE(1, 0); // Length is 1 byte
|
|
146
|
+
buf[2] = 0; // Value is 0
|
|
147
|
+
return buf;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Convert BigInt to Buffer via Hex
|
|
151
|
+
let hex = n.toString(16);
|
|
152
|
+
if (hex.length % 2 !== 0) hex = '0' + hex;
|
|
153
|
+
const data = Buffer.from(hex, 'hex');
|
|
154
|
+
|
|
155
|
+
// Safety check for 16-bit overflow
|
|
156
|
+
if (data.length > 0xFFFF)
|
|
157
|
+
throw new RangeError('BigInt exceeds 16-bit length header capacity');
|
|
158
|
+
|
|
159
|
+
// Structure: [Header (2 bytes)] + [Data]
|
|
160
|
+
const header = Buffer.allocUnsafe(2);
|
|
161
|
+
header.writeUInt16BE(data.length, 0);
|
|
162
|
+
|
|
163
|
+
return Buffer.concat([header, data]);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Normalizes various input types into a Uint8Array.
|
|
168
|
+
*/
|
|
169
|
+
toBytes(input) {
|
|
170
|
+
if (input instanceof Uint8Array) return input;
|
|
171
|
+
if (typeof input === 'string') return new TextEncoder().encode(input);
|
|
172
|
+
return new TextEncoder().encode(JSON.stringify(input) || '');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export default Utils;
|