RosettaCodeData/Task/SHA-1/JavaScript/sha-1.js

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);