hive-p2p 1.0.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 +674 -0
- package/README.md +512 -0
- package/browser.mjs +4 -0
- package/core/arbiter.mjs +125 -0
- package/core/crypto-codex.mjs +249 -0
- package/core/gossip.mjs +159 -0
- package/core/ice-offer-manager.mjs +175 -0
- package/core/node-services.mjs +126 -0
- package/core/node.mjs +146 -0
- package/core/parameters.mjs +142 -0
- package/core/peer-store.mjs +252 -0
- package/core/route-builder.mjs +176 -0
- package/core/topologist.mjs +254 -0
- package/core/unicast.mjs +155 -0
- package/index.mjs +4 -0
- package/libs/argon2-ES6.min.mjs +1 -0
- package/libs/socket.io-4-8-1.min.js +7 -0
- package/libs/three-4.5.min.js +6 -0
- package/libs/xxhash32.mjs +65 -0
- package/package.json +44 -0
- package/services/clock.mjs +144 -0
- package/services/converter.mjs +64 -0
- package/services/cryptos.mjs +83 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const PRIME32_1 = 2654435761;
|
|
2
|
+
const PRIME32_2 = 2246822519;
|
|
3
|
+
const PRIME32_3 = 3266489917;
|
|
4
|
+
const PRIME32_4 = 668265263;
|
|
5
|
+
const PRIME32_5 = 374761393;
|
|
6
|
+
let encoder;
|
|
7
|
+
/** @param input - byte array or string @param seed - optional seed (32-bit unsigned); */
|
|
8
|
+
export function xxHash32(input, seed = 0) {
|
|
9
|
+
const buffer = typeof input === 'string' ? (encoder ??= new TextEncoder()).encode(input) : input;
|
|
10
|
+
const b = buffer;
|
|
11
|
+
let acc = (seed + PRIME32_5) & 0xffffffff;
|
|
12
|
+
let offset = 0;
|
|
13
|
+
if (b.length >= 16) {
|
|
14
|
+
const accN = [
|
|
15
|
+
(seed + PRIME32_1 + PRIME32_2) & 0xffffffff,
|
|
16
|
+
(seed + PRIME32_2) & 0xffffffff,
|
|
17
|
+
(seed + 0) & 0xffffffff,
|
|
18
|
+
(seed - PRIME32_1) & 0xffffffff,
|
|
19
|
+
];
|
|
20
|
+
const b = buffer;
|
|
21
|
+
const limit = b.length - 16;
|
|
22
|
+
let lane = 0;
|
|
23
|
+
for (offset = 0; (offset & 0xfffffff0) <= limit; offset += 4) {
|
|
24
|
+
const i = offset;
|
|
25
|
+
const laneN0 = b[i + 0] + (b[i + 1] << 8);
|
|
26
|
+
const laneN1 = b[i + 2] + (b[i + 3] << 8);
|
|
27
|
+
const laneNP = laneN0 * PRIME32_2 + ((laneN1 * PRIME32_2) << 16);
|
|
28
|
+
let acc = (accN[lane] + laneNP) & 0xffffffff;
|
|
29
|
+
acc = (acc << 13) | (acc >>> 19);
|
|
30
|
+
const acc0 = acc & 0xffff;
|
|
31
|
+
const acc1 = acc >>> 16;
|
|
32
|
+
accN[lane] = (acc0 * PRIME32_1 + ((acc1 * PRIME32_1) << 16)) & 0xffffffff;
|
|
33
|
+
lane = (lane + 1) & 0x3;
|
|
34
|
+
}
|
|
35
|
+
acc =
|
|
36
|
+
(((accN[0] << 1) | (accN[0] >>> 31)) +
|
|
37
|
+
((accN[1] << 7) | (accN[1] >>> 25)) +
|
|
38
|
+
((accN[2] << 12) | (accN[2] >>> 20)) +
|
|
39
|
+
((accN[3] << 18) | (accN[3] >>> 14))) &
|
|
40
|
+
0xffffffff;
|
|
41
|
+
}
|
|
42
|
+
acc = (acc + buffer.length) & 0xffffffff;
|
|
43
|
+
const limit = buffer.length - 4;
|
|
44
|
+
for (; offset <= limit; offset += 4) {
|
|
45
|
+
const i = offset;
|
|
46
|
+
const laneN0 = b[i + 0] + (b[i + 1] << 8);
|
|
47
|
+
const laneN1 = b[i + 2] + (b[i + 3] << 8);
|
|
48
|
+
const laneP = laneN0 * PRIME32_3 + ((laneN1 * PRIME32_3) << 16);
|
|
49
|
+
acc = (acc + laneP) & 0xffffffff;
|
|
50
|
+
acc = (acc << 17) | (acc >>> 15);
|
|
51
|
+
acc = ((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) & 0xffffffff;
|
|
52
|
+
}
|
|
53
|
+
for (; offset < b.length; ++offset) {
|
|
54
|
+
const lane = b[offset];
|
|
55
|
+
acc = acc + lane * PRIME32_5;
|
|
56
|
+
acc = (acc << 11) | (acc >>> 21);
|
|
57
|
+
acc = ((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) & 0xffffffff;
|
|
58
|
+
}
|
|
59
|
+
acc = acc ^ (acc >>> 15);
|
|
60
|
+
acc = (((acc & 0xffff) * PRIME32_2) & 0xffffffff) + (((acc >>> 16) * PRIME32_2) << 16);
|
|
61
|
+
acc = acc ^ (acc >>> 13);
|
|
62
|
+
acc = (((acc & 0xffff) * PRIME32_3) & 0xffffffff) + (((acc >>> 16) * PRIME32_3) << 16);
|
|
63
|
+
acc = acc ^ (acc >>> 16);
|
|
64
|
+
return acc < 0 ? acc + 4294967296 : acc;
|
|
65
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hive-p2p",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "P2P networking library for browser and Node.js with WebRTC",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.mjs",
|
|
7
|
+
"browser": "./browser.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"browser": "./browser.mjs",
|
|
11
|
+
"node": "./index.mjs",
|
|
12
|
+
"default": "./index.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"core/",
|
|
17
|
+
"libs/",
|
|
18
|
+
"services/",
|
|
19
|
+
"index.mjs",
|
|
20
|
+
"browser.mjs"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@noble/ed25519": "3.0.0",
|
|
24
|
+
"@noble/hashes": "2.0.0",
|
|
25
|
+
"argon2": "~0.44.0",
|
|
26
|
+
"express": "~5.1.0",
|
|
27
|
+
"node-pre-gyp": "~0.17.0",
|
|
28
|
+
"simple-peer": "~9.11.1",
|
|
29
|
+
"socket.io-client": "~4.8.1",
|
|
30
|
+
"socket.io": "~4.8.1",
|
|
31
|
+
"wrtc": "~0.4.7",
|
|
32
|
+
"ws": "~8.18.3"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/Seigneur-Machiavel/hive-p2p.git"
|
|
37
|
+
},
|
|
38
|
+
"keywords": ["p2p", "webrtc", "networking", "browser", "nodejs", "gossip", "mesh"],
|
|
39
|
+
"author": "Seigneur-Machiavel",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronized Network Clock
|
|
3
|
+
* Simple, efficient NTP-based time synchronization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class Clock {
|
|
7
|
+
verbose;
|
|
8
|
+
mockMode; // if true, use local time without sync
|
|
9
|
+
static #instance = null;
|
|
10
|
+
|
|
11
|
+
#offset = null; // ms difference from local time
|
|
12
|
+
#syncing = false;
|
|
13
|
+
#lastSync = 0;
|
|
14
|
+
#sources = ['time.google.com', 'time.cloudflare.com', 'pool.ntp.org'];
|
|
15
|
+
|
|
16
|
+
constructor(verbose = 0, mockMode = false) {
|
|
17
|
+
this.verbose = verbose;
|
|
18
|
+
this.mockMode = mockMode;
|
|
19
|
+
if (Clock.#instance) return Clock.#instance;
|
|
20
|
+
else Clock.#instance = this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// PUBLIC API
|
|
24
|
+
static get instance() { return Clock.#instance || new Clock(); }
|
|
25
|
+
static get time() { return Clock.instance.time; } // Sync API - returns current synchronized time or null
|
|
26
|
+
get time() {
|
|
27
|
+
if (this.mockMode) return Date.now();
|
|
28
|
+
if (this.#offset === null) return null;
|
|
29
|
+
return Date.now() + Math.round(this.#offset);
|
|
30
|
+
}
|
|
31
|
+
async sync(verbose) { // Force synchronization - returns promise with synchronized time
|
|
32
|
+
if (verbose !== undefined) this.verbose = verbose;
|
|
33
|
+
if (this.mockMode) return Date.now(); // Bypass sync in mock mode
|
|
34
|
+
|
|
35
|
+
if (this.#syncing) { // Wait for current sync to complete
|
|
36
|
+
while (this.#syncing) await new Promise(resolve => setTimeout(resolve, 50));
|
|
37
|
+
return this.time;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.#syncing = true;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const samples = await this.#fetchTimeSamples();
|
|
44
|
+
if (samples.length === 0) {
|
|
45
|
+
console.warn('[Clock] All NTP sources failed, using local time');
|
|
46
|
+
this.#offset = 0;
|
|
47
|
+
return this.time;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.#offset = this.#calculateOffset(samples);
|
|
51
|
+
this.#lastSync = Date.now();
|
|
52
|
+
|
|
53
|
+
// Continue refining in background if we got partial results
|
|
54
|
+
if (samples.length < this.#sources.length) setTimeout(() => this.#backgroundRefine(), 100);
|
|
55
|
+
return this.time;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('[Clock] Sync failed:', error);
|
|
58
|
+
this.#offset = 0; // Fallback to local time
|
|
59
|
+
return this.time;
|
|
60
|
+
} finally { this.#syncing = false; }
|
|
61
|
+
}
|
|
62
|
+
get status() { // Get sync status info
|
|
63
|
+
if (this.mockMode) return { synchronized: true, syncing: false, offset: 0, lastSync: Date.now(), age: 0 };
|
|
64
|
+
return {
|
|
65
|
+
synchronized: this.#offset !== null,
|
|
66
|
+
syncing: this.#syncing,
|
|
67
|
+
offset: this.#offset,
|
|
68
|
+
lastSync: this.#lastSync,
|
|
69
|
+
age: this.#lastSync ? Date.now() - this.#lastSync : null
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// PRIVATE METHODS
|
|
74
|
+
async #fetchTimeSamples() { // Fetch time samples from all sources in parallel
|
|
75
|
+
const promises = this.#sources.map(source => this.#fetchTimeFromSource(source));
|
|
76
|
+
const results = await Promise.allSettled(promises);
|
|
77
|
+
const samples = [];
|
|
78
|
+
for (const result of results) if (result.status === 'fulfilled') samples.push(result.value);
|
|
79
|
+
return samples;
|
|
80
|
+
}
|
|
81
|
+
/** @param {'time.google.com' | 'time.cloudflare.com' | 'pool.ntp.org'} source */
|
|
82
|
+
async #fetchTimeFromSource(source) { // Fetch time from a single NTP source
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000); // 2s timeout
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const startTime = Date.now();
|
|
88
|
+
const response = await fetch(`https://${source}`, { method: 'HEAD', signal: controller.signal, cache: 'no-cache' });
|
|
89
|
+
const networkLatency = (Date.now() - startTime) / 2; // Rough RTT/2
|
|
90
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
91
|
+
|
|
92
|
+
const serverTime = new Date(response.headers.get('date')).getTime();
|
|
93
|
+
if (isNaN(serverTime)) throw new Error('Invalid date header');
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
source,
|
|
97
|
+
serverTime: serverTime + networkLatency, // Compensate for network delay
|
|
98
|
+
localTime: Date.now(),
|
|
99
|
+
latency: networkLatency * 2
|
|
100
|
+
};
|
|
101
|
+
} finally { clearTimeout(timeoutId); }
|
|
102
|
+
}
|
|
103
|
+
/** @param {Array<{serverTime: number, localTime: number, latency: number}>} samples */
|
|
104
|
+
#calculateOffset(samples) { // Calculate offset from multiple samples
|
|
105
|
+
if (samples.length === 1) return samples[0].serverTime - samples[0].localTime;
|
|
106
|
+
samples.sort((a, b) => a.latency - b.latency); // Sort by latency, prefer lower latency sources
|
|
107
|
+
|
|
108
|
+
const offsets = [];
|
|
109
|
+
for (const sample of samples) offsets.push(sample.serverTime - sample.localTime);
|
|
110
|
+
offsets.sort((a, b) => a - b); // Use median to filter outliers
|
|
111
|
+
const mid = Math.floor(offsets.length / 2);
|
|
112
|
+
|
|
113
|
+
if (offsets.length % 2 === 0) return (offsets[mid - 1] + offsets[mid]) / 2;
|
|
114
|
+
return offsets[mid];
|
|
115
|
+
}
|
|
116
|
+
async #backgroundRefine() { // Background refinement after initial sync
|
|
117
|
+
if (this.#syncing) return;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const samples = await this.#fetchTimeSamples();
|
|
121
|
+
if (samples.length === 0) return; // All failed
|
|
122
|
+
|
|
123
|
+
// Only update if change is significant (> 100ms)
|
|
124
|
+
const newOffset = this.#calculateOffset(samples);
|
|
125
|
+
if (Math.abs(newOffset - this.#offset) > 100) this.#offset = newOffset;
|
|
126
|
+
} catch (error) { if (this.verbose) console.warn('[Clock] Background refine failed:', error); }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
export async function CLOCK_TEST() { // DEBUG TEST WHILE RUNNING AS STANDALONE
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
const clock = new Clock();
|
|
134
|
+
clock.sync().then(() => {
|
|
135
|
+
console.log('Synchronized in: ', Date.now() - startTime, 'ms');
|
|
136
|
+
console.log('Synchronized time:', new Date(clock.time).toISOString());
|
|
137
|
+
console.log('Clock status:', clock.status);
|
|
138
|
+
}).catch(console.error);
|
|
139
|
+
|
|
140
|
+
while (true) {
|
|
141
|
+
console.log('Clock status:', clock.status);
|
|
142
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class Converter {
|
|
2
|
+
IS_BROWSER = typeof window !== 'undefined';
|
|
3
|
+
FROMBASE64_AVAILABLE = typeof Uint8Array.fromBase64 === 'function';
|
|
4
|
+
textEncoder = new TextEncoder();
|
|
5
|
+
textDecoder = new TextDecoder();
|
|
6
|
+
buffer2 = new ArrayBuffer(2); view2 = new DataView(this.buffer2);
|
|
7
|
+
buffer4 = new ArrayBuffer(4); view4 = new DataView(this.buffer4);
|
|
8
|
+
buffer8 = new ArrayBuffer(8); view8 = new DataView(this.buffer8);
|
|
9
|
+
hexMap = { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, 'F': 15, 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, 'f': 15 };
|
|
10
|
+
|
|
11
|
+
// ... TO BYTES
|
|
12
|
+
/** Number should be between 0 and 65535 @param {number} num - Integer to convert to 2 bytes Uint8Array */
|
|
13
|
+
numberTo2Bytes(num) { this.view2.setUint16(0, num, true); return new Uint8Array(this.buffer2); }
|
|
14
|
+
/** Number should be between 0 and 4294967295 @param {number} num - Integer to convert to 4 bytes Uint8Array */
|
|
15
|
+
numberTo4Bytes(num) { this.view4.setUint32(0, num, true); return new Uint8Array(this.buffer4); }
|
|
16
|
+
/** Number should be between 0 and 18446744073709551615 @param {number} num - Integer to convert to 8 bytes Uint8Array */
|
|
17
|
+
numberTo8Bytes(num) { this.view8.setBigUint64(0, BigInt(num), true); return new Uint8Array(this.buffer8); }
|
|
18
|
+
stringToBytes(str = 'toto') { return this.textEncoder.encode(str); }
|
|
19
|
+
/** @param {string} hex - Hex string to convert to Uint8Array */
|
|
20
|
+
hexToBytes(hex) {
|
|
21
|
+
const length = hex.length / 2;
|
|
22
|
+
const uint8Array = new Uint8Array(length);
|
|
23
|
+
for (let i = 0, j = 0; i < length; ++i, j += 2) uint8Array[i] = (this.hexMap[hex[j]] << 4) + this.hexMap[hex[j + 1]];
|
|
24
|
+
return uint8Array;
|
|
25
|
+
}
|
|
26
|
+
/** Base64 string to convert to Uint8Array @param {string} base64 @returns {Uint8Array} */
|
|
27
|
+
base64toBytes(base64) {
|
|
28
|
+
if (!this.IS_BROWSER) return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
29
|
+
if (this.FROMBASE64_AVAILABLE) return Uint8Array.fromBase64(base64);
|
|
30
|
+
const binaryString = atob(base64);
|
|
31
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
32
|
+
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
|
|
33
|
+
return bytes;
|
|
34
|
+
}
|
|
35
|
+
// BYTES TO ...
|
|
36
|
+
/** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
|
|
37
|
+
bytes2ToNumber(uint8Array) { for (let i = 0; i < 2; i++) this.view2.setUint8(i, uint8Array[i]); return this.view2.getUint16(0, true); }
|
|
38
|
+
/** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
|
|
39
|
+
bytes4ToNumber(uint8Array) { for (let i = 0; i < 4; i++) this.view4.setUint8(i, uint8Array[i]); return this.view4.getUint32(0, true); }
|
|
40
|
+
/** @param {Uint8Array} uint8Array - Uint8Array to convert to number */
|
|
41
|
+
bytes8ToNumber(uint8Array) { for (let i = 0; i < 8; i++) this.view8.setUint8(i, uint8Array[i]); return Number(this.view8.getBigUint64(0, true)); }
|
|
42
|
+
/** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
|
|
43
|
+
bytesToString(uint8Array) { return this.textDecoder.decode(uint8Array); }
|
|
44
|
+
/** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
|
|
45
|
+
static bytesToHex(uint8Array, minLength = 0) {
|
|
46
|
+
let hexStr = '';
|
|
47
|
+
for (const byte of uint8Array) hexStr += byte < 16 ? `0${byte.toString(16)}` : byte.toString(16);
|
|
48
|
+
if (minLength > 0) { hexStr = hexStr.padStart(minLength, '0'); }
|
|
49
|
+
return hexStr;
|
|
50
|
+
}
|
|
51
|
+
/** @param {Uint8Array} uint8Array - Uint8Array to convert to string */
|
|
52
|
+
bytesToHex(uint8Array, minLength = 0) { return Converter.bytesToHex(uint8Array, minLength); }
|
|
53
|
+
// OTHERS
|
|
54
|
+
/** @param {string} hex - Hex string to convert to bits @param {'string' | 'arrayOfString' | 'arrayOfNumbers'} format - Output format, default: string */
|
|
55
|
+
static hexToBits(hex = 'ffffff', format = 'string') {
|
|
56
|
+
let bitsString = []; // WE USE STRING METHODS FOR PERFORMANCE (number '0' = 8 Bytes | string '0' = 1 Byte)
|
|
57
|
+
if (format === 'arrayOfNumbers') for (let i = 0; i < hex.length; i++) bitsString.push(...(parseInt(hex[i], 16).toString(2).padStart(4, '0').split('').map(b => parseInt(b, 10))));
|
|
58
|
+
else for (let i = 0; i < hex.length; i++) bitsString.push(parseInt(hex[i], 16).toString(2).padStart(4, '0'));
|
|
59
|
+
return format === 'string' ? bitsString.join('') : bitsString;
|
|
60
|
+
}
|
|
61
|
+
/** @param {Uint8Array} uint8Array - Uint8Array to convert to bits @param {'string' | 'arrayOfString' | 'arrayOfNumbers'} format - Output format, default: string */
|
|
62
|
+
static bytesToBits(uint8Array, format = 'string') { return this.hexToBits(this.bytesToHex(uint8Array), format); }
|
|
63
|
+
static ipToInt(ip = '192.168.0.1') { return ip.split('.').reduce((int, oct) => (int << 8) + parseInt(oct, 10), 0); }
|
|
64
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Converter } from './converter.mjs';
|
|
2
|
+
const IS_BROWSER = typeof window !== 'undefined';
|
|
3
|
+
|
|
4
|
+
// ED25519 EXPOSURE NODEJS/BROWSER COMPATIBLE ---------------------------------
|
|
5
|
+
const [ed_, {sha512}] = await Promise.all([
|
|
6
|
+
import(IS_BROWSER ? 'https://unpkg.com/@noble/ed25519@3.0.0/index.js' : '@noble/ed25519'),
|
|
7
|
+
import(IS_BROWSER ? 'https://unpkg.com/@noble/hashes@2.0.0/sha2.js' : '@noble/hashes/sha2.js')
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
/** @type {import('@noble/ed25519')} */
|
|
11
|
+
const ed25519 = ed_;
|
|
12
|
+
ed25519.hashes.sha512 = sha512;
|
|
13
|
+
export { ed25519 };
|
|
14
|
+
//-----------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
// ARGON2 EXPOSURE NODEJS/BROWSER COMPATIBLE ----------------------------------
|
|
17
|
+
export class Argon2Unified {
|
|
18
|
+
converter = new Converter();
|
|
19
|
+
/** @type {import('argon2')} */ argon2;
|
|
20
|
+
|
|
21
|
+
/** This function hashes a password using Argon2 - Browser/NodeJS unified
|
|
22
|
+
* @param {string} pass - Password to hash
|
|
23
|
+
* @param {string} salt - Salt to use for the hash
|
|
24
|
+
* @param {number} [mem] - Memory usage in KiB, default: 2**16 = 65_536 (64 MiB) | RECOMMENDED: 2**16
|
|
25
|
+
* @param {number} [time] - Time cost in iterations, default: 1
|
|
26
|
+
* @param {number} [parallelism] - Number of threads to use, default: 1
|
|
27
|
+
* @param {number} [type] - 0: Argon2d, 1: Argon2i, 2: Argon2id, default: 2 (Argon2id)
|
|
28
|
+
* @param {number} [hashLen] - Length of the hash in bytes, default: 32 */
|
|
29
|
+
hash = async (pass, salt, mem = 2**16, time = 1, parallelism = 1, type = 2, hashLen = 32) => {
|
|
30
|
+
const params = this.#createArgon2Params(pass, salt, time, mem, parallelism, type, hashLen);
|
|
31
|
+
const argon2Lib = await this.getArgon2Lib();
|
|
32
|
+
const hashResult = IS_BROWSER ? await argon2Lib.hash(params) : await argon2Lib.hash(pass, params);
|
|
33
|
+
if (!hashResult) return false;
|
|
34
|
+
|
|
35
|
+
const encoded = hashResult.encoded || hashResult;
|
|
36
|
+
const result = this.#standardizeArgon2FromEncoded(encoded);
|
|
37
|
+
if (!result) return false;
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
async getArgon2Lib() {
|
|
42
|
+
if (this.argon2) return this.argon2;
|
|
43
|
+
|
|
44
|
+
if (!IS_BROWSER) { try {
|
|
45
|
+
const a = await import('argon2');
|
|
46
|
+
this.argon2 = a;
|
|
47
|
+
} catch (error) { throw new Error('Please install argon2 package: npm install argon2'); } }
|
|
48
|
+
if (this.argon2) return this.argon2;
|
|
49
|
+
|
|
50
|
+
try { if (argon2) {
|
|
51
|
+
console.log('Argon2 loaded as a global variable');
|
|
52
|
+
this.argon2 = argon2;
|
|
53
|
+
return this.argon2;
|
|
54
|
+
}} catch (error) { }
|
|
55
|
+
if (this.argon2) return this.argon2;
|
|
56
|
+
|
|
57
|
+
console.log('trying import argon2 ES6 and inject in window');
|
|
58
|
+
const argon2ES6 = await import('../libs/argon2-ES6.min.mjs');
|
|
59
|
+
window.argon2 = argon2ES6.default; // EXPOSE TO GLOBAL SCOPE
|
|
60
|
+
this.argon2 = argon2ES6.default;
|
|
61
|
+
return this.argon2;
|
|
62
|
+
};
|
|
63
|
+
#createArgon2Params(pass = "averylongpassword123456", salt = "saltsaltsaltsaltsalt", time = 1, mem = 2**10, parallelism = 1, type = 2, hashLen = 32) {
|
|
64
|
+
const fixedSalt = salt.padEnd(20, '0').substring(0, 16); // 16 bytes minimum
|
|
65
|
+
return {
|
|
66
|
+
type, pass, parallelism,
|
|
67
|
+
time, timeCost: time, // we preserve both for compatibility
|
|
68
|
+
mem, memoryCost: mem, // we preserve both for compatibility
|
|
69
|
+
hashLen, hashLength: hashLen, // we preserve both for compatibility
|
|
70
|
+
salt: IS_BROWSER ? fixedSalt : Buffer.from(fixedSalt),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
#standardizeArgon2FromEncoded(encoded = '$argon2id$v=19$m=1048576,t=1,p=1$c2FsdHNhbHRzYWx0c2FsdHNhbHQ$UamPN/XTTX4quPewQNw4/s3y1JJeS22cRroh5l7OTMM') {
|
|
74
|
+
const base64 = encoded.split('$').pop();
|
|
75
|
+
const hash = this.converter.base64toBytes(base64);
|
|
76
|
+
const hex = this.converter.bytesToHex(hash);
|
|
77
|
+
/** @type {string} */
|
|
78
|
+
const bitsString = Converter.hexToBits(hex, 'string');
|
|
79
|
+
if (!bitsString) return false;
|
|
80
|
+
return { encoded, hash, hex, bitsString };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//-----------------------------------------------------------------------------
|