Skip to content

Commit 818f88d

Browse files
committed
refactor: extract to64() util and improve sha2 algorithm
1 parent 002ca24 commit 818f88d

File tree

4 files changed

+98
-52
lines changed

4 files changed

+98
-52
lines changed

index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ const { timingSafeEqual } = require('crypto');
44
const { sha2Crypt } = require('./lib/sha2');
55
const { parseMagicSalt } = require('./lib/utils');
66

7+
/**
8+
* @param {*} password - The password string.
9+
* @param {*} magic - The salt string.
10+
*/
711
function crypt (password, magic) {
8-
const { algorithm, rounds, salt } = parseMagicSalt(magic);
12+
const { algorithm, prefix, rounds, salt } = parseMagicSalt(magic);
913
const result = [''];
1014

11-
result.push(algorithm === 'sha256' ? 5 : 6);
15+
result.push(prefix);
1216
if (magic.slice(1).split('$').length === 3 || rounds !== 5000) result.push(`rounds=${rounds}`);
1317
result.push(salt);
14-
result.push(sha2Crypt(password, { algorithm, rounds, salt }));
18+
result.push(sha2Crypt({ algorithm, password, rounds, salt }));
1519

1620
return result.join('$');
1721
}

lib/sha2.js

Lines changed: 9 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const { createHash } = require('crypto');
4-
const { DICTIONNARY } = require('./utils');
4+
const { to64 } = require('./utils');
55

