Appearance
实现 哈希计算器 工具
分类: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-input、el-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 效果截图
