Skip to content

实现 哈希计算器 工具

分类:developer | 标签:哈希、MD5、SHA、校验 使用浏览器原生 Web Crypto 计算 MD5 / SHA-1 / SHA-256 / SHA-512

2.1 功能说明

使用浏览器原生 Web Crypto 计算 MD5 / SHA-1 / SHA-256 / SHA-512

核心能力

  • 标准哈希
  • MD5
  • SHA-1
  • SHA-256
  • SHA-512
  • 扩展算法与比对
  • CRC32
  • SHA3-256
  • RIPEMD160
  • 全部
  • 算法
  • 哈希值

2.1.1 使用指南

哈希计算器使用指南

支持的算法

  • MD5:128 位,速度快,常用于文件完整性校验(注意:已不推荐用于安全场景)。
  • SHA-1:160 位,逐步被淘汰。
  • SHA-256 / SHA-512:密码学安全的摘要算法,推荐用于签名与校验。

使用说明

输入文本或选择文件,选择算法后实时计算。文本按 UTF-8 编码后取摘要;文件按原始字节计算。

扩展算法与比对

  • CRC32:32 位循环冗余校验(查表法,多项式 0xEDB88320),常见于 ZIP / PNG 等格式的完整性校验,不具备密码学安全性。
  • SHA3-256:基于 Keccak 海绵结构的新一代标准摘要(FIPS 202),与 SHA-2 的设计完全不同,抗长度扩展攻击。
  • RIPEMD160:160 位双路并行摘要算法,比特币地址生成中广泛使用。

选择「全部」可一次性列出三种结果。在「预期哈希值」中粘贴对方提供的校验值后, 表格会自动标记 匹配 / 不匹配;比较时忽略大小写、空格与常见的 sha256: 之类前缀,也可直接粘贴 <哈希> <文件名> 格式的校验行。

隐私说明

所有计算在浏览器本地完成,不上传任何数据。MD5、CRC32、SHA3-256、RIPEMD160 均由本项目内置纯 JS 实现, SHA-1/256/512 使用浏览器原生 Web Crypto。

2.2 代码实现

2.2.1 组件结构

本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-inputel-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。

2.2.2 核心逻辑概览

<script setup> 中定义的主要函数/方法:

  • md5()
  • rotl()
  • sha()
  • bytesFromText()
  • process()
  • handleFile()
  • clearAll()
  • crcTable()
  • crc32()
  • rotl64()
  • keccakF()
  • sha3_256()
  • rmdF()
  • ripemd160()
  • currentExtBytes()
  • processExt()
  • onExtTextInput()
  • handleExtFile()
  • clearExtFile()
  • clearExt()
  • copy()

2.2.3 关键实现代码

以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。

vue
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import ToolPageShell from '@/components/ToolPageShell.vue'

const activeTab = ref('standard')

const algo = ref('SHA-256')
const input = ref('')
const output = ref('')
const error = ref('')
let fileBytes = null

/* ---------------- MD5(内置实现,按字节计算) ---------------- */
const MD5_K = [
  0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
  0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
  0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
  0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
  0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
  0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
  0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
  0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
]
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
]

function md5(bytes) {
  const rotl = (x, c) => (x << c) | (x >>> (32 - c))
  const bitLen = bytes.length * 8
  const total = (bytes.length + 1 + 8 + 63) & ~63
  const msg = new Uint8Array(total)
  msg.set(bytes)
  msg[bytes.length] = 0x80
  const dv = new DataView(msg.buffer)
  dv.setUint32(total - 8, bitLen >>> 0, true)
  dv.setUint32(total - 4, Math.floor(bitLen / 0x100000000), true)

  let a0 = 0x67452301, b0 = 0xefcdab89, c0 = 0x98badcfe, d0 = 0x10325476
  const M = new Int32Array(16)
  for (let off = 0; off < total; off += 64) {
    for (let i = 0; i < 16; i++) M[i] = dv.getInt32(off + i * 4, true)
    let A = a0, B = b0, C = c0, D = d0
    for (let i = 0; i < 64; i++) {
      let f, g
      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 t = (B + rotl((A + f + MD5_K[i] + M[g]) | 0, MD5_S[i])) | 0
      A = D; D = C; C = B; B = t
    }
    a0 = (a0 + A) | 0; b0 = (b0 + B) | 0; c0 = (c0 + C) | 0; d0 = (d0 + D) | 0
  }
  const out = new Uint8Array(16)
  const odv = new DataView(out.buffer)
  ;[a0, b0, c0, d0].forEach((v, idx) => odv.setInt32(idx * 4, v, true))
  return [...out].map(b => b.toString(16).padStart(2, '0')).join('')
}

/* ---------------- Web Crypto SHA ---------------- */
async function sha(alg, bytes) {
  const buf = await crypto.subtle.digest(alg, bytes)
  return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('')
}

function bytesFromText(str) {
  return new TextEncoder().encode(str)
}