66
// #1, #2, #3: byte number in group
77
const blocksOrder = {
@@ -45,15 +45,15 @@ const blocksOrder = {
4545
};
4646

4747
/**
48-
* Crypt compliant sha256 and sha512 hash generator
48+
* sha256 and sha512 checksum hash generator following the Modular Crypt Format
4949
*
50-
* @param {string} password - the password to encrypt
5150
* @param {object} options - options
51+
* @param {string} options.password - the password to encrypt
5252
* @param {'sha256'|'sha512'} options.algorithm
5353
* @param {number} options.rounds
5454
* @param {string} options.salt
5555
*/
56-
function sha2Crypt (password, { algorithm, rounds, salt }) {
56+
function sha2Crypt ({ algorithm, password, rounds, salt }) {
5757
if (
5858
!algorithm
5959
|| !(algorithm === 'sha256' || algorithm === 'sha512')
@@ -74,7 +74,7 @@ function sha2Crypt (password, { algorithm, rounds, salt }) {
7474
.update(salt);
7575

7676
// step 4
77-
const B = createHash(algorithm)
77+
const digestB = createHash(algorithm)
7878
// step 5
7979
.update(password)
8080
// step 6
@@ -86,11 +86,11 @@ function sha2Crypt (password, { algorithm, rounds, salt }) {
8686

8787
// step 9
8888
for (let offset = 0; offset + digestSize < passwordByteLength; offset += digestSize) {
89-
A.update(B);
89+
A.update(digestB);
9090
}
9191

9292
// step 10
93-
A.update(B.slice(0, passwordByteLength % digestSize));
93+
A.update(digestB.slice(0, passwordByteLength % digestSize));
9494

9595
// step 11
9696
passwordByteLength.toString(2)
@@ -100,7 +100,7 @@ function sha2Crypt (password, { algorithm, rounds, salt }) {
100100
A.update(
101101
bit !== '0'
102102
// step 11 - a
103-
? B
103+
? digestB
104104
// step 11 - b
105105
: password
106106
);
@@ -189,43 +189,7 @@ function sha2Crypt (password, { algorithm, rounds, salt }) {
189189
}, digestA);
190190

191191
// step 22
192-
const hash = [];
193-
194-
for (let index = 0; index < digestC.length; index += 3) {
195-
const buffer = Buffer.alloc(3);
196-
197-
buffer[0] = digestC[blocksOrder[algorithm][index]];
198-
buffer[1] = digestC[blocksOrder[algorithm][index + 1]];
199-
buffer[2] = digestC[blocksOrder[algorithm][index + 2]];
200-
201-
const b64Encode = [];
202-
203-
// 1st
204-
b64Encode.push(DICTIONNARY.charAt(
205-
buffer[0] & parseInt('00111111', 2)
206-
));
207-
208-
// 2nd
209-
b64Encode.push(DICTIONNARY.charAt(
210-
(buffer[0] & parseInt('11000000', 2)) >>> 6 | (buffer[1] & parseInt('00001111', 2)) << 2
211-
));
212-
213-
// 3rd
214-
b64Encode.push(DICTIONNARY.charAt(
215-
(buffer[1] & parseInt('11110000', 2)) >>> 4 | (buffer[2] & parseInt('00000011', 2)) << 4
216-
));
217-
218-
// 4th
219-
b64Encode.push(DICTIONNARY.charAt(
220-
(buffer[2] & parseInt('11111100', 2)) >>> 2
221-
));
222-
223-
hash.push(b64Encode.join(''));
224-
}
225-
226-
return hash
227-
.join('')
228-
.slice(0, digestC.length === 32 ? -1 : -2);
192+
return to64(digestC, blocksOrder[algorithm]).slice(0, digestC.length === 32 ? -1 : -2);
229193
}
230194

231195
module.exports = { sha2Crypt };

lib/utils.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ function randomCryptSalt (length) {
3535
return salt.join('');
3636
}
3737

38+
/**
39+
* Magic salt parser
40+
*
41+
* @param {string} magic
42+
* @returns {{ algorithm: string; prefix: number|string; rounds: number; salt: string}}
43+
*/
3844
function parseMagicSalt (magic) {
3945
const params = magic.slice(1).split('$');
4046

@@ -48,7 +54,9 @@ function parseMagicSalt (magic) {
4854
}
4955

5056
if (
57+
// SHA256-CRYPT
5158
prefix !== 5
59+
// SHA512-CRYPT
5260
&& prefix !== 6
5361
) {
5462
throw new Error('Only sha256-crypt and sha512-crypt algorithms are supported');
@@ -76,13 +84,67 @@ function parseMagicSalt (magic) {
7684

7785
return {
7886
algorithm,
87+
prefix,
7988
rounds,
8089
salt
8190
};
8291
}
8392

93+
/**
94+
* @param {Buffer} data
95+
* @param {Number[]} blocksOrder
96+
*/
97+
function to64 (data, blocksOrder) {
98+
if (!Buffer.isBuffer(data)) {
99+
throw new Error('data must be a buffer');
100+
}
101+
102+
if (!Array.isArray(blocksOrder)) {
103+
throw new Error('blocksOrder must be an array of integers');
104+
}
105+
106+
const hash64 = [];
107+
108+
for (let index = 0; index < data.length; index += 3) {
109+
const buffer = Buffer.alloc(3);
110+
111+
buffer[0] = data[blocksOrder[index]];
112+
buffer[1] = data[blocksOrder[index + 1]];
113+
buffer[2] = data[blocksOrder[index + 2]];
114+
115+
// 1st
116+
hash64.push(DICTIONNARY.charAt(
117+
// (base 16) 0x3f === (base 2) 00111111 === (base 10) 63
118+
buffer[0] & parseInt('0x3f')
119+
));
120+
121+
// 2nd
122+
hash64.push(DICTIONNARY.charAt(
123+
// (base 16) 0xc0 === (base 2) 11000000 === (base 10) 192
124+
// (base 16) 0xf === (base 2) 00001111 === (base 10) 15
125+
(buffer[0] & parseInt('0xc0')) >>> 6 | (buffer[1] & parseInt('0xf')) << 2
126+
));
127+
128+
// 3rd
129+
hash64.push(DICTIONNARY.charAt(
130+
// (base 16) 0xf0 === (base 2) 11110000 === (base 10) 240
131+
// (base 16) 0x3 === (base 2) 00000011 === (base 10) 3
132+
(buffer[1] & parseInt('0xf0')) >>> 4 | (buffer[2] & parseInt('0x3')) << 4
133+
));
134+
135+
// 4th
136+
hash64.push(DICTIONNARY.charAt(
137+
// (base 16) 0xfc === (base 2) 11111100 === (base 10) 252
138+
(buffer[2] & parseInt('0xfc')) >>> 2
139+
));
140+
}
141+
142+
return hash64.join('');
143+
}
144+
84145
module.exports = {
85146
DICTIONNARY,
147+
parseMagicSalt,
86148
randomCryptSalt,
87-
parseMagicSalt
149+
to64
88150
};

test/lib.test.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
const { test } = require('tap');
44
const { crypt, verify } = require('..');
55
const { sha2Crypt } = require('../lib/sha2');
6-
const { parseMagicSalt } = require('../lib/utils');
6+
const { parseMagicSalt, to64 } = require('../lib/utils');
77

88
// We conditionnally execute this test as it can take more than 4 hours to complete
99
if (!process.env.FAST_CRYPT === 'true') {
@@ -114,14 +114,14 @@ test('It should throw when trying to use an unsupported algorithm', async (t) =>
114114
}
115115
});
116116

117-
const sha2UnsupportedAlgorithms = [null, undefined, 'md5'];
117+
const sha2UnsupportedAlgorithms = [null, undefined, 'scrypt'];
118118

119119
for (const algorithm of sha2UnsupportedAlgorithms) {
120120
t.test(`inside the internal private sha2Crypt() method with the algorithm option set to '${algorithm}'`, async (t) => {
121121
t.plan(2);
122122

123123
try {
124-
sha2Crypt('password', { algorithm });
124+
sha2Crypt({ algorithm, password: 'password' });
125125
} catch (err) {
126126
t.ok(err);
127127
t.equal(err.message, `Unknown algorithm '${algorithm}', only sha256 and sha512 algorithms are supported`);
@@ -153,3 +153,19 @@ test('It should correctly parse the magic salt', async (t) => {
153153
t.equal(salt, 'roundstoohigh');
154154
});
155155
});
156+
157+
test('It should throw on bad `to64()` options :', async (t) => {
158+
t.test('when `data` is not a buffer', async (t) => {
159+
t.plan(1);
160+
161+
t.throws(() => to64('not a buffer', [0, 12]), 'data must be a buffer');
162+
});
163+
164+
t.test('when `blocksOrder` is not an array of integers', async (t) => {
165+
t.plan(1);
166+
167+
t.throws(
168+
() => to64(Buffer.alloc(16), 'not an array of integers'),
169+
'blocksOrder must be an array of integers');
170+
});
171+
});

0 commit comments

Comments
 (0)