165 lines
6.1 KiB
JavaScript
165 lines
6.1 KiB
JavaScript
class SHA1 {
|
|
constructor() {
|
|
// Constants
|
|
this.BLOCK_LENGTH = 64; // Bytes
|
|
this.H = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0];
|
|
this.K = [0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6];
|
|
}
|
|
|
|
/**
|
|
* Rotates left (circular left shift) on a 32-bit number.
|
|
* @param {number} n - The 32-bit number.
|
|
* @param {number} bits - The number of bits to rotate.
|
|
* @returns {number} The rotated 32-bit number.
|
|
*/
|
|
_rotl(n, bits) {
|
|
// Ensure n is treated as 32-bit for the shift operations
|
|
return ((n << bits) | (n >>> (32 - bits))) | 0; // | 0 ensures result is 32-bit signed int
|
|
}
|
|
|
|
/**
|
|
* Adds SHA-1 padding to the message bytes.
|
|
* @param {Uint8Array} messageBytes - The message as bytes.
|
|
* @returns {Uint8Array} The padded message bytes.
|
|
*/
|
|
_addPadding(messageBytes) {
|
|
const msgLenBytes = messageBytes.length;
|
|
// Use BigInt for potentially large bit lengths
|
|
const bitLength = BigInt(msgLenBytes) * 8n;
|
|
|
|
// Calculate the total padded length in bytes
|
|
// Need msgLenBytes + 1 (for 0x80) + k (zeros) + 8 (length) = N * 64
|
|
// msgLenBytes + 9 + k = N * 64
|
|
// The number of zero bytes 'k' needed is:
|
|
// k = (64 - (msgLenBytes + 9) % 64) % 64
|
|
const k = (this.BLOCK_LENGTH - ((msgLenBytes + 9) % this.BLOCK_LENGTH)) % this.BLOCK_LENGTH;
|
|
const paddedLength = msgLenBytes + 1 + k + 8;
|
|
const paddedBytes = new Uint8Array(paddedLength);
|
|
|
|
// 1. Copy original message
|
|
paddedBytes.set(messageBytes);
|
|
|
|
// 2. Append a single '1' bit (0x80 byte)
|
|
paddedBytes[msgLenBytes] = 0x80;
|
|
|
|
// 3. Append K '0' bits (zero bytes). Uint8Array initializes to 0, so this is implicit.
|
|
|
|
// 4. Append length L as a 64-bit big-endian integer.
|
|
const lengthOffset = paddedLength - 8;
|
|
for (let i = 0; i < 8; i++) {
|
|
// Extract byte from BigInt bitLength
|
|
const shift = BigInt(i) * 8n;
|
|
// Place it in big-endian order (most significant byte first)
|
|
paddedBytes[lengthOffset + 7 - i] = Number((bitLength >> shift) & 0xffn);
|
|
}
|
|
|
|
return paddedBytes;
|
|
}
|
|
|
|
/**
|
|
* Computes the SHA-1 hash of a message string.
|
|
* @param {string} message - The input message string.
|
|
* @returns {string} The SHA-1 hash as a 40-character hex string.
|
|
*/
|
|
messageDigest(message) {
|
|
// 1. Convert message string to UTF-8 bytes
|
|
const encoder = new TextEncoder(); // Assumes UTF-8
|
|
const messageBytes = encoder.encode(message);
|
|
|
|
// 2. Add padding
|
|
const paddedBytes = this._addPadding(messageBytes);
|
|
|
|
// 3. Initialize hash values
|
|
let state = [...this.H]; // Create a working copy
|
|
|
|
// 4. Process message in 64-byte (512-bit) blocks
|
|
for (let i = 0; i < paddedBytes.length; i += this.BLOCK_LENGTH) {
|
|
const blockBytes = paddedBytes.subarray(i, i + this.BLOCK_LENGTH);
|
|
|
|
// Prepare the message schedule (80 32-bit words)
|
|
const W = new Array(80);
|
|
|
|
// a. Copy block into first 16 words (big-endian)
|
|
for (let t = 0; t < 16; t++) {
|
|
const offset = t * 4;
|
|
W[t] = ((blockBytes[offset] << 24) |
|
|
(blockBytes[offset + 1] << 16) |
|
|
(blockBytes[offset + 2] << 8) |
|
|
(blockBytes[offset + 3])) | 0; // | 0 ensures 32-bit signed int
|
|
}
|
|
|
|
// b. Extend the first 16 words into the remaining 64 words
|
|
for (let t = 16; t < 80; t++) {
|
|
const wt = W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16];
|
|
W[t] = this._rotl(wt, 1);
|
|
}
|
|
|
|
// Initialize hash value for this block
|
|
let a = state[0];
|
|
let b = state[1];
|
|
let c = state[2];
|
|
let d = state[3];
|
|
let e = state[4];
|
|
|
|
// Main loop
|
|
for (let t = 0; t < 80; t++) {
|
|
let f, k;
|
|
const round = Math.floor(t / 20);
|
|
switch (round) {
|
|
case 0: // Rounds 0-19
|
|
f = (b & c) | (~b & d);
|
|
k = this.K[0];
|
|
break;
|
|
case 1: // Rounds 20-39
|
|
f = b ^ c ^ d;
|
|
k = this.K[1];
|
|
break;
|
|
case 2: // Rounds 40-59
|
|
f = (b & c) | (b & d) | (c & d);
|
|
k = this.K[2];
|
|
break;
|
|
case 3: // Rounds 60-79
|
|
f = b ^ c ^ d;
|
|
k = this.K[3];
|
|
break;
|
|
}
|
|
|
|
// Ensure all additions are performed modulo 2^32
|
|
const temp = (this._rotl(a, 5) + f + e + k + W[t]) | 0;
|
|
e = d;
|
|
d = c;
|
|
c = this._rotl(b, 30);
|
|
b = a;
|
|
a = temp;
|
|
}
|
|
|
|
// Add this block's hash to the result so far (modulo 2^32)
|
|
state[0] = (state[0] + a) | 0;
|
|
state[1] = (state[1] + b) | 0;
|
|
state[2] = (state[2] + c) | 0;
|
|
state[3] = (state[3] + d) | 0;
|
|
state[4] = (state[4] + e) | 0;
|
|
}
|
|
|
|
// 5. Produce the final hash value (big-endian) as a hex string
|
|
let hexHash = "";
|
|
for (const h of state) {
|
|
// Convert each 32-bit component to hex, padding with '0'
|
|
// Use >>> 0 to treat h as unsigned before converting to hex parts
|
|
const unsignedH = h >>> 0;
|
|
hexHash += (unsignedH >>> 24).toString(16).padStart(2, '0');
|
|
hexHash += ((unsignedH >>> 16) & 0xff).toString(16).padStart(2, '0');
|
|
hexHash += ((unsignedH >>> 8) & 0xff).toString(16).padStart(2, '0');
|
|
hexHash += (unsignedH & 0xff).toString(16).padStart(2, '0');
|
|
}
|
|
|
|
return hexHash;
|
|
}
|
|
}
|
|
|
|
// --- Main execution (equivalent to C++ main) ---
|
|
const sha1 = new SHA1();
|
|
const message = "Rosetta Code";
|
|
const digest = sha1.messageDigest(message);
|
|
console.log(digest);
|