Skip to content

Checksum Verifier — TypeScript source

Drag and drop a file to compute its MD5, SHA-1, SHA-256, and SHA-512 checksums. Paste an expected hash to verify integrity - detect tampered or corrupted downloads instantly. Runs entirely in your browser.

This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

/**
 * Checksum Verifier - pure logic.
 *
 * MD5 is implemented from scratch in pure JS (RFC 1321) with an incremental
 * update/finalize interface, because the Web Crypto API does not support MD5.
 * SHA-1 / SHA-256 / SHA-512 use the native Web Crypto `subtle.digest`.
 */

export interface ChecksumResult {
  md5: string;
  sha1: string;
  sha256: string;
  sha512: string;
}

export type HashAlgorithm = 'md5' | 'sha1' | 'sha256' | 'sha512';

// ---------------------------------------------------------------------------
// MD5 (RFC 1321)
// ---------------------------------------------------------------------------

// Per-round shift amounts (RFC 1321, section 3.4).
const MD5_S = [
  7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
  5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
  4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
  6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
];

// T[i] = floor(2^32 * |sin(i + 1)|) - the RFC 1321 sine table. IEEE-754
// doubles make this deterministic on every platform.
const MD5_K = new Int32Array(64).map((_, i) =>
  Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296),
);

export interface MD5Hasher {
  /** Absorb bytes. Call repeatedly for chunked input (no length limit). */
  update(data: Uint8Array): void;
  /** Pad, append the 64-bit length, and return the lowercase hex digest. */
  digestHex(): string;
}

/** Create an incremental MD5 hasher (RFC 1321, MD5 is only for integrity checks - not security). */
export function createMD5(): MD5Hasher {
  let a0 = 0x67452301 | 0;
  let b0 = 0xefcdab89 | 0;
  let c0 = 0x98badcfe | 0;
  let d0 = 0x10325476 | 0;
  let total = 0;
  const buf = new Uint8Array(64);
  let buffered = 0;

  function transform(block: Uint8Array, start: number): void {
    const m = new Int32Array(16);
    for (let i = 0; i < 16; i++) {
      const o = start + i * 4;
      m[i] = block[o] | (block[o + 1] << 8) | (block[o + 2] << 16) | (block[o + 3] << 24);
    }
    let a = a0;
    let b = b0;
    let c = c0;
    let d = d0;
    for (let i = 0; i < 64; i++) {
      let f: number;
      let g: number;
      if (i < 16) {
        f = (b & c) | (~b & d);
        g = i;
      } else if (i < 32) {
        f = (d & b) | (~d & c);
        g = (5 * i + 1) % 16;
      } else if (i < 48) {
        f = b ^ c ^ d;
        g = (3 * i + 5) % 16;
      } else {
        f = c ^ (b | ~d);
        g = (7 * i) % 16;
      }
      const sum = (f + a + MD5_K[i] + m[g]) | 0;
      const rot = MD5_S[i];
      const tmp = d;
      d = c;
      c = b;
      b = (b + ((sum << rot) | (sum >>> (32 - rot)))) | 0;
      a = tmp;
    }
    a0 = (a0 + a) | 0;
    b0 = (b0 + b) | 0;
    c0 = (c0 + c) | 0;
    d0 = (d0 + d) | 0;
  }

  function update(data: Uint8Array): void {
    total += data.length;
    let pos = 0;
    if (buffered > 0) {
      const take = Math.min(64 - buffered, data.length);
      buf.set(data.subarray(0, take), buffered);
      buffered += take;
      pos = take;
      if (buffered === 64) {
        transform(buf, 0);
        buffered = 0;
      }
    }
    while (pos + 64 <= data.length) {
      transform(data, pos);
      pos += 64;
    }
    if (pos < data.length) {
      buf.set(data.subarray(pos), buffered);
      buffered += data.length - pos;
    }
  }

  function digestHex(): string {
    // Capture the message length BEFORE padding updates `total`.
    const bitLenLo = (total * 8) >>> 0;
    const bitLenHi = Math.floor(total / 536870912) >>> 0; // total * 8 / 2^32
    const padLen = buffered < 56 ? 56 - buffered : 120 - buffered;
    const pad = new Uint8Array(padLen);
    pad[0] = 0x80;
    update(pad);
    const lenBytes = new Uint8Array(8);
    lenBytes[0] = bitLenLo & 0xff;
    lenBytes[1] = (bitLenLo >>> 8) & 0xff;
    lenBytes[2] = (bitLenLo >>> 16) & 0xff;
    lenBytes[3] = (bitLenLo >>> 24) & 0xff;
    lenBytes[4] = bitLenHi & 0xff;
    lenBytes[5] = (bitLenHi >>> 8) & 0xff;
    lenBytes[6] = (bitLenHi >>> 16) & 0xff;
    lenBytes[7] = (bitLenHi >>> 24) & 0xff;
    update(lenBytes); // buffered is exactly 56, so this fills the final block
    function wordLE(n: number): string {
      let s = '';
      for (let i = 0; i < 4; i++) s += ((n >>> (8 * i)) & 0xff).toString(16).padStart(2, '0');
      return s;
    }
    return wordLE(a0) + wordLE(b0) + wordLE(c0) + wordLE(d0);
  }

  return { update, digestHex };
}