async function process() {
  error.value = ''
  output.value = ''
  if (fileBytes) {
    output.value = algo.value === 'MD5' ? md5(fileBytes) : await sha(algo.value, fileBytes)
    return
  }
  if (!input.value) return
  const bytes = bytesFromText(input.value)
  if (algo.value === 'MD5') {
    output.value = md5(bytes)
  } else {
    output.value = await sha(algo.value, bytes)
  }
}

function handleFile(file) {
  const reader = new FileReader()
  reader.onload = async () => {
    fileBytes = new Uint8Array(reader.result)
    await process()
  }
  reader.onerror = () => ElMessage.error('文件读取失败')
  reader.readAsArrayBuffer(file.raw)
}

function clearAll() {
  input.value = ''
  output.value = ''
  error.value = ''
  fileBytes = null
}

/* ================= 扩展算法:CRC32 / SHA3-256 / RIPEMD160 ================= */

/* ---------------- CRC32(查表法,多项式 0xEDB88320) ---------------- */
let CRC_TABLE = null
function crcTable() {
  if (CRC_TABLE) return CRC_TABLE
  const t = new Uint32Array(256)
  for (let n = 0; n < 256; n++) {
    let c = n
    for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1
    t[n] = c >>> 0
  }
  CRC_TABLE = t
  return t
}
function crc32(bytes) {
  const t = crcTable()
  let c = 0xffffffff
  for (let i = 0; i < bytes.length; i++) c = t[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)
  return ((c ^ 0xffffffff) >>> 0).toString(16).padStart(8, '0')
}

/* ---------------- SHA3-256(Keccak-f[1600] 海绵结构,FIPS 202) ---------------- */
const MASK64 = (1n << 64n) - 1n
const KECCAK_RC = [
  0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
  0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
  0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
  0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
  0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
  0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n
]
// ρ 旋转位移表,索引为 x + 5*y
const KECCAK_ROT = [
  0, 1, 62, 28, 27,
  36, 44, 6, 55, 20,
  3, 10, 43, 25, 39,
  41, 45, 15, 21, 8,
  18, 2, 61, 56, 14
]
function rotl64(x, n) {
  if (n === 0) return x
  const b = BigInt(n)
  return ((x << b) | (x >> (64n - b))) & MASK64
}
function keccakF(A) {
  const C = new Array(5)
  const D = new Array(5)
  const B = new Array(25)
  for (let round = 0; round < 24; round++) {
    // θ
    for (let x = 0; x < 5; x++) C[x] = A[x] ^ A[x + 5] ^ A[x + 10] ^ A[x + 15] ^ A[x + 20]
    for (let x = 0; x < 5; x++) D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1)
    for (let y = 0; y < 5; y++) for (let x = 0; x < 5; x++) A[x + 5 * y] ^= D[x]
    // ρ + π
    for (let y = 0; y < 5; y++) {
      for (let x = 0; x < 5; x++) {
        B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(A[x + 5 * y], KECCAK_ROT[x + 5 * y])
      }
    }
    // χ
    for (let y = 0; y < 5; y++) {
      for (let x = 0; x < 5; x++) {
        A[x + 5 * y] = B[x + 5 * y] ^ (~B[((x + 1) % 5) + 5 * y] & MASK64 & B[((x + 2) % 5) + 5 * y])
      }
    }
    // ι
    A[0] ^= KECCAK_RC[round]
  }
}
function sha3_256(bytes) {
  const rate = 136 // (1600 - 2*256) / 8
  const A = new Array(25).fill(0n)
  const padLen = rate - (bytes.length % rate)
  const total = bytes.length + padLen
  const msg = new Uint8Array(total)
  msg.set(bytes)
  msg[bytes.length] = 0x06 // SHA-3 域分隔符 + pad10*1 起始位
  msg[total - 1] |= 0x80
  for (let off = 0; off < total; off += rate) {
    for (let i = 0; i < rate / 8; i++) {
      let lane = 0n
      for (let j = 7; j >= 0; j--) lane = (lane << 8n) | BigInt(msg[off + i * 8 + j])
      A[i] ^= lane
    }
    keccakF(A)
  }
  let hex = ''
  for (let i = 0; i < 4; i++) {
    let lane = A[i]
    for (let j = 0; j < 8; j++) {
      hex += Number(lane & 0xffn).toString(16).padStart(2, '0')
      lane >>= 8n
    }
  }
  return hex
}

