aboutsummaryrefslogtreecommitdiffstats
path: root/src/git/sha1.ts
blob: 8f7300d057fb0c2e77b7a9515e1b16aeb297e16d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
import { SHA1_DVS, ubcCheck } from "./sha1dc-tables";
 
/**
 * Incremental SHA-1. Workers' crypto.subtle is one-shot and async; pack
 * streaming needs a running digest and the object database wants sync
 * hashing, so we carry our own (git still speaks SHA-1 for object ids).
 */
export class Sha1 {
  private h0 = 0x67452301 | 0;
  private h1 = 0xefcdab89 | 0;
  private h2 = 0x98badcfe | 0;
  private h3 = 0x10325476 | 0;
  private h4 = 0xc3d2e1f0 | 0;
  private block = new Uint8Array(64);
  private blockLen = 0;
  private bytes = 0;
  private w = new Int32Array(80);
 
  /** Reinitialize so one instance can hash many inputs without reallocating. */
  reset(): this {
    this.h0 = 0x67452301 | 0;
    this.h1 = 0xefcdab89 | 0;
    this.h2 = 0x98badcfe | 0;
    this.h3 = 0x10325476 | 0;
    this.h4 = 0xc3d2e1f0 | 0;
    this.blockLen = 0;
    this.bytes = 0;
    return this;
  }
 
  update(data: Uint8Array): this {
    this.bytes += data.length;
    let off = 0;
    if (this.blockLen > 0) {
      const need = 64 - this.blockLen;
      const take = Math.min(need, data.length);
      this.block.set(data.subarray(0, take), this.blockLen);
      this.blockLen += take;
      off = take;
      if (this.blockLen === 64) {
        this.compress(this.block, 0);
        this.blockLen = 0;
      }
    }
    while (off + 64 <= data.length) {
      this.compress(data, off);
      off += 64;
    }
    if (off < data.length) {
      this.block.set(data.subarray(off), 0);
      this.blockLen = data.length - off;
    }
    return this;
  }
 
  digest(): Uint8Array {
    const bitLenHi = Math.floor((this.bytes * 8) / 0x100000000);
    const bitLenLo = (this.bytes * 8) >>> 0;
    const pad = new Uint8Array(((this.blockLen < 56 ? 56 : 120) - this.blockLen) + 8);
    pad[0] = 0x80;
    const dv = new DataView(pad.buffer);
    dv.setUint32(pad.length - 8, bitLenHi);
    dv.setUint32(pad.length - 4, bitLenLo);
    this.update(pad);
    const out = new Uint8Array(20);
    const ov = new DataView(out.buffer);
    ov.setInt32(0, this.h0);
    ov.setInt32(4, this.h1);
    ov.setInt32(8, this.h2);
    ov.setInt32(12, this.h3);
    ov.setInt32(16, this.h4);
    return out;
  }
 
  private compress(buf: Uint8Array, off: number): void {
    const w = this.w;
    for (let i = 0; i < 16; i++) {
      const j = off + i * 4;
      w[i] = (buf[j] << 24) | (buf[j + 1] << 16) | (buf[j + 2] << 8) | buf[j + 3];
    }
    for (let i = 16; i < 80; i++) {
      const n = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
      w[i] = (n << 1) | (n >>> 31);
    }
    let a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4;
    for (let i = 0; i < 80; i++) {
      let f: number, k: number;
      if (i < 20) {
        f = (b & c) | (~b & d);
        k = 0x5a827999;
      } else if (i < 40) {
        f = b ^ c ^ d;
        k = 0x6ed9eba1;
      } else if (i < 60) {
        f = (b & c) | (b & d) | (c & d);
        k = 0x8f1bbcdc | 0;
      } else {
        f = b ^ c ^ d;
        k = 0xca62c1d6 | 0;
      }
      const t = (((a << 5) | (a >>> 27)) + f + e + k + w[i]) | 0;
      e = d;
      d = c;
      c = (b << 30) | (b >>> 2);
      b = a;
      a = t;
    }
    this.h0 = (this.h0 + a) | 0;
    this.h1 = (this.h1 + b) | 0;
    this.h2 = (this.h2 + c) | 0;
    this.h3 = (this.h3 + d) | 0;
    this.h4 = (this.h4 + e) | 0;
  }
}
 
export function sha1(data: Uint8Array): Uint8Array {
  return new Sha1().update(data).digest();
}
 
/*
 * SHA-1 collision detection (SHA-1DC)
 *
 * A port of Stevens & Shumow's sha1collisiondetection, the same defence
 * git adopted after SHAttered. Every compressed block is screened by
 * ubcCheck against 32 known disturbance vectors; a flagged DV is then
 * confirmed by recompressing the block from the DV's test step with the
 * perturbed message. If the reconstructed chaining value reproduces the
 * real one, the block is half of a collision attack.
 *
 * Like git, safe-hash mangling stays off: the digest is always plain
 * SHA-1, so object ids never move. Detection is reported out-of-band and
 * the caller refuses the object.
 */
 
const DVS = SHA1_DVS.map((dv) => ({
  testt: dv.testt,
  maskb: dv.maskb,
  dm: Int32Array.from(dv.dm),
}));
 
const K0 = 0x5a827999 | 0;
const K1 = 0x6ed9eba1 | 0;
const K2 = 0x8f1bbcdc | 0;
const K3 = 0xca62c1d6 | 0;
 
/**
 * One step of the compression function, run over a rotating register file
 * so a single loop can serve any step index. At step `i` the roles
 * (a,b,c,d,e) live at v[(role - i) mod 5].
 */
function stepForward(v: Int32Array, i: number, m: Int32Array): void {
  const ia = (5 - (i % 5)) % 5;
  const ib = (ia + 1) % 5;
  const ic = (ia + 2) % 5;
  const id = (ia + 3) % 5;
  const ie = (ia + 4) % 5;
  const a = v[ia], b = v[ib], c = v[ic], d = v[id], e = v[ie];
  let f: number, k: number;
  if (i < 20) { f = d ^ (b & (c ^ d)); k = K0; }
  else if (i < 40) { f = b ^ c ^ d; k = K1; }
  else if (i < 60) { f = (b & c) | (d & (b ^ c)); k = K2; }
  else { f = b ^ c ^ d; k = K3; }
  v[ie] = (e + (((a << 5) | (a >>> 27)) + f + k + m[i])) | 0;
  v[ib] = (b << 30) | (b >>> 2);
}
 
/** Inverse of stepForward: undoes step `i`. */
function stepBackward(v: Int32Array, i: number, m: Int32Array): void {
  const ia = (5 - (i % 5)) % 5;
  const ib = (ia + 1) % 5;
  const ic = (ia + 2) % 5;
  const id = (ia + 3) % 5;
  const ie = (ia + 4) % 5;
  const a = v[ia];
  const b = ((v[ib] >>> 30) | (v[ib] << 2)) | 0;
  v[ib] = b;
  const c = v[ic], d = v[id], e = v[ie];
  let f: number, k: number;
  if (i < 20) { f = d ^ (b & (c ^ d)); k = K0; }
  else if (i < 40) { f = b ^ c ^ d; k = K1; }
  else if (i < 60) { f = (b & c) | (d & (b ^ c)); k = K2; }
  else { f = b ^ c ^ d; k = K3; }
  v[ie] = (e - (((a << 5) | (a >>> 27)) + f + k + m[i])) | 0;
}
 
/**
 * Reconstruct the chaining values a block would have had, had it been
 * compressed with the perturbed message `me2`. Runs backwards from step
 * `t` to recover the input chaining value, then forwards to step 79 for
 * the output. `state` is the real block's register file entering step t.
 */
function recompress(
  t: number,
  ihvin: Int32Array,
  ihvout: Int32Array,
  me2: Int32Array,
  state: Int32Array,
  scratch: Int32Array
): void {
  scratch.set(state);
  for (let i = t - 1; i >= 0; i--) stepBackward(scratch, i, me2);
  ihvin.set(scratch);
  scratch.set(state);
  for (let i = t; i < 80; i++) stepForward(scratch, i, me2);
  for (let j = 0; j < 5; j++) ihvout[j] = (ihvin[j] + scratch[j]) | 0;
}
 
/**
 * Collision-detecting SHA-1. Drop-in for {@link Sha1}: `digest()` returns
 * the identical bytes for every input. After digesting, {@link collision}
 * reports whether any block looked like a collision-attack near-collision
 * block.
 */
export class Sha1Dc {
  private ihv = new Int32Array(5);
  private block = new Uint8Array(64);
  private blockLen = 0;
  private bytes = 0;
  /** expanded message of the block being compressed */
  private m1 = new Int32Array(80);
  private m2 = new Int32Array(80);
  /** register file entering steps 58 and 65 — the only DV test steps */
  private state58 = new Int32Array(5);
  private state65 = new Int32Array(5);
  private ihvin = new Int32Array(5);
  private ihvout = new Int32Array(5);
  private scratch = new Int32Array(5);
  private found = false;
 
  constructor() {
    this.reset();
  }
 
  /** True when a near-collision block was seen since the last reset. */
  get collision(): boolean {
    return this.found;
  }
 
  reset(): this {
    this.ihv[0] = 0x67452301 | 0;
    this.ihv[1] = 0xefcdab89 | 0;
    this.ihv[2] = 0x98badcfe | 0;
    this.ihv[3] = 0x10325476 | 0;
    this.ihv[4] = 0xc3d2e1f0 | 0;
    this.blockLen = 0;
    this.bytes = 0;
    this.found = false;
    return this;
  }
 