/** Synchronous one-shot MD5 of an ArrayBuffer, as lowercase hex. */
export function md5(data: ArrayBuffer): string {
  const hasher = createMD5();
  hasher.update(new Uint8Array(data));
  return hasher.digestHex();
}

// ---------------------------------------------------------------------------
// SHA-1 / SHA-256 / SHA-512 via Web Crypto
// ---------------------------------------------------------------------------

function getSubtle(): SubtleCrypto {
  const subtle = globalThis.crypto?.subtle;
  if (!subtle) {
    throw new Error('Web Crypto (crypto.subtle) is not available in this environment');
  }
  return subtle;
}

function toHex(bytes: Uint8Array): string {
  let s = '';
  for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, '0');
  return s;
}

/** Digest a buffer with Web Crypto and return lowercase hex. */
export async function shaHex(
  algorithm: 'SHA-1' | 'SHA-256' | 'SHA-512',
  data: ArrayBuffer | Uint8Array,
): Promise<string> {
  const view = data instanceof Uint8Array ? data : new Uint8Array(data);
  const digest = await getSubtle().digest(algorithm, view);
  return toHex(new Uint8Array(digest));
}

/** Compute all four checksums of a buffer (MD5 pure JS + 3 Web Crypto digests). */
export async function computeChecksums(data: ArrayBuffer): Promise<ChecksumResult> {
  const [sha1, sha256, sha512] = await Promise.all([
    shaHex('SHA-1', data),
    shaHex('SHA-256', data),
    shaHex('SHA-512', data),
  ]);
  return { md5: md5(data), sha1, sha256, sha512 };
}

// ---------------------------------------------------------------------------
// Verification helpers
// ---------------------------------------------------------------------------

const LENGTH_TO_ALGO: Record<string, HashAlgorithm> = {
  '32': 'md5',
  '40': 'sha1',
  '64': 'sha256',
  '128': 'sha512',
};

/** Normalize a pasted hash: drop whitespace and `:` grouping, lowercase. */
function normalizeHash(hash: string): string {
  return hash.trim().replace(/[\s:]/g, '').toLowerCase();
}

/** Detect the algorithm from the hex length: 32=MD5, 40=SHA-1, 64=SHA-256, 128=SHA-512. */
export function detectHashAlgorithm(hash: string): HashAlgorithm | null {
  const h = normalizeHash(hash);
  if (!/^[0-9a-f]+$/.test(h)) return null;
  return LENGTH_TO_ALGO[String(h.length)] ?? null;
}

/**
 * Compare an expected hash against a computed result. Returns null when the
 * expected string is not a recognizable hex hash of a supported length.
 */
export function verifyChecksum(
  expected: string,
  result: ChecksumResult,
): { algorithm: HashAlgorithm; match: boolean } | null {
  const algorithm = detectHashAlgorithm(expected);
  if (!algorithm) return null;
  return { algorithm, match: normalizeHash(expected) === result[algorithm] };
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →