Introduction to SHA-224 in TypeScript
SHA-224 is a variant of the SHA-2 family of cryptographic hash functions that produces a 224-bit (28-byte) hash value. It is designed to provide a good balance between security and performance, making it suitable for many cryptographic applications, especially where collision resistance is important but the full security of SHA-256 is not required.
This tutorial will guide you through implementing SHA-224 in TypeScript, covering:
- Understanding the SHA-224 algorithm
- Step-by-step implementation in pure TypeScript
- Using the Web Crypto API for browser environments
- Using the Node.js Crypto module for server environments
- Performance optimizations and benchmarks
- Common use cases and integration patterns
Prerequisites
To follow this tutorial, you should have:
- Basic understanding of TypeScript and JavaScript
- Familiarity with bitwise operations and hexadecimal notation
- Node.js installed for running the server-side examples
- A modern browser for the Web Crypto API examples
- Basic knowledge of cryptographic concepts
SHA-224 Algorithm Overview
SHA-224 is essentially SHA-256 with different initial values and truncated output. It follows the same core algorithm steps as other SHA-2 variants:
- Preprocessing: Padding the message to ensure its length is congruent to 448 modulo 512
- Parsing: Breaking the padded message into 512-bit blocks
- Message schedule: Creating the message schedule from each block
- Compression function: Processing each block with the compression function
- Output: Producing the final hash value (truncated to 224 bits)
Key Constants and Parameters
SHA-224 uses specific constants and initial hash values:
// SHA-224 Initial Hash Values (H)
// These values represent the first 32 bits of the fractional parts of the
// square roots of the 9th through 16th primes (23, 29, 31, 37, 41, 43, 47, 53)
const H0_224: number[] = [
0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4
];
// Round Constants (K)
// First 32 bits of the fractional parts of the cube roots of the first 64 primes (2 to 311)
const K: number[] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
];
SHA-224 Differences from SHA-256
While SHA-224 follows the same algorithm as SHA-256, there are two key differences:
- Initial Hash Values: SHA-224 uses different initial hash values (H0 through H7)
- Output Truncation: The final hash is truncated to 224 bits (28 bytes) instead of 256 bits (32 bytes)
Why Implement SHA-224?
You might wonder why you should implement SHA-224 when libraries already exist. Reasons include:
- Educational Value: Understanding cryptographic algorithms by implementing them
- Customizability: Adding specific features or optimizations for your use case
- Reduced Dependencies: Avoiding third-party dependencies in lightweight applications
- Legacy System Support: Supporting systems where modern crypto libraries aren't available
- Security Audit: Enabling detailed security reviews of your cryptographic implementation
Pure TypeScript Implementation
Let's implement SHA-224 in pure TypeScript, without relying on any built-in cryptographic APIs. This implementation will work in any TypeScript environment.
Helper Functions
First, we'll define bit manipulation helper functions required for the SHA-224 algorithm:
/**
* Right-rotates a 32-bit number by the specified number of bits
*/
function rightRotate(value: number, amount: number): number {
return ((value >>> amount) | (value << (32 - amount))) >>> 0;
}
/**
* Converts a string to an array of bytes
*/
function stringToBytes(str: string): number[] {
const bytes: number[] = [];
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i);
if (charCode < 0x80) {
// Single byte character
bytes.push(charCode);
} else if (charCode < 0x800) {
// Two byte character
bytes.push(0xc0 | (charCode >> 6),
0x80 | (charCode & 0x3f));
} else if (charCode < 0xd800 || charCode >= 0xe000) {
// Three byte character
bytes.push(0xe0 | (charCode >> 12),
0x80 | ((charCode >> 6) & 0x3f),
0x80 | (charCode & 0x3f));
} else {
// Four byte character (surrogate pair)
i++;
const nextCharCode = str.charCodeAt(i);
const codePoint = 0x10000 + (((charCode & 0x3ff) << 10) | (nextCharCode & 0x3ff));
bytes.push(0xf0 | (codePoint >> 18),
0x80 | ((codePoint >> 12) & 0x3f),
0x80 | ((codePoint >> 6) & 0x3f),
0x80 | (codePoint & 0x3f));
}
}
return bytes;
}
/**
* Converts a number to a byte array
*/
function numberToBytes(n: number): number[] {
const bytes = new Array(8);
for (let i = 7; i >= 0; i--) {
bytes[i] = n & 0xff;
n = n >>> 8;
}
return bytes;
}
/**
* Converts a byte array to a hex string
*/
function bytesToHex(bytes: number[]): string {
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
Core SHA-224 Implementation
Now, let's implement the main SHA-224 function:
/**
* Computes the SHA-224 hash of the given message
*
* @param message - The message to hash (string or byte array)
* @returns The SHA-224 hash as a hex string
*/
function sha224(message: string | number[]): string {
// Convert string to byte array if needed
const bytes = typeof message === 'string' ? stringToBytes(message) : message;
// Initialize hash values (h0 to h7) - SHA-224 specific
const h = [
0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4
];
// Pre-processing: Padding the message
// Append the bit '1' to the message
const paddedBytes = [...bytes, 0x80];
// Append 0 ≤ k < 512 bits '0' such that (message length + 1 + k + 64) is a multiple of 512
const byteLength = bytes.length * 8; // Length in bits
const blockCount = Math.ceil((paddedBytes.length + 8) / 64); // 64 bytes = 512 bits per block
const paddedLength = blockCount * 64; // Total length in bytes after padding
// Ensure we have enough space for padding and length
while (paddedBytes.length < paddedLength - 8) {
paddedBytes.push(0);
}
// Append length of the original message as a 64-bit big-endian integer
const lengthBytes = numberToBytes(byteLength);
for (let i = 0; i < 8; i++) {
paddedBytes.push(lengthBytes[i]);
}
// Process the message in successive 512-bit (64-byte) chunks
for (let i = 0; i < paddedBytes.length; i += 64) {
// Create a 64-entry message schedule array w[0..63]
const w = new Array(64).fill(0);
// Copy the chunk into the first 16 words of the message schedule array
for (let j = 0; j < 16; j++) {
w[j] = (paddedBytes[i + (j * 4)] << 24) |
(paddedBytes[i + (j * 4) + 1] << 16) |
(paddedBytes[i + (j * 4) + 2] << 8) |
(paddedBytes[i + (j * 4) + 3]);
}
// Extend the first 16 words into the remaining 48 words
for (let j = 16; j < 64; j++) {
const s0 = rightRotate(w[j - 15], 7) ^ rightRotate(w[j - 15], 18) ^ (w[j - 15] >>> 3);
const s1 = rightRotate(w[j - 2], 17) ^ rightRotate(w[j - 2], 19) ^ (w[j - 2] >>> 10);
w[j] = (w[j - 16] + s0 + w[j - 7] + s1) >>> 0;
}
// Initialize working variables to current hash state
let [a, b, c, d, e, f, g, h_val] = h;
// Compression function main loop
for (let j = 0; j < 64; j++) {
const S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
const ch = (e & f) ^ (~e & g);
const temp1 = (h_val + S1 + ch + K[j] + w[j]) >>> 0;
const S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
const maj = (a & b) ^ (a & c) ^ (b & c);
const temp2 = (S0 + maj) >>> 0;
h_val = g;
g = f;
f = e;
e = (d + temp1) >>> 0;
d = c;
c = b;
b = a;
a = (temp1 + temp2) >>> 0;
}
// Add the compressed chunk to the current hash value
h[0] = (h[0] + a) >>> 0;
h[1] = (h[1] + b) >>> 0;
h[2] = (h[2] + c) >>> 0;
h[3] = (h[3] + d) >>> 0;
h[4] = (h[4] + e) >>> 0;
h[5] = (h[5] + f) >>> 0;
h[6] = (h[6] + g) >>> 0;
h[7] = (h[7] + h_val) >>> 0;
}
// Produce the final hash value (big-endian)
// Note: For SHA-224, we only use h[0] through h[6], dropping h[7]
const result = [];
for (let i = 0; i < 7; i++) {
result.push((h[i] >> 24) & 0xff);
result.push((h[i] >> 16) & 0xff);
result.push((h[i] >> 8) & 0xff);
result.push(h[i] & 0xff);
}
// Convert to hex string
return bytesToHex(result);
}
Testing the Implementation
Let's create a function to test our SHA-224 implementation against known test vectors:
/**
* Tests the SHA-224 implementation against known test vectors
*/
function testSHA224() {
// Test vectors from RFC 3874
const testCases = [
{
input: '',
expected: 'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f'
},
{
input: 'abc',
expected: '23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7'
},
{
input: 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq',
expected: '75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525'
},
{
input: 'The quick brown fox jumps over the lazy dog',
expected: '730e109bd7a8a32b1cb9d9a09aa2325d2430587ddbc0c38bad911525'
},
{
input: 'The quick brown fox jumps over the lazy dog.',
expected: '619cba8e8e05826e9b8c519c0a5c68f4fb653e8a3d8aa04bb2c8cd4c'
}
];
let allPassed = true;
for (const testCase of testCases) {
const result = sha224(testCase.input);
const passed = result === testCase.expected;
console.log(`Test case: "${testCase.input.substring(0, 30)}${testCase.input.length > 30 ? '...' : ''}"`);
console.log(`Expected: ${testCase.expected}`);
console.log(`Got: ${result}`);
console.log(`Result: ${passed ? 'PASS' : 'FAIL'}`);
console.log('-------------------');
if (!passed) {
allPassed = false;
}
}
// Test a million 'a' characters (if needed)
/*
console.log("Testing 1,000,000 'a' characters...");
const millionA = new Array(1000000).fill('a').join('');
const millionAResult = sha224(millionA);
const millionAExpected = '20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67';
console.log(`Expected: ${millionAExpected}`);
console.log(`Got: ${millionAResult}`);
console.log(`Result: ${millionAResult === millionAExpected ? 'PASS' : 'FAIL'}`);
if (millionAResult !== millionAExpected) {
allPassed = false;
}
*/
console.log(`Overall result: ${allPassed ? 'ALL TESTS PASSED' : 'SOME TESTS FAILED'}`);
return allPassed;
}
// Run the tests
testSHA224();
Understanding the Implementation
Key points about the pure TypeScript implementation:
- Bitwise Operations: SHA-224 relies heavily on bitwise operations (XOR, AND, OR, rotations)
- 32-bit Unsigned Integers: All operations are performed on 32-bit integers, using the
>>>
operator to ensure we work with unsigned values - Padding: The message is padded to ensure its length in bits plus 64 is a multiple of 512
- Message Schedule: The message schedule array expands the 16 words of each block into 64 words
- Compression Function: This is the core operation that mixes the message data with the hash state
- Truncation: For SHA-224, we only use the first 7 words (224 bits) of the final hash value
Using the Web Crypto API
Modern browsers provide the Web Crypto API, which includes efficient, native implementations of cryptographic functions, including SHA-224. Let's see how to use it:
/**
* Computes the SHA-224 hash using the Web Crypto API
*
* @param message - The message to hash
* @returns A Promise resolving to the SHA-224 hash as a hex string
*/
async function sha224WebCrypto(message: string): Promise {
// Convert the message to an ArrayBuffer
const encoder = new TextEncoder();
const data = encoder.encode(message);
// Use the subtle crypto API to compute the digest
const hashBuffer = await crypto.subtle.digest('SHA-224', data);
// Convert the hash to a hex string
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
/**
* Test the Web Crypto API implementation of SHA-224
*/
async function testWebCryptoSHA224() {
try {
const testCases = [
{
input: '',
expected: 'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f'
},
{
input: 'abc',
expected: '23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7'
},
{
input: 'The quick brown fox jumps over the lazy dog',
expected: '730e109bd7a8a32b1cb9d9a09aa2325d2430587ddbc0c38bad911525'
}
];
let allPassed = true;
for (const testCase of testCases) {
const result = await sha224WebCrypto(testCase.input);
const passed = result === testCase.expected;
console.log(`Test case: "${testCase.input}"`);
console.log(`Expected: ${testCase.expected}`);
console.log(`Got: ${result}`);
console.log(`Result: ${passed ? 'PASS' : 'FAIL'}`);
console.log('-------------------');
if (!passed) {
allPassed = false;
}
}
console.log(`Overall result: ${allPassed ? 'ALL TESTS PASSED' : 'SOME TESTS FAILED'}`);
return allPassed;
} catch (error) {
console.error('Error testing Web Crypto API SHA-224:', error);
return false;
}
}
// Check if Web Crypto API is available
if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) {
console.log('Web Crypto API is available, running tests...');
testWebCryptoSHA224().catch(console.error);
} else {
console.log('Web Crypto API is not available in this environment');
}
Browser Compatibility Note
While most modern browsers support the Web Crypto API, there are a few considerations:
- In some browsers, the Web Crypto API is only available in secure contexts (HTTPS)
- Older browsers may not support SHA-224 specifically
- Internet Explorer has limited or no support for the Web Crypto API
Always include fallback mechanisms when using the Web Crypto API in production applications.
Using Node.js Crypto Module
For server-side applications, Node.js provides a built-in Crypto module with strong performance and security. Here's how to use it for SHA-224:
// Import the crypto module
import * as crypto from 'crypto';
/**
* Computes the SHA-224 hash using the Node.js Crypto module
*
* @param message - The message to hash
* @param encoding - The encoding of the message (default: 'utf8')
* @returns The SHA-224 hash as a hex string
*/
function sha224NodeCrypto(
message: string | Buffer,
encoding: crypto.BinaryToTextEncoding = 'hex'
): string {
const hash = crypto.createHash('sha224');
// Update the hash with the message
hash.update(message);
// Return the digest in the specified encoding
return hash.digest(encoding);
}
/**
* Stream-based SHA-224 hashing for large files
*
* @param filePath - The path to the file to hash
* @returns A Promise resolving to the SHA-224 hash as a hex string
*/
function sha224File(filePath: string): Promise {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha224');
const stream = require('fs').createReadStream(filePath);
stream.on('data', (data: Buffer) => {
hash.update(data);
});
stream.on('end', () => {
resolve(hash.digest('hex'));
});
stream.on('error', (error: Error) => {
reject(error);
});
});
}
/**
* Test the Node.js Crypto implementation of SHA-224
*/
function testNodeCryptoSHA224() {
const testCases = [
{
input: '',
expected: 'd14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f'
},
{
input: 'abc',
expected: '23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7'
},
{
input: 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq',
expected: '75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525'
},
{
input: 'The quick brown fox jumps over the lazy dog',
expected: '730e109bd7a8a32b1cb9d9a09aa2325d2430587ddbc0c38bad911525'
}
];
let allPassed = true;
for (const testCase of testCases) {
const result = sha224NodeCrypto(testCase.input);
const passed = result === testCase.expected;
console.log(`Test case: "${testCase.input.substring(0, 30)}${testCase.input.length > 30 ? '...' : ''}"`);
console.log(`Expected: ${testCase.expected}`);
console.log(`Got: ${result}`);
console.log(`Result: ${passed ? 'PASS' : 'FAIL'}`);
console.log('-------------------');
if (!passed) {
allPassed = false;
}
}
console.log(`Overall result: ${allPassed ? 'ALL TESTS PASSED' : 'SOME TESTS FAILED'}`);
return allPassed;
}
// Run the tests if in Node.js environment
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
console.log('Node.js environment detected, running tests...');
testNodeCryptoSHA224();
} else {
console.log('Not in Node.js environment, skipping Node.js Crypto tests');
}
HMAC with SHA-224
The Node.js Crypto module also makes it easy to implement HMAC (Hash-based Message Authentication Code) using SHA-224:
/**
* Creates an HMAC using SHA-224
*
* @param message - The message to authenticate
* @param key - The secret key
* @returns The HMAC as a hex string
*/
function hmacSHA224(message: string | Buffer, key: string | Buffer): string {
const hmac = crypto.createHmac('sha224', key);
hmac.update(message);
return hmac.digest('hex');
}
// Example usage
const message = 'This is a secure message';
const key = 'secretKey123';
const authCode = hmacSHA224(message, key);
console.log(`HMAC-SHA224: ${authCode}`);
Performance Considerations
When implementing SHA-224 in TypeScript, performance can vary significantly depending on your approach. Let's compare the performance of our different implementations:
/**
* Benchmarks different SHA-224 implementations
*
* @param iterations - Number of iterations for each test
* @param messageSize - Size of test message in characters
*/
async function benchmarkSHA224(iterations: number = 1000, messageSize: number = 1000): Promise {
console.log(`Benchmarking SHA-224 implementations with ${iterations} iterations on ${messageSize} character message...`);
// Generate a random message of specified size
let message = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < messageSize; i++) {
message += characters.charAt(Math.floor(Math.random() * characters.length));
}
// Benchmark pure TypeScript implementation
console.log('Benchmarking pure TypeScript implementation...');
const pureStart = performance.now();
for (let i = 0; i < iterations; i++) {
sha224(message);
}
const pureEnd = performance.now();
const pureDuration = pureEnd - pureStart;
console.log(`Pure TypeScript: ${pureDuration.toFixed(2)}ms (${(pureDuration / iterations).toFixed(2)}ms per hash)`);
// Benchmark Web Crypto API (if available)
if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) {
console.log('Benchmarking Web Crypto API implementation...');
const webCryptoStart = performance.now();
for (let i = 0; i < iterations; i++) {
await sha224WebCrypto(message);
}
const webCryptoEnd = performance.now();
const webCryptoDuration = webCryptoEnd - webCryptoStart;
console.log(`Web Crypto API: ${webCryptoDuration.toFixed(2)}ms (${(webCryptoDuration / iterations).toFixed(2)}ms per hash)`);
console.log(`Web Crypto is ${(pureDuration / webCryptoDuration).toFixed(2)}x faster than pure TypeScript`);
}
// Benchmark Node.js Crypto (if available)
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
console.log('Benchmarking Node.js Crypto implementation...');
const nodeCryptoStart = performance.now();
for (let i = 0; i < iterations; i++) {
sha224NodeCrypto(message);
}
const nodeCryptoEnd = performance.now();
const nodeCryptoDuration = nodeCryptoEnd - nodeCryptoStart;
console.log(`Node.js Crypto: ${nodeCryptoDuration.toFixed(2)}ms (${(nodeCryptoDuration / iterations).toFixed(2)}ms per hash)`);
console.log(`Node.js Crypto is ${(pureDuration / nodeCryptoDuration).toFixed(2)}x faster than pure TypeScript`);
}
}
// Run the benchmark
benchmarkSHA224(1000, 1000).catch(console.error);
Optimization Techniques
If you need to optimize the pure TypeScript implementation, consider these techniques:
Typed Arrays
Using Uint32Array instead of regular arrays for internal operations can improve performance by reducing memory allocations and ensuring 32-bit operations:
// Use typed arrays for the hash state and message schedule
const h = new Uint32Array(8);
const w = new Uint32Array(64);
Loop Unrolling
Unrolling tight loops can reduce loop overhead and improve performance:
// Instead of:
for (let j = 0; j < 64; j++) {
// Compression function
}
// Unroll into blocks:
for (let j = 0; j < 64; j += 8) {
// Compression function for j
// Compression function for j+1
// ...
// Compression function for j+7
}
Message Pre-allocation
Pre-allocate the padded message array to avoid resizing:
// Calculate the final size up front
const finalSize = blockCount * 64;
const paddedBytes = new Uint8Array(finalSize);
// Copy input bytes
for (let i = 0; i < bytes.length; i++) {
paddedBytes[i] = bytes[i];
}
// Set padding byte
paddedBytes[bytes.length] = 0x80;
WebAssembly
For maximum performance, consider implementing the core hash function in WebAssembly:
// Load the WebAssembly module
const wasmModule = await WebAssembly.instantiateStreaming(
fetch('/sha224.wasm'),
{ env: { memory: new WebAssembly.Memory({ initial: 1 }) } }
);
// Use the WebAssembly implementation
function sha224Wasm(message) {
// ... code to prepare input for WebAssembly ...
return wasmModule.instance.exports.sha224(/* params */);
}
Performance Comparison
Based on typical benchmarks, here's how the different implementations compare:
- Node.js Crypto Module: Fastest (C++ implementation)
- Web Crypto API: Very fast (native browser implementation)
- WebAssembly Implementation: Fast (near-native speed)
- Optimized TypeScript: Moderate performance
- Pure TypeScript: Slowest (but most portable)
For production use, always prefer the native implementations (Node.js Crypto or Web Crypto API) when available.
Common Use Cases and Integration Patterns
Let's explore some common patterns for integrating SHA-224 into TypeScript applications:
Content Integrity Verification
SHA-224 can be used to verify the integrity of files or messages:
import * as fs from 'fs';
import * as crypto from 'crypto';
interface FileVerification {
fileName: string;
expectedHash: string;
actualHash: string;
isValid: boolean;
}
/**
* Verifies the integrity of files against expected SHA-224 hashes
*
* @param files - Array of file paths and their expected hashes
* @returns Promise resolving to verification results
*/
async function verifyFilesIntegrity(
files: Array<{ path: string, hash: string }>
): Promise {
const results: FileVerification[] = [];
for (const file of files) {
try {
const actualHash = await sha224File(file.path);
results.push({
fileName: file.path,
expectedHash: file.hash,
actualHash,
isValid: actualHash === file.hash
});
} catch (error) {
console.error(`Error verifying ${file.path}:`, error);
results.push({
fileName: file.path,
expectedHash: file.hash,
actualHash: 'ERROR',
isValid: false
});
}
}
return results;
}
// Example usage
const filesToVerify = [
{ path: '/path/to/file1.txt', hash: '123abc...' },
{ path: '/path/to/file2.txt', hash: '456def...' }
];
verifyFilesIntegrity(filesToVerify)
.then(results => {
console.log('Verification Results:');
for (const result of results) {
console.log(`${result.fileName}: ${result.isValid ? 'VALID' : 'INVALID'}`);
if (!result.isValid) {
console.log(` Expected: ${result.expectedHash}`);
console.log(` Actual: ${result.actualHash}`);
}
}
})
.catch(error => {
console.error('Verification failed:', error);
});
Password Hashing
Although specialized password hashing functions are preferred for password storage, SHA-224 can be used as part of a proper password hashing scheme:
import * as crypto from 'crypto';
interface PasswordHash {
hash: string;
salt: string;
iterations: number;
}
/**
* Creates a secure password hash using PBKDF2 with SHA-224
*
* @param password - The password to hash
* @param iterations - Number of iterations (default: 10000)
* @returns Promise resolving to the password hash information
*/
function hashPassword(password: string, iterations: number = 10000): Promise {
return new Promise((resolve, reject) => {
// Generate a random salt
crypto.randomBytes(16, (err, salt) => {
if (err) {
return reject(err);
}
// Use PBKDF2 with SHA-224 as the HMAC
crypto.pbkdf2(
password,
salt,
iterations,
28, // SHA-224 produces 28 bytes
'sha224',
(err, derivedKey) => {
if (err) {
return reject(err);
}
resolve({
hash: derivedKey.toString('hex'),
salt: salt.toString('hex'),
iterations
});
}
);
});
});
}
/**
* Verifies a password against a stored hash
*
* @param password - The password to verify
* @param storedHash - The stored hash information
* @returns Promise resolving to a boolean indicating if the password is valid
*/
function verifyPassword(password: string, storedHash: PasswordHash): Promise {
return new Promise((resolve, reject) => {
const salt = Buffer.from(storedHash.salt, 'hex');
crypto.pbkdf2(
password,
salt,
storedHash.iterations,
28,
'sha224',
(err, derivedKey) => {
if (err) {
return reject(err);
}
resolve(derivedKey.toString('hex') === storedHash.hash);
}
);
});
}
// Example usage
async function passwordDemo() {
try {
// Hash a password
const password = 'MySecurePassword123!';
const hashedPassword = await hashPassword(password);
console.log('Password hash:', hashedPassword);
// Verify the correct password
const validResult = await verifyPassword(password, hashedPassword);
console.log('Valid password verification:', validResult);
// Verify an incorrect password
const invalidResult = await verifyPassword('WrongPassword', hashedPassword);
console.log('Invalid password verification:', invalidResult);
} catch (error) {
console.error('Password hashing error:', error);
}
}
passwordDemo().catch(console.error);
Password Hashing Warning
While the example above shows how to use SHA-224 with PBKDF2 for password hashing, in production systems, consider these alternatives:
- Argon2 (preferred for new applications)
- bcrypt (widely supported)
- scrypt (memory-hard function)
These algorithms are specifically designed for password hashing and offer better protection against specialized attacks.
Digital Signatures
SHA-224 can be used as part of a digital signature scheme:
import * as crypto from 'crypto';
/**
* Creates a digital signature using SHA-224 and RSA
*
* @param message - The message to sign
* @param privateKey - The RSA private key in PEM format
* @returns The signature as a hex string
*/
function signMessage(message: string, privateKey: string): string {
const sign = crypto.createSign('RSA-SHA224');
sign.update(message);
return sign.sign(privateKey, 'hex');
}
/**
* Verifies a digital signature using SHA-224 and RSA
*
* @param message - The original message
* @param signature - The signature to verify
* @param publicKey - The RSA public key in PEM format
* @returns Boolean indicating if the signature is valid
*/
function verifySignature(message: string, signature: string, publicKey: string): boolean {
const verify = crypto.createVerify('RSA-SHA224');
verify.update(message);
return verify.verify(publicKey, signature, 'hex');
}
// Example usage (in Node.js environment)
function signatureDemo() {
// Generate RSA key pair
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
});
// Message to sign
const message = 'This is a secure message that needs authentication';
// Sign the message
const signature = signMessage(message, privateKey);
console.log('Signature:', signature);
// Verify the signature
const isValid = verifySignature(message, signature, publicKey);
console.log('Signature verification:', isValid);
// Try verifying a tampered message
const tamperedMessage = message + ' (tampered)';
const isValidTampered = verifySignature(tamperedMessage, signature, publicKey);
console.log('Tampered message verification:', isValidTampered);
}
// Run the demo if in Node.js environment
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
signatureDemo();
}
Content-Addressable Storage
SHA-224 can be used to implement content-addressable storage, where files are stored and retrieved based on their content hash:
import * as fs from 'fs';
import * as path from 'path';
import * as crypto from 'crypto';
class ContentAddressableStorage {
private storageDir: string;
constructor(storageDir: string) {
this.storageDir = storageDir;
// Ensure storage directory exists
if (!fs.existsSync(storageDir)) {
fs.mkdirSync(storageDir, { recursive: true });
}
}
/**
* Stores content and returns its SHA-224 hash
*
* @param content - The content to store
* @returns The SHA-224 hash of the content
*/
storeContent(content: string | Buffer): string {
// Convert string to buffer if needed
const data = typeof content === 'string' ? Buffer.from(content) : content;
// Calculate the SHA-224 hash
const hash = crypto.createHash('sha224').update(data).digest('hex');
// Store the content using the hash as the filename
const filePath = path.join(this.storageDir, hash);
fs.writeFileSync(filePath, data);
return hash;
}
/**
* Retrieves content by its SHA-224 hash
*
* @param hash - The SHA-224 hash of the content
* @returns The content as a Buffer, or null if not found
*/
retrieveContent(hash: string): Buffer | null {
const filePath = path.join(this.storageDir, hash);
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath);
}
return null;
}
/**
* Verifies if content matches a given hash
*
* @param content - The content to verify
* @param hash - The expected SHA-224 hash
* @returns Boolean indicating if the content matches the hash
*/
verifyContent(content: string | Buffer, hash: string): boolean {
const data = typeof content === 'string' ? Buffer.from(content) : content;
const computedHash = crypto.createHash('sha224').update(data).digest('hex');
return computedHash === hash;
}
/**
* Lists all stored content hashes
*
* @returns Array of SHA-224 hashes
*/
listContent(): string[] {
return fs.readdirSync(this.storageDir);
}
}
// Example usage
function casDemo() {
const storage = new ContentAddressableStorage('./content-store');
// Store some content
const content1 = 'This is the first piece of content';
const hash1 = storage.storeContent(content1);
console.log(`Content 1 stored with hash: ${hash1}`);
const content2 = 'This is the second piece of content';
const hash2 = storage.storeContent(content2);
console.log(`Content 2 stored with hash: ${hash2}`);
// Retrieve content by hash
const retrieved1 = storage.retrieveContent(hash1);
if (retrieved1) {
console.log(`Retrieved content 1: ${retrieved1.toString()}`);
}
// Verify content
const isValid = storage.verifyContent(content1, hash1);
console.log(`Content verification: ${isValid}`);
// List all stored content
const allContent = storage.listContent();
console.log(`All stored content hashes: ${allContent.join(', ')}`);
}
// Run the demo if in Node.js environment
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
casDemo();
}
Conclusion and Best Practices
In this tutorial, we've explored multiple approaches to implementing and using SHA-224 in TypeScript, from pure TypeScript implementations to leveraging native APIs in browsers and Node.js.
Implementation Recommendations
Based on our exploration, here are the recommended approaches for different scenarios:
Production Web Applications
Use the Web Crypto API with a pure TypeScript fallback:
- Provides the best performance on supported browsers
- Falls back to pure implementation for older browsers
- Consider adding polyfills for broader compatibility
Node.js Applications
Use the built-in Crypto module:
- Offers the best performance and security
- Maintained and updated with security patches
- Supports streaming for large files
Cross-Platform Libraries
Use a hybrid approach with environment detection:
- Detect available native implementations
- Fall back to pure TypeScript when needed
- Consider WebAssembly for performance-critical applications
Educational Purposes
Implement the pure TypeScript version:
- Provides deeper understanding of the algorithm
- Allows custom modifications for learning
- Can be adapted for teaching cryptographic concepts
Security Best Practices
When using SHA-224 in your applications, keep these security principles in mind:
- Use Case Appropriateness: SHA-224 is suitable for many applications but may not be the best choice for all. For password storage, prefer specialized password hashing functions like Argon2, bcrypt, or PBKDF2.
- Content Validation: Always validate that a message hasn't been tampered with before accepting it based on a SHA-224 hash.
- Side-Channel Protection: Be aware of potential side-channel attacks when performing cryptographic operations, especially in security-critical applications.
- Constant-Time Comparison: When comparing hashes, use constant-time comparison functions to prevent timing attacks.
- Library Updates: Keep your cryptographic libraries updated to ensure you have the latest security patches.
- Test Vectors: Always validate your implementation against known test vectors to ensure correctness.
Next Steps
Now that you understand how to implement and use SHA-224 in TypeScript, you can:
- Explore the code examples repository for more implementation patterns
- Read about comparisons between SHA-224 and other hash functions
- Check out the security considerations for using SHA-224 in different contexts
- Learn about quantum computing impacts on SHA-224 and other hash functions
- Explore practical use cases for SHA-224 in real-world applications