  update(data: Uint8Array): this {
    this.bytes += data.length;
    let off = 0;
    if (this.blockLen > 0) {
      const need = 64 - this.blockLen;
      const take = Math.min(need, data.length);
      this.block.set(data.subarray(0, take), this.blockLen);
      this.blockLen += take;
      off = take;
      if (this.blockLen === 64) {
        this.process(this.block, 0);
        this.blockLen = 0;
      }
    }
    while (off + 64 <= data.length) {
      this.process(data, off);
      off += 64;
    }
    if (off < data.length) {
      this.block.set(data.subarray(off), 0);
      this.blockLen = data.length - off;
    }
    return this;
  }
 
  digest(): Uint8Array {
    const bitLenHi = Math.floor((this.bytes * 8) / 0x100000000);
    const bitLenLo = (this.bytes * 8) >>> 0;
    const pad = new Uint8Array(((this.blockLen < 56 ? 56 : 120) - this.blockLen) + 8);
    pad[0] = 0x80;
    const dv = new DataView(pad.buffer);
    dv.setUint32(pad.length - 8, bitLenHi);
    dv.setUint32(pad.length - 4, bitLenLo);
    this.update(pad);
    const out = new Uint8Array(20);
    const ov = new DataView(out.buffer);
    for (let i = 0; i < 5; i++) ov.setInt32(i * 4, this.ihv[i]);
    return out;
  }
 
  /** Compress one block, retaining what the collision check needs. */
  private process(buf: Uint8Array, off: number): void {
    const w = this.m1;
    for (let i = 0; i < 16; i++) {
      const j = off + i * 4;
      w[i] = (buf[j] << 24) | (buf[j + 1] << 16) | (buf[j + 2] << 8) | buf[j + 3];
    }
    for (let i = 16; i < 80; i++) {
      const n = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
      w[i] = (n << 1) | (n >>> 31);
    }
    const ihv = this.ihv;
    let a = ihv[0], b = ihv[1], c = ihv[2], d = ihv[3], e = ihv[4];
    for (let i = 0; i < 80; i++) {
      // The unrolled reference names registers canonically while this loop
      // shifts them, so a snapshot at step i must be rotated back by i mod 5.
      if (i === 58) {
        // 58 mod 5 == 3
        this.state58[0] = d; this.state58[1] = e; this.state58[2] = a;
        this.state58[3] = b; this.state58[4] = c;
      } else if (i === 65) {
        // 65 mod 5 == 0
        this.state65[0] = a; this.state65[1] = b; this.state65[2] = c;
        this.state65[3] = d; this.state65[4] = e;
      }
      let f: number, k: number;
      if (i < 20) { f = (b & c) | (~b & d); k = K0; }
      else if (i < 40) { f = b ^ c ^ d; k = K1; }
      else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = K2; }
      else { f = b ^ c ^ d; k = K3; }
      const t = (((a << 5) | (a >>> 27)) + f + e + k + w[i]) | 0;
      e = d;
      d = c;
      c = (b << 30) | (b >>> 2);
      b = a;
      a = t;
    }
    ihv[0] = (ihv[0] + a) | 0;
    ihv[1] = (ihv[1] + b) | 0;
    ihv[2] = (ihv[2] + c) | 0;
    ihv[3] = (ihv[3] + d) | 0;
    ihv[4] = (ihv[4] + e) | 0;
 
    if (this.found) return; // already refused; skip the rest of the work
    const mask = ubcCheck(w);
    if (mask === 0) return;
    const m2 = this.m2;
    for (const dv of DVS) {
      if ((mask & (1 << dv.maskb)) === 0) continue;
      const dm = dv.dm;
      for (let j = 0; j < 80; j++) m2[j] = w[j] ^ dm[j];
      recompress(
        dv.testt,
        this.ihvin,
        this.ihvout,
        m2,
        dv.testt === 58 ? this.state58 : this.state65,
        this.scratch
      );
      const o = this.ihvout;
      if (((o[0] ^ ihv[0]) | (o[1] ^ ihv[1]) | (o[2] ^ ihv[2]) | (o[3] ^ ihv[3]) | (o[4] ^ ihv[4])) === 0) {
        this.found = true;
        return;
      }
    }
  }
}
 
/** Thrown when a hashed object carries a SHA-1 collision-attack block. */
export class Sha1CollisionError extends Error {
  readonly oid: string;
  constructor(oid: string) {
    super(`object ${oid.slice(0, 12)} triggered SHA-1 collision detection; refused`);
    this.name = "Sha1CollisionError";
    this.oid = oid;
  }
}