Skip to content

Commit 93d0997

Browse files
committed
Replace SJCL-powered SHA-256 HMAC in crypto-sha256-js with a handrolled one inspired by it
1 parent b798cec commit 93d0997

File tree

11 files changed

+695
-65
lines changed

11 files changed

+695
-65
lines changed

packages/crypto-sha256-js/package.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@
1212
"author": "[email protected]",
1313
"license": "Apache-2.0",
1414
"dependencies": {
15-
"@aws/crypto-sjcl-bitArray": "^0.0.1",
16-
"@aws/crypto-sjcl-codecArrayBuffer": "^0.0.1",
17-
"@aws/crypto-sjcl-codecString": "^0.0.1",
18-
"@aws/crypto-sjcl-hmac": "^0.0.1",
19-
"@aws/crypto-sjcl-sha256": "^0.0.1",
20-
"@aws/types": "^0.0.1"
15+
"@aws/types": "^0.0.1",
16+
"@aws/util-utf8-browser": "^0.0.1"
2117
},
2218
"devDependencies": {
19+
"@aws/util-hex-encoding": "^0.0.1",
2320
"@types/jest": "^20.0.2",
2421
"jest": "^20.0.4",
2522
"typescript": "^2.3"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {hashTestVectors} from './knownHashes.fixture';
2+
import {RawSha256} from './RawSha256';
3+
4+
describe('Hash', () => {
5+
let idx = 0;
6+
for (const [input, result] of hashTestVectors) {
7+
it('should match known hash calculations: ' + idx++, () => {
8+
const hash = new RawSha256;
9+
hash.update(input);
10+
expect(hash.digest()).toEqual(result);
11+
});
12+
}
13+
});
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import {
2+
BLOCK_SIZE,
3+
DIGEST_LENGTH,
4+
INIT,
5+
KEY,
6+
MAX_HASHABLE_LENGTH,
7+
} from './constants';
8+
9+
/**
10+
* @internal
11+
*/
12+
export class RawSha256 {
13+
private state: Int32Array = Int32Array.from(INIT);
14+
private temp: Int32Array = new Int32Array(64);
15+
private buffer: Uint8Array = new Uint8Array(64);
16+
private bufferLength: number = 0;
17+
private bytesHashed: number = 0;
18+
19+
/**
20+
* @internal
21+
*/
22+
finished: boolean = false;
23+
24+
update(data: Uint8Array): void {
25+
if (this.finished) {
26+
throw new Error("Attempted to update an already finished hash.");
27+
}
28+
29+
let position = 0;
30+
let {byteLength} = data;
31+
this.bytesHashed += byteLength;
32+
33+
if (this.bytesHashed * 8 > MAX_HASHABLE_LENGTH) {
34+
throw new Error("Cannot hash more than 2^53 - 1 bits");
35+
}
36+
37+
while (byteLength > 0) {
38+
this.buffer[this.bufferLength++] = data[position++];
39+
byteLength--;
40+
41+
if (this.bufferLength === BLOCK_SIZE) {
42+
this.hashBuffer();
43+
this.bufferLength = 0;
44+
}
45+
}
46+
}
47+
48+
digest(): Uint8Array {
49+
if (!this.finished) {
50+
const bitsHashed = this.bytesHashed * 8;
51+
const bufferView = new DataView(
52+
this.buffer.buffer,
53+
this.buffer.byteOffset,
54+
this.buffer.byteLength
55+
);
56+
57+
const undecoratedLength = this.bufferLength;
58+
bufferView.setUint8(this.bufferLength++, 0x80);
59+
60+
// Ensure the final block has enough room for the hashed length
61+
if (undecoratedLength % BLOCK_SIZE >= BLOCK_SIZE - 8) {
62+
for (let i = this.bufferLength; i < BLOCK_SIZE; i++) {
63+
bufferView.setUint8(i, 0);
64+
}
65+
this.hashBuffer();
66+
this.bufferLength = 0;
67+
}
68+
69+
for (let i = this.bufferLength; i < BLOCK_SIZE - 8; i++) {
70+
bufferView.setUint8(i, 0);
71+
}
72+
bufferView.setUint32(
73+
BLOCK_SIZE - 8,
74+
Math.floor(bitsHashed / 0x100000000),
75+
true
76+
);
77+
bufferView.setUint32(
78+
BLOCK_SIZE - 4,
79+
bitsHashed
80+
);
81+
82+
this.hashBuffer();
83+
84+
this.finished = true;
85+
}
86+
87+
// The value in state is little-endian rather than big-endian, so flip
88+
// each word into a new Uint8Array
89+
const out = new Uint8Array(DIGEST_LENGTH);
90+
for (let i = 0; i < 8; i++) {
91+
out[i * 4] = (this.state[i] >>> 24) & 0xff;
92+
out[i * 4 + 1] = (this.state[i] >>> 16) & 0xff;
93+
out[i * 4 + 2] = (this.state[i] >>> 8) & 0xff;
94+
out[i * 4 + 3] = (this.state[i] >>> 0) & 0xff;
95+
}
96+
97+
return out;
98+
}
99+
100+
private hashBuffer(): void {
101+
const {buffer, state} = this;
102+
103+
let state0 = state[0],
104+
state1 = state[1],
105+
state2 = state[2],
106+
state3 = state[3],
107+
state4 = state[4],
108+
state5 = state[5],
109+
state6 = state[6],
110+
state7 = state[7];
111+
112+
for (let i = 0; i < BLOCK_SIZE; i++) {
113+
if (i < 16) {
114+
this.temp[i] = (
115+
((buffer[i * 4] & 0xff) << 24) |
116+
((buffer[(i * 4) + 1] & 0xff) << 16) |
117+
((buffer[(i * 4) + 2] & 0xff) << 8) |
118+
(buffer[(i * 4) + 3] & 0xff)
119+
);
120+
} else {
121+
let u = this.temp[i - 2];
122+
const t1 = (u >>> 17 | u << 15) ^
123+
(u >>> 19 | u << 13) ^
124+
(u >>> 10);
125+
126+
u = this.temp[i - 15];
127+
const t2 = (u >>> 7 | u << 25) ^
128+
(u >>> 18 | u << 14) ^
129+
(u >>> 3);
130+
131+
this.temp[i] = (t1 + this.temp[i - 7] | 0) +
132+
(t2 + this.temp[i - 16] | 0);
133+
}
134+
135+
const t1 = (
136+
(
137+
(
138+
(
139+
(state4 >>> 6 | state4 << 26) ^
140+
(state4 >>> 11 | state4 << 21) ^
141+
(state4 >>> 25 | state4 << 7)
142+
)
143+
+ ((state4 & state5) ^ (~state4 & state6))
144+
) | 0
145+
)
146+
+ ((state7 + ((KEY[i] + this.temp[i]) | 0)) | 0)
147+
) | 0;
148+
149+
const t2 = (
150+
(
151+
(state0 >>> 2 | state0 << 30) ^
152+
(state0 >>> 13 | state0 << 19) ^
153+
(state0 >>> 22 | state0 << 10)
154+
) + ((state0 & state1) ^ (state0 & state2) ^ (state1 & state2))
155+
) | 0;
156+
157+
state7 = state6;
158+
state6 = state5;
159+
state5 = state4;
160+
state4 = (state3 + t1) | 0;
161+
state3 = state2;
162+
state2 = state1;
163+
state1 = state0;
164+
state0 = (t1 + t2) | 0;
165+
}
166+
167+
state[0] += state0;
168+
state[1] += state1;
169+
state[2] += state2;
170+
state[3] += state3;
171+
state[4] += state4;
172+
state[5] += state5;
173+
state[6] += state6;
174+
state[7] += state7;
175+
}
176+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* @internal
3+
*/
4+
export const BLOCK_SIZE: number = 64;
5+
6+
/**
7+
* @internal
8+
*/
9+
export const DIGEST_LENGTH: number = 32;
10+
11+
/**
12+
* @internal
13+
*/
14+
export const KEY = new Uint32Array([
15+
0x428a2f98,
16+
0x71374491,
17+
0xb5c0fbcf,
18+
0xe9b5dba5,
19+
0x3956c25b,
20+
0x59f111f1,
21+
0x923f82a4,
22+
0xab1c5ed5,
23+
0xd807aa98,
24+
0x12835b01,
25+
0x243185be,
26+
0x550c7dc3,
27+
0x72be5d74,
28+
0x80deb1fe,
29+
0x9bdc06a7,
30+
0xc19bf174,
31+
0xe49b69c1,
32+
0xefbe4786,
33+
0x0fc19dc6,
34+
0x240ca1cc,
35+
0x2de92c6f,
36+
0x4a7484aa,
37+
0x5cb0a9dc,
38+
0x76f988da,
39+
0x983e5152,
40+
0xa831c66d,
41+
0xb00327c8,
42+
0xbf597fc7,
43+
0xc6e00bf3,
44+
0xd5a79147,
45+
0x06ca6351,
46+
0x14292967,
47+
0x27b70a85,
48+
0x2e1b2138,
49+
0x4d2c6dfc,
50+
0x53380d13,
51+
0x650a7354,
52+
0x766a0abb,
53+
0x81c2c92e,
54+
0x92722c85,
55+
0xa2bfe8a1,
56+
0xa81a664b,
57+
0xc24b8b70,
58+
0xc76c51a3,
59+
0xd192e819,
60+
0xd6990624,
61+
0xf40e3585,
62+
0x106aa070,
63+
0x19a4c116,
64+
0x1e376c08,
65+
0x2748774c,
66+
0x34b0bcb5,
67+
0x391c0cb3,
68+
0x4ed8aa4a,
69+
0x5b9cca4f,
70+
0x682e6ff3,
71+
0x748f82ee,
72+
0x78a5636f,
73+
0x84c87814,
74+
0x8cc70208,
75+
0x90befffa,
76+
0xa4506ceb,
77+
0xbef9a3f7,
78+
0xc67178f2
79+
]);
80+
81+
/**
82+
* @internal
83+
*/
84+
export const INIT = [
85+
0x6a09e667,
86+
0xbb67ae85,
87+
0x3c6ef372,
88+
0xa54ff53a,
89+
0x510e527f,
90+
0x9b05688c,
91+
0x1f83d9ab,
92+
0x5be0cd19,
93+
];
94+
95+
/**
96+
* @internal
97+
*/
98+
export const MAX_HASHABLE_LENGTH = 2 ** 53 - 1;

packages/crypto-sha256-js/src/jsSha256.spec.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
import {Sha256} from './jsSha256';
2-
import SjclSha256 = require('@aws/crypto-sjcl-sha256');
3-
import SjclHmac = require('@aws/crypto-sjcl-hmac');
2+
import {RawSha256} from './RawSha256';
3+
import {hashTestVectors, hmacTestVectors} from './knownHashes.fixture';
44

55
describe('Sha256', () => {
6-
it('should create an instance of SjclSha256 by default', () => {
6+
it('should create an instance of RawSha256 by default', () => {
77
const sha256 = new Sha256();
8-
expect((sha256 as any).hash).toBeInstanceOf(SjclSha256);
8+
expect((sha256 as any).hash).toBeInstanceOf(RawSha256);
99
});
1010

11-
it('should create an instance of SjclHmac if a secret is present', () => {
11+
it('should create an outer hash if a secret is present', () => {
1212
const sha256 = new Sha256('foo');
13-
expect((sha256 as any).hash).toBeInstanceOf(SjclHmac);
13+
expect((sha256 as any).hash).toBeInstanceOf(RawSha256);
14+
expect((sha256 as any).outer).toBeInstanceOf(RawSha256);
1415
});
1516

1617
it('should accept ArrayBufferView secrets', () => {
1718
const sha256 = new Sha256(Uint8Array.from([0xde, 0xad]));
18-
expect((sha256 as any).hash).toBeInstanceOf(SjclHmac);
1919
});
2020

2121
it('should accept ArrayBuffer secrets', () => {
2222
const sha256 = new Sha256(Uint8Array.from([0xde, 0xad]).buffer);
23-
expect((sha256 as any).hash).toBeInstanceOf(SjclHmac);
2423
});
2524

2625
it('should call update when given data', () => {
@@ -54,25 +53,9 @@ describe('Sha256', () => {
5453
);
5554
});
5655

57-
it('should call finalize when creating SHA-256 digests', () => {
58-
const sha256 = new Sha256();
59-
const spy = jest.spyOn((sha256 as any).hash, 'finalize');
60-
61-
sha256.digest();
62-
expect(spy.mock.calls.length).toBe(1);
63-
});
64-
65-
it('should call digest when creating SHA-256 HMACs', () => {
66-
const sha256 = new Sha256('secret');
67-
const spy = jest.spyOn((sha256 as any).hash, 'digest');
68-
69-
sha256.digest();
70-
expect(spy.mock.calls.length).toBe(1);
71-
});
72-
7356
it('should trap finalization errors', async () => {
7457
const sha256 = new Sha256();
75-
jest.spyOn((sha256 as any).hash, 'finalize')
58+
jest.spyOn((sha256 as any).hash, 'digest')
7659
.mockImplementation(() => {
7760
throw new Error('PANIC');
7861
});
@@ -82,4 +65,22 @@ describe('Sha256', () => {
8265
() => { /* Promise rejected, just as expected */ }
8366
);
8467
});
68+
69+
let idx = 0;
70+
for (const [input, result] of hashTestVectors) {
71+
it('should match known hash calculations: ' + idx++, async () => {
72+
const hash = new Sha256();
73+
hash.update(input);
74+
expect(await hash.digest()).toEqual(result);
75+
});
76+
}
77+
78+
idx = 0;
79+
for (const [key, data, result] of hmacTestVectors) {
80+
it('should match known hash calculations: ' + idx++, async () => {
81+
const hash = new Sha256(key);
82+
hash.update(data);
83+
expect(await hash.digest()).toEqual(result);
84+
});
85+
}
8586
});

0 commit comments

Comments
 (0)