@alvin_sudarta/primehash 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/package.json +35 -0
- package/primehash.js +418 -0
- package/primehash.min.js +19 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alvin_sudarta/primehash",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An original password hashing scheme integrating a Keccak-based sponge construction with an adaptive prime-number-based salting mechanism.",
|
|
5
|
+
"main": "primehash.js",
|
|
6
|
+
"browser": "primehash.min.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"primehash.js",
|
|
9
|
+
"primehash.min.js"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"password",
|
|
13
|
+
"hashing",
|
|
14
|
+
"keccak",
|
|
15
|
+
"sponge",
|
|
16
|
+
"salt",
|
|
17
|
+
"prime",
|
|
18
|
+
"cryptography",
|
|
19
|
+
"security",
|
|
20
|
+
"bcrypt",
|
|
21
|
+
"pbkdf",
|
|
22
|
+
"hmac",
|
|
23
|
+
"sha"
|
|
24
|
+
],
|
|
25
|
+
"author": "Alvin Sudarta <21.alvinsudarta@gmail.com>",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/AlvinSudarta/primehash.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/AlvinSudarta/primehash/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/AlvinSudarta/primehash#readme"
|
|
35
|
+
}
|
package/primehash.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* PrimeHash v1.0.0
|
|
3
|
+
*
|
|
4
|
+
* An original password hashing scheme that integrates a Keccak-based sponge
|
|
5
|
+
* construction with an adaptive prime-number-based salting mechanism.
|
|
6
|
+
*
|
|
7
|
+
* Author : Alvin Sudarta (NIM: 32220028)
|
|
8
|
+
* Faculty : Technology and Design – Informatics Study Program
|
|
9
|
+
* Inst. : Universitas Bunda Mulia, Jakarta
|
|
10
|
+
* Year : 2026
|
|
11
|
+
*
|
|
12
|
+
* Usage (browser / CDN):
|
|
13
|
+
* const hash = PrimeHash.hash("mypassword", 24, 32);
|
|
14
|
+
* const result = PrimeHash.verify("mypassword", 24, hash);
|
|
15
|
+
*
|
|
16
|
+
* Usage (Node.js / CommonJS):
|
|
17
|
+
* const PrimeHash = require('./primehash');
|
|
18
|
+
*/
|
|
19
|
+
(function (global, factory) {
|
|
20
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
21
|
+
module.exports = factory();
|
|
22
|
+
} else {
|
|
23
|
+
global.PrimeHash = factory();
|
|
24
|
+
}
|
|
25
|
+
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
/* =========================================================================
|
|
29
|
+
* CONSTANTS
|
|
30
|
+
* ========================================================================= */
|
|
31
|
+
|
|
32
|
+
// Keccak-p(1600, 24) Iota round constants, precomputed as little-endian byte arrays
|
|
33
|
+
const IOTA_BYTES = [
|
|
34
|
+
'0000000000000001', '0000000000008082', '800000000000808A',
|
|
35
|
+
'8000000080008000', '000000000000808B', '0000000080000001',
|
|
36
|
+
'8000000080008081', '8000000000008009', '000000000000008A',
|
|
37
|
+
'0000000000000088', '0000000080008009', '000000008000000A',
|
|
38
|
+
'000000008000808B', '800000000000008B', '8000000000008089',
|
|
39
|
+
'8000000000008003', '8000000000008002', '8000000000000080',
|
|
40
|
+
'000000000000800A', '800000008000000A', '8000000080008081',
|
|
41
|
+
'8000000000008080', '0000000080000001', '8000000080008008',
|
|
42
|
+
].map(function (h) {
|
|
43
|
+
var b = BigInt('0x' + h);
|
|
44
|
+
var bytes = new Uint8Array(8);
|
|
45
|
+
for (var z = 0; z < 8; z++) bytes[z] = Number((b >> BigInt(z * 8)) & 0xFFn);
|
|
46
|
+
return bytes;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Lane rotation offsets for Rho step (5×5 matrix indexed [x][y])
|
|
50
|
+
const RHO_SHIFTS = [
|
|
51
|
+
[0, 36, 3, 41, 18],
|
|
52
|
+
[1, 44, 10, 45, 2],
|
|
53
|
+
[62, 6, 43, 15, 61],
|
|
54
|
+
[28, 55, 25, 21, 56],
|
|
55
|
+
[27, 20, 39, 8, 14],
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/* =========================================================================
|
|
59
|
+
* PRIME NUMBER CACHE
|
|
60
|
+
* Sieve of Eratosthenes up to 200 000, computed once on first use.
|
|
61
|
+
* Covers all indices needed by primePosition() (max index ≈ 10001).
|
|
62
|
+
* ========================================================================= */
|
|
63
|
+
|
|
64
|
+
var _primes = null;
|
|
65
|
+
|
|
66
|
+
function ensurePrimes() {
|
|
67
|
+
if (_primes) return;
|
|
68
|
+
var limit = 200000;
|
|
69
|
+
var sieve = new Uint8Array(limit + 1).fill(1);
|
|
70
|
+
sieve[0] = sieve[1] = 0;
|
|
71
|
+
for (var i = 2; i * i <= limit; i++) {
|
|
72
|
+
if (sieve[i]) {
|
|
73
|
+
for (var j = i * i; j <= limit; j += i) sieve[j] = 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
_primes = [];
|
|
77
|
+
for (var k = 2; k <= limit; k++) {
|
|
78
|
+
if (sieve[k]) _primes.push(k);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function nthPrime(n) {
|
|
83
|
+
ensurePrimes();
|
|
84
|
+
return _primes[n - 1];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* =========================================================================
|
|
88
|
+
* KECCAK PERMUTATION – spongeF1600_partial
|
|
89
|
+
* Byte-level implementation of Theta, Rho+Pi, Chi, and Iota steps.
|
|
90
|
+
* Matches the PHP reference implementation exactly.
|
|
91
|
+
* ========================================================================= */
|
|
92
|
+
|
|
93
|
+
function spongeF1600_partial(state, rounds) {
|
|
94
|
+
// Decode flat 200-byte state into 5×5 lanes of 8 bytes each
|
|
95
|
+
var S = [];
|
|
96
|
+
for (var x = 0; x < 5; x++) {
|
|
97
|
+
S[x] = [];
|
|
98
|
+
for (var y = 0; y < 5; y++) {
|
|
99
|
+
var off = (y * 5 + x) * 8;
|
|
100
|
+
S[x][y] = state.slice(off, off + 8);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (var r = 0; r < rounds; r++) {
|
|
105
|
+
|
|
106
|
+
// ── Theta ──────────────────────────────────────────────────────────
|
|
107
|
+
var C = [], D = [];
|
|
108
|
+
for (var cx = 0; cx < 5; cx++) {
|
|
109
|
+
C[cx] = new Uint8Array(8);
|
|
110
|
+
D[cx] = new Uint8Array(8);
|
|
111
|
+
for (var cz = 0; cz < 8; cz++) {
|
|
112
|
+
C[cx][cz] = S[cx][0][cz] ^ S[cx][1][cz] ^ S[cx][2][cz]
|
|
113
|
+
^ S[cx][3][cz] ^ S[cx][4][cz];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (var dx = 0; dx < 5; dx++) {
|
|
117
|
+
var px = (dx + 4) % 5, nx = (dx + 1) % 5;
|
|
118
|
+
for (var dz = 0; dz < 8; dz++) {
|
|
119
|
+
var carry = (C[nx][dz] & 0x80) ? 1 : 0;
|
|
120
|
+
var rot = ((C[nx][dz] << 1) & 0xFF) | carry;
|
|
121
|
+
D[dx][dz] = C[px][dz] ^ rot;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (var tx = 0; tx < 5; tx++)
|
|
125
|
+
for (var ty = 0; ty < 5; ty++)
|
|
126
|
+
for (var tz = 0; tz < 8; tz++)
|
|
127
|
+
S[tx][ty][tz] ^= D[tx][tz];
|
|
128
|
+
|
|
129
|
+
// ── Rho + Pi ───────────────────────────────────────────────────────
|
|
130
|
+
var T = [];
|
|
131
|
+
for (var rx = 0; rx < 5; rx++) { T[rx] = []; for (var ry = 0; ry < 5; ry++) T[rx][ry] = new Uint8Array(8); }
|
|
132
|
+
for (var rpx = 0; rpx < 5; rpx++) {
|
|
133
|
+
for (var rpy = 0; rpy < 5; rpy++) {
|
|
134
|
+
var tnx = rpy;
|
|
135
|
+
var tny = (2 * rpx + 3 * rpy) % 5;
|
|
136
|
+
var shift = RHO_SHIFTS[rpx][rpy];
|
|
137
|
+
var bShift = (shift / 8) | 0;
|
|
138
|
+
var bBit = shift % 8;
|
|
139
|
+
for (var rz = 0; rz < 8; rz++) {
|
|
140
|
+
var src = (rz - bShift + 8) % 8;
|
|
141
|
+
var cur = S[rpx][rpy][src];
|
|
142
|
+
var nxt = S[rpx][rpy][(src + 1) % 8];
|
|
143
|
+
T[tnx][tny][rz] = bBit === 0 ? cur
|
|
144
|
+
: (((cur << bBit) & 0xFF) | ((nxt >> (8 - bBit)) & 0xFF));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Chi ────────────────────────────────────────────────────────────
|
|
150
|
+
var U = [];
|
|
151
|
+
for (var ux = 0; ux < 5; ux++) {
|
|
152
|
+
U[ux] = [];
|
|
153
|
+
for (var uy = 0; uy < 5; uy++) {
|
|
154
|
+
U[ux][uy] = new Uint8Array(8);
|
|
155
|
+
for (var uz = 0; uz < 8; uz++) {
|
|
156
|
+
U[ux][uy][uz] = (T[ux][uy][uz]
|
|
157
|
+
^ ((~T[(ux + 1) % 5][uy][uz]) & T[(ux + 2) % 5][uy][uz])) & 0xFF;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Iota ───────────────────────────────────────────────────────────
|
|
163
|
+
var rc = IOTA_BYTES[r];
|
|
164
|
+
for (var iz = 0; iz < 8; iz++) U[0][0][iz] ^= rc[iz];
|
|
165
|
+
|
|
166
|
+
S = U;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Re-encode back to flat 200-byte state
|
|
170
|
+
var result = new Uint8Array(200);
|
|
171
|
+
for (var ey = 0; ey < 5; ey++)
|
|
172
|
+
for (var ex = 0; ex < 5; ex++) {
|
|
173
|
+
var eoff = (ey * 5 + ex) * 8;
|
|
174
|
+
for (var ez = 0; ez < 8; ez++) result[eoff + ez] = S[ex][ey][ez];
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* =========================================================================
|
|
180
|
+
* SPONGE CONSTRUCTION
|
|
181
|
+
* rate=8 bits (1 byte), capacity=1592 bits, suffix=0x06 (SHA-3 domain).
|
|
182
|
+
* ========================================================================= */
|
|
183
|
+
|
|
184
|
+
function sponge(rate, capacity, inputBytes, suffix, outputLen, rounds) {
|
|
185
|
+
if (rate + capacity !== 1600 || rate % 8 !== 0)
|
|
186
|
+
throw new Error('Invalid rate/capacity combination');
|
|
187
|
+
|
|
188
|
+
var rateBytes = rate / 8;
|
|
189
|
+
var state = new Uint8Array(200);
|
|
190
|
+
var offset = 0;
|
|
191
|
+
var blockSize = 0;
|
|
192
|
+
|
|
193
|
+
// Absorb
|
|
194
|
+
while (offset < inputBytes.length) {
|
|
195
|
+
blockSize = Math.min(inputBytes.length - offset, rateBytes);
|
|
196
|
+
for (var i = 0; i < blockSize; i++) state[i] ^= inputBytes[offset + i];
|
|
197
|
+
offset += blockSize;
|
|
198
|
+
if (blockSize === rateBytes) {
|
|
199
|
+
state = spongeF1600_partial(state, rounds);
|
|
200
|
+
blockSize = 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Padding (multi-rate padding)
|
|
205
|
+
state[blockSize] ^= suffix;
|
|
206
|
+
if ((suffix & 0x80) !== 0 && blockSize === rateBytes - 1)
|
|
207
|
+
state = spongeF1600_partial(state, rounds);
|
|
208
|
+
state[rateBytes - 1] ^= 0x80;
|
|
209
|
+
state = spongeF1600_partial(state, rounds);
|
|
210
|
+
|
|
211
|
+
// Squeeze
|
|
212
|
+
var out = [];
|
|
213
|
+
while (outputLen > 0) {
|
|
214
|
+
blockSize = Math.min(outputLen, rateBytes);
|
|
215
|
+
for (var j = 0; j < blockSize; j++) out.push(state[j]);
|
|
216
|
+
outputLen -= blockSize;
|
|
217
|
+
if (outputLen > 0) state = spongeF1600_partial(state, rounds);
|
|
218
|
+
}
|
|
219
|
+
return new Uint8Array(out);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* =========================================================================
|
|
223
|
+
* UTILITY HELPERS
|
|
224
|
+
* ========================================================================= */
|
|
225
|
+
|
|
226
|
+
function bytesToBinStr(bytes) {
|
|
227
|
+
var s = '';
|
|
228
|
+
for (var i = 0; i < bytes.length; i++)
|
|
229
|
+
s += bytes[i].toString(2).padStart(8, '0');
|
|
230
|
+
return s;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function binStrToBytes(s) {
|
|
234
|
+
var pad = Math.ceil(s.length / 8) * 8;
|
|
235
|
+
s = s.padEnd(pad, '0');
|
|
236
|
+
var out = new Uint8Array(s.length / 8);
|
|
237
|
+
for (var i = 0; i < out.length; i++)
|
|
238
|
+
out[i] = parseInt(s.substring(i * 8, i * 8 + 8), 2);
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function bytesToHex(bytes) {
|
|
243
|
+
return Array.from(bytes).map(function (b) { return b.toString(16).padStart(2, '0'); }).join('');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hexToBytes(hex) {
|
|
247
|
+
var out = new Uint8Array(hex.length / 2);
|
|
248
|
+
for (var i = 0; i < out.length; i++)
|
|
249
|
+
out[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Generate a cryptographically random 64-bit salt as a binary string. */
|
|
254
|
+
function saltGenerator() {
|
|
255
|
+
var buf = new Uint8Array(8);
|
|
256
|
+
(typeof crypto !== 'undefined' ? crypto : require('crypto').webcrypto).getRandomValues(buf);
|
|
257
|
+
return bytesToBinStr(buf);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* =========================================================================
|
|
261
|
+
* PRIME-POSITION SALTING
|
|
262
|
+
* ========================================================================= */
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Derive 64 prime numbers deterministically from the password's ASCII values.
|
|
266
|
+
* Each prime determines one bit-insertion position for the salt.
|
|
267
|
+
*/
|
|
268
|
+
function primePosition(password) {
|
|
269
|
+
var vals = [];
|
|
270
|
+
for (var i = 0; i < password.length; i++) vals.push(password.charCodeAt(i) & 0xFF);
|
|
271
|
+
var cur = vals.reduce(function (a, b) { return a + b; }, 0);
|
|
272
|
+
var primes = [];
|
|
273
|
+
for (var k = 0; k < 64; k++) {
|
|
274
|
+
cur += vals[k % vals.length];
|
|
275
|
+
primes.push(nthPrime((cur % 10000) + 2));
|
|
276
|
+
}
|
|
277
|
+
return primes;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Insert 64 salt bits into `data` at positions determined by `primePos`.
|
|
282
|
+
* Positions are computed without sorting, matching the PHP reference exactly.
|
|
283
|
+
*
|
|
284
|
+
* @param {string|Uint8Array} data
|
|
285
|
+
* @param {string} salt – 64-character binary string ('0'/'1')
|
|
286
|
+
* @param {number[]} primePos – array of 64 prime numbers
|
|
287
|
+
* @returns {Uint8Array}
|
|
288
|
+
*/
|
|
289
|
+
function salting(data, salt, primePos) {
|
|
290
|
+
var bytes;
|
|
291
|
+
if (typeof data === 'string') {
|
|
292
|
+
bytes = new Uint8Array(data.length);
|
|
293
|
+
for (var i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i) & 0xFF;
|
|
294
|
+
} else {
|
|
295
|
+
bytes = data;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var binData = bytesToBinStr(bytes);
|
|
299
|
+
var totalBits = binData.length + 64;
|
|
300
|
+
var saltPos = [];
|
|
301
|
+
|
|
302
|
+
for (var k = 0; k < 64; k++) {
|
|
303
|
+
var pos = primePos[k] % totalBits;
|
|
304
|
+
while (saltPos.includes(pos)) pos = (pos + 1) % totalBits;
|
|
305
|
+
saltPos.push(pos);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Sequential insertion (no pre-sorting) – each insertion shifts subsequent positions
|
|
309
|
+
var result = binData;
|
|
310
|
+
for (var n = 0; n < 64; n++) {
|
|
311
|
+
var p = saltPos[n] + n;
|
|
312
|
+
result = result.slice(0, p) + salt[n] + result.slice(p);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return binStrToBytes(result);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* =========================================================================
|
|
319
|
+
* PUBLIC API
|
|
320
|
+
* ========================================================================= */
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Hash a password using the PrimeHash algorithm.
|
|
324
|
+
*
|
|
325
|
+
* @param {string} password – Plaintext password (non-empty)
|
|
326
|
+
* @param {number} round – Keccak rounds, 1–24 (recommended: 24)
|
|
327
|
+
* @param {number} length – Output length in bytes, 24–99 (recommended: 32)
|
|
328
|
+
* @returns {string} Hexadecimal hash string (length * 2 hex characters)
|
|
329
|
+
*/
|
|
330
|
+
function hash(password, round, length) {
|
|
331
|
+
if (!password) throw new Error('Password cannot be empty');
|
|
332
|
+
if (length < 24 || length > 99) throw new Error('Length must be between 24 and 99');
|
|
333
|
+
if (round < 1 || round > 24) throw new Error('Round must be between 1 and 24');
|
|
334
|
+
|
|
335
|
+
var salt = saltGenerator();
|
|
336
|
+
var positions = primePosition(password);
|
|
337
|
+
var saltedPw = salting(password, salt, positions);
|
|
338
|
+
var hashed = sponge(8, 1592, saltedPw, 0x06, length - 8, round);
|
|
339
|
+
var withSalt = salting(hashed, salt, positions);
|
|
340
|
+
return bytesToHex(withSalt);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Verify a plaintext password against a stored PrimeHash.
|
|
345
|
+
*
|
|
346
|
+
* @param {string} password – Plaintext password to verify
|
|
347
|
+
* @param {number} round – Keccak rounds used during hashing
|
|
348
|
+
* @param {string} hashedPassword – Stored hex hash to verify against
|
|
349
|
+
* @returns {{ valid: boolean, updateHash: string|null }}
|
|
350
|
+
* `updateHash` is a fresh hash (for hash rotation) when `valid` is true.
|
|
351
|
+
*/
|
|
352
|
+
function verify(password, round, hashedPassword) {
|
|
353
|
+
var lenHex = hashedPassword.length;
|
|
354
|
+
if (lenHex % 2 !== 0) throw new Error('HashedPassword must have even hex length');
|
|
355
|
+
var lenBytes = lenHex / 2;
|
|
356
|
+
if (lenBytes < 24 || lenBytes > 99) throw new Error('HashedPassword length out of range');
|
|
357
|
+
|
|
358
|
+
var lenPwHex = lenHex - 16; // 16 hex chars = 8 bytes = 64 bits of stored salt
|
|
359
|
+
var lenPwBit = lenPwHex * 4;
|
|
360
|
+
var lenPwByte = (lenPwBit / 8) | 0;
|
|
361
|
+
|
|
362
|
+
var positions = primePosition(password);
|
|
363
|
+
|
|
364
|
+
// Build 64 unique marker characters (A–Z, a–z, 0–9, +, /)
|
|
365
|
+
var markers = [];
|
|
366
|
+
for (var i = 0; i < 26; i++) markers.push(String.fromCharCode(65 + i));
|
|
367
|
+
for (var i = 0; i < 26; i++) markers.push(String.fromCharCode(97 + i));
|
|
368
|
+
for (var i = 0; i < 10; i++) markers.push(String.fromCharCode(48 + i));
|
|
369
|
+
markers.push('+', '/');
|
|
370
|
+
|
|
371
|
+
var totalBits = lenPwBit + 64;
|
|
372
|
+
var saltPos = [];
|
|
373
|
+
var markerAtPos = {};
|
|
374
|
+
|
|
375
|
+
for (var k = 0; k < 64; k++) {
|
|
376
|
+
var pos = positions[k] % totalBits;
|
|
377
|
+
while (saltPos.includes(pos)) pos = (pos + 1) % totalBits;
|
|
378
|
+
saltPos.push(pos);
|
|
379
|
+
markerAtPos[pos] = markers[k];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Reconstruct the marked bitstream to locate where salt bits were inserted
|
|
383
|
+
var marked = '='.repeat(lenPwBit).split('');
|
|
384
|
+
for (var m = 0; m < 64; m++) {
|
|
385
|
+
marked.splice(saltPos[m] + m, 0, markerAtPos[saltPos[m]]);
|
|
386
|
+
}
|
|
387
|
+
var markedStr = marked.join('');
|
|
388
|
+
|
|
389
|
+
// Map each marker index → its position in the stored hash binary string
|
|
390
|
+
var hashBin = bytesToBinStr(hexToBytes(hashedPassword));
|
|
391
|
+
var saltMap = {};
|
|
392
|
+
for (var j = 0; j < markedStr.length; j++) {
|
|
393
|
+
var idx = markers.indexOf(markedStr[j]);
|
|
394
|
+
if (idx !== -1) saltMap[idx] = j;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Extract the 64 salt bits in original order
|
|
398
|
+
var saltBits = new Array(64).fill('0');
|
|
399
|
+
for (var key in saltMap) {
|
|
400
|
+
var bitPos = saltMap[key];
|
|
401
|
+
if (bitPos < hashBin.length) saltBits[parseInt(key)] = hashBin[bitPos];
|
|
402
|
+
}
|
|
403
|
+
var salt = saltBits.join('');
|
|
404
|
+
|
|
405
|
+
// Recompute hash with the extracted salt and compare
|
|
406
|
+
var saltedPw = salting(password, salt, positions);
|
|
407
|
+
var reHash = sponge(8, 1592, saltedPw, 0x06, lenPwByte, round);
|
|
408
|
+
var reWithSalt = salting(reHash, salt, positions);
|
|
409
|
+
var reHex = bytesToHex(reWithSalt);
|
|
410
|
+
|
|
411
|
+
var valid = reHex === hashedPassword;
|
|
412
|
+
var updateHash = valid ? hash(password, round, lenBytes) : null;
|
|
413
|
+
|
|
414
|
+
return { valid: valid, updateHash: updateHash };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return { hash: hash, verify: verify };
|
|
418
|
+
}));
|
package/primehash.min.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*! PrimeHash v1.0.0 | (c) 2026 Alvin Sudarta (NIM:32220028) | Universitas Bunda Mulia Jakarta */
|
|
2
|
+
(function(g,f){typeof module!='undefined'&&module.exports?module.exports=f():g.PrimeHash=f()}(typeof globalThis!='undefined'?globalThis:this,function(){'use strict';
|
|
3
|
+
const IB=['0000000000000001','0000000000008082','800000000000808A','8000000080008000','000000000000808B','0000000080000001','8000000080008081','8000000000008009','000000000000008A','0000000000000088','0000000080008009','000000008000000A','000000008000808B','800000000000008B','8000000000008089','8000000000008003','8000000000008002','8000000000000080','000000000000800A','800000008000000A','8000000080008081','8000000000008080','0000000080000001','8000000080008008'].map(function(h){var b=BigInt('0x'+h),a=new Uint8Array(8);for(var z=0;z<8;z++)a[z]=Number((b>>BigInt(z*8))&0xFFn);return a});
|
|
4
|
+
const RS=[[0,36,3,41,18],[1,44,10,45,2],[62,6,43,15,61],[28,55,25,21,56],[27,20,39,8,14]];
|
|
5
|
+
var _P=null;
|
|
6
|
+
function _ep(){if(_P)return;var L=200000,sv=new Uint8Array(L+1).fill(1);sv[0]=sv[1]=0;for(var i=2;i*i<=L;i++)if(sv[i])for(var j=i*i;j<=L;j+=i)sv[j]=0;_P=[];for(var k=2;k<=L;k++)if(sv[k])_P.push(k)}
|
|
7
|
+
function _np(n){_ep();return _P[n-1]}
|
|
8
|
+
function _perm(st,rds){var S=[];for(var x=0;x<5;x++){S[x]=[];for(var y=0;y<5;y++){var o=(y*5+x)*8;S[x][y]=st.slice(o,o+8)}}for(var r=0;r<rds;r++){var C=[],D=[];for(var cx=0;cx<5;cx++){C[cx]=new Uint8Array(8);D[cx]=new Uint8Array(8);for(var cz=0;cz<8;cz++)C[cx][cz]=S[cx][0][cz]^S[cx][1][cz]^S[cx][2][cz]^S[cx][3][cz]^S[cx][4][cz]}for(var dx=0;dx<5;dx++){var pxv=(dx+4)%5,nxv=(dx+1)%5;for(var dz=0;dz<8;dz++){var ca=(C[nxv][dz]&0x80)?1:0,rv=((C[nxv][dz]<<1)&0xFF)|ca;D[dx][dz]=C[pxv][dz]^rv}}for(var tx=0;tx<5;tx++)for(var ty=0;ty<5;ty++)for(var tz=0;tz<8;tz++)S[tx][ty][tz]^=D[tx][tz];var T=[];for(var rx=0;rx<5;rx++){T[rx]=[];for(var ry=0;ry<5;ry++)T[rx][ry]=new Uint8Array(8)}for(var rpx=0;rpx<5;rpx++)for(var rpy=0;rpy<5;rpy++){var tnx=rpy,tny=(2*rpx+3*rpy)%5,sh=RS[rpx][rpy],bsf=(sh/8)|0,bb=sh%8;for(var rz=0;rz<8;rz++){var src=(rz-bsf+8)%8,cu=S[rpx][rpy][src],nxt=S[rpx][rpy][(src+1)%8];T[tnx][tny][rz]=bb===0?cu:(((cu<<bb)&0xFF)|((nxt>>(8-bb))&0xFF))}}var U=[];for(var ux=0;ux<5;ux++){U[ux]=[];for(var uy=0;uy<5;uy++){U[ux][uy]=new Uint8Array(8);for(var uz=0;uz<8;uz++)U[ux][uy][uz]=(T[ux][uy][uz]^((~T[(ux+1)%5][uy][uz])&T[(ux+2)%5][uy][uz]))&0xFF}}var rc=IB[r];for(var iz=0;iz<8;iz++)U[0][0][iz]^=rc[iz];S=U}var res=new Uint8Array(200);for(var ey=0;ey<5;ey++)for(var ex=0;ex<5;ex++){var eo=(ey*5+ex)*8;for(var ez=0;ez<8;ez++)res[eo+ez]=S[ex][ey][ez]}return res}
|
|
9
|
+
function _spng(ra,ca,inp,su,ol,rds){if(ra+ca!==1600||ra%8!==0)throw new Error('Invalid rate/capacity');var rb=ra/8,st=new Uint8Array(200),off=0,bsz=0;while(off<inp.length){bsz=Math.min(inp.length-off,rb);for(var i=0;i<bsz;i++)st[i]^=inp[off+i];off+=bsz;if(bsz===rb){st=_perm(st,rds);bsz=0}}st[bsz]^=su;if((su&0x80)!==0&&bsz===rb-1)st=_perm(st,rds);st[rb-1]^=0x80;st=_perm(st,rds);var out=[];while(ol>0){bsz=Math.min(ol,rb);for(var j=0;j<bsz;j++)out.push(st[j]);ol-=bsz;if(ol>0)st=_perm(st,rds)}return new Uint8Array(out)}
|
|
10
|
+
function _b2b(b){var s='';for(var i=0;i<b.length;i++)s+=b[i].toString(2).padStart(8,'0');return s}
|
|
11
|
+
function _s2b(s){var p=Math.ceil(s.length/8)*8;s=s.padEnd(p,'0');var o=new Uint8Array(s.length/8);for(var i=0;i<o.length;i++)o[i]=parseInt(s.substring(i*8,i*8+8),2);return o}
|
|
12
|
+
function _b2h(b){return Array.from(b).map(function(v){return v.toString(16).padStart(2,'0')}).join('')}
|
|
13
|
+
function _h2b(h){var o=new Uint8Array(h.length/2);for(var i=0;i<o.length;i++)o[i]=parseInt(h.substring(i*2,i*2+2),16);return o}
|
|
14
|
+
function _sg(){var b=new Uint8Array(8);(typeof crypto!='undefined'?crypto:require('crypto').webcrypto).getRandomValues(b);return _b2b(b)}
|
|
15
|
+
function _pp(pw){var v=[];for(var i=0;i<pw.length;i++)v.push(pw.charCodeAt(i)&0xFF);var c=v.reduce(function(a,b){return a+b},0),r=[];for(var k=0;k<64;k++){c+=v[k%v.length];r.push(_np((c%10000)+2))}return r}
|
|
16
|
+
function _ins(d,sl,ppos){var b;if(typeof d==='string'){b=new Uint8Array(d.length);for(var i=0;i<d.length;i++)b[i]=d.charCodeAt(i)&0xFF}else b=d;var bd=_b2b(b),tb=bd.length+64,sp=[];for(var k=0;k<64;k++){var pos=ppos[k]%tb;while(sp.includes(pos))pos=(pos+1)%tb;sp.push(pos)}var res=bd;for(var n=0;n<64;n++){var p=sp[n]+n;res=res.slice(0,p)+sl[n]+res.slice(p)}return _s2b(res)}
|
|
17
|
+
function hash(pw,rds,le){if(!pw)throw new Error('Password cannot be empty');if(le<24||le>99)throw new Error('Length must be between 24 and 99');if(rds<1||rds>24)throw new Error('Round must be between 1 and 24');var sl=_sg(),pos=_pp(pw),sp=_ins(pw,sl,pos),h=_spng(8,1592,sp,0x06,le-8,rds),ws=_ins(h,sl,pos);return _b2h(ws)}
|
|
18
|
+
function verify(pw,rds,hp){var lh=hp.length;if(lh%2!==0)throw new Error('HashedPassword must have even hex length');var lb=lh/2;if(lb<24||lb>99)throw new Error('HashedPassword length out of range');var lph=lh-16,lpb=lph*4,lpby=(lpb/8)|0,pos=_pp(pw),mk=[];for(var i=0;i<26;i++)mk.push(String.fromCharCode(65+i));for(var i=0;i<26;i++)mk.push(String.fromCharCode(97+i));for(var i=0;i<10;i++)mk.push(String.fromCharCode(48+i));mk.push('+','/');var tb=lpb+64,sp=[],mpp={};for(var k=0;k<64;k++){var p=pos[k]%tb;while(sp.includes(p))p=(p+1)%tb;sp.push(p);mpp[p]=mk[k]}var ma='='.repeat(lpb).split('');for(var m=0;m<64;m++)ma.splice(sp[m]+m,0,mpp[sp[m]]);var ms=ma.join(''),hb=_b2b(_h2b(hp)),sm={};for(var j=0;j<ms.length;j++){var ix=mk.indexOf(ms[j]);if(ix!==-1)sm[ix]=j}var sb=new Array(64).fill('0');for(var key in sm){var bp=sm[key];if(bp<hb.length)sb[parseInt(key)]=hb[bp]}var sl2=sb.join(''),ssp=_ins(pw,sl2,pos),rh=_spng(8,1592,ssp,0x06,lpby,rds),rws=_ins(rh,sl2,pos),rhx=_b2h(rws),v=rhx===hp;return{valid:v,updateHash:v?hash(pw,rds,lb):null}}
|
|
19
|
+
return{hash:hash,verify:verify}}));
|