/* ---------------- RIPEMD-160(双路并行压缩) ---------------- */
const RMD_ZL = [
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
  7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8,
  3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12,
  1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2,
  4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13
]
const RMD_ZR = [
  5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12,
  6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2,
  15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13,
  8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14,
  12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11
]
const RMD_SL = [
  11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8,
  7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12,
  11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5,
  11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12,
  9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6
]
const RMD_SR = [
  8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6,
  9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11,
  9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5,
  15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8,
  8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11
]
const RMD_KL = [0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e]
const RMD_KR = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000]
function rmdF(j, x, y, z) {
  if (j < 16) return x ^ y ^ z
  if (j < 32) return (x & y) | (~x & z)
  if (j < 48) return (x | ~y) ^ z
  if (j < 64) return (x & z) | (y & ~z)
  return x ^ (y | ~z)
}
function ripemd160(bytes) {
  const rotl = (x, c) => (x << c) | (x >>> (32 - c))
  const bitLen = bytes.length * 8
  const total = (bytes.length + 1 + 8 + 63) & ~63
  const msg = new Uint8Array(total)
  msg.set(bytes)
  msg[bytes.length] = 0x80
  const dv = new DataView(msg.buffer)
  dv.setUint32(total - 8, bitLen >>> 0, true)
  dv.setUint32(total - 4, Math.floor(bitLen / 0x100000000), true)

  let h0 = 0x67452301, h1 = 0xefcdab89, h2 = 0x98badcfe, h3 = 0x10325476, h4 = 0xc3d2e1f0
  const X = new Int32Array(16)
  for (let off = 0; off < total; off += 64) {
    for (let i = 0; i < 16; i++) X[i] = dv.getInt32(off + i * 4, true)
    let al = h0, bl = h1, cl = h2, dl = h3, el = h4
    let ar = h0, br = h1, cr = h2, dr = h3, er = h4
    for (let j = 0; j < 80; j++) {
      const rnd = (j / 16) | 0
      let t = (al + rmdF(j, bl, cl, dl) + X[RMD_ZL[j]] + RMD_KL[rnd]) | 0
      t = (rotl(t, RMD_SL[j]) + el) | 0
      al = el; el = dl; dl = rotl(cl, 10); cl = bl; bl = t
      t = (ar + rmdF(79 - j, br, cr, dr) + X[RMD_ZR[j]] + RMD_KR[rnd]) | 0
      t = (rotl(t, RMD_SR[j]) + er) | 0
      ar = er; er = dr; dr = rotl(cr, 10); cr = br; br = t
    }
    const t = (h1 + cl + dr) | 0
    h1 = (h2 + dl + er) | 0
    h2 = (h3 + el + ar) | 0
    h3 = (h4 + al + br) | 0
    h4 = (h0 + bl + cr) | 0
    h0 = t
  }
  const out = new Uint8Array(20)
  const odv = new DataView(out.buffer)
  ;[h0, h1, h2, h3, h4].forEach((v, i) => odv.setInt32(i * 4, v, true))
  return [...out].map(b => b.toString(16).padStart(2, '0')).join('')
}

/* ---------------- 扩展算法 tab 的状态与交互 ---------------- */
const EXT_ALGOS = {
  CRC32: crc32,
  'SHA3-256': sha3_256,
  RIPEMD160: ripemd160
}

const extAlgo = ref('SHA3-256')
const extInput = ref('')
const extError = ref('')
const extResults = ref([])
const expected = ref('')
const extFileName = ref('')
const extFileSize = ref(0)
let extFileBytes = null

// 归一化预期值:去空白、转小写、剥离 "sha256:" 之类前缀与 "<hash>  <文件名>" 后缀
const normalizedExpected = computed(() => {
  let v = expected.value.trim()
  if (!v) return ''
  const firstToken = v.split(/[\s*]+/).filter(Boolean)[0] || ''
  v = firstToken
  const colon = v.lastIndexOf(':')
  if (colon !== -1) v = v.slice(colon + 1)
  return v.toLowerCase()
})

const anyMatch = computed(() =>
  extResults.value.some(r => r.value.toLowerCase() === normalizedExpected.value)
)

const extResultText = computed(() =>
  extResults.value.map(r => `${r.name}: ${r.value}`).join('\n')
)

function currentExtBytes() {
  if (extFileBytes) return extFileBytes
  if (!extInput.value) return null
  return bytesFromText(extInput.value)
}

function processExt() {
  extError.value = ''
  extResults.value = []
  const bytes = currentExtBytes()
  if (!bytes) return
  try {
    const names = extAlgo.value === '全部' ? Object.keys(EXT_ALGOS) : [extAlgo.value]
    extResults.value = names.map(name => ({ name, value: EXT_ALGOS[name](bytes) }))
  } catch (e) {
    extError.value = '计算失败:' + e.message
  }
}

function onExtTextInput() {
  // 文本输入时以文本为准,自动放弃已选文件
  if (extFileBytes) clearExtFile()
  else processExt()
}

function handleExtFile(file) {
  const reader = new FileReader()
  reader.onload = () => {
    extFileBytes = new Uint8Array(reader.result)
    extFileName.value = file.name
    extFileSize.value = extFileBytes.length
    processExt()
  }
  reader.onerror = () => ElMessage.error('文件读取失败')
  reader.readAsArrayBuffer(file.raw)
}

function clearExtFile() {
  extFileBytes = null
  extFileName.value = ''
  extFileSize.value = 0
  processExt()
}

function clearExt() {
  extInput.value = ''
  expected.value = ''
  extError.value = ''
  extResults.value = []
  clearExtFile()
}

async function copy(text) {
  try {
    await navigator.clipboard.writeText(text)
    ElMessage.success('已复制')
  } catch {
    ElMessage.warning('复制失败,请手动复制')
  }
}

2.3 效果截图

哈希计算器 效果截图

工具访问地址:https://www.i91tools.com/tools/hash-calculator