|
#!/usr/bin/env bash |
|
# |
|
# basha256.sh - sha256 in pure Bash 3.2 |
|
# |
|
# ./basha256.sh < bigfile.bin |
|
# |
|
# Everything below: the message schedule, the compression function and even |
|
# reading the raw bytes off stdin is done with nothing but Bash builtins and arithmetic. |
|
# |
|
# TIL: Bash cannot store a NUL byte in a variable, so stdin is read in NUL-delimited chunks and the delimiters are re-inserted by hand. |
|
# LC_ALL=C makes every byte a single "character" so slicing works. |
|
|
|
# the 512-bit block compression function |
|
# Args: the sixteen big-endian 32-bit words of one block. |
|
# Reads/updates the eight hash words _H0.._H7 of the calling scope. |
|
_sha256_compress() { |
|
local M=4294967295 # 0xffffffff, the 32-bit mask |
|
local -a w=( "$@" ) # w[0..15] = this block's words |
|
local -a K=( |
|
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 |
|
) |
|
local i x y s0 s1 S0 S1 ch maj t1 t2 a b c d e f g h |
|
|
|
# Extend the sixteen words into sixty-four. rotr(v,n) = (v>>n)|(v<<32-n), |
|
# masked to 32 bits after the XORs. |
|
for (( i=16; i<64; i++ )); do |
|
x=${w[i-15]}; y=${w[i-2]} |
|
s0=$(( ( ((x>>7)|(x<<25)) ^ ((x>>18)|(x<<14)) ^ (x>>3) ) & M )) |
|
s1=$(( ( ((y>>17)|(y<<15)) ^ ((y>>19)|(y<<13)) ^ (y>>10) ) & M )) |
|
w[i]=$(( (w[i-16] + s0 + w[i-7] + s1) & M )) |
|
done |
|
|
|
a=$_H0; b=$_H1; c=$_H2; d=$_H3; e=$_H4; f=$_H5; g=$_H6; h=$_H7 |
|
|
|
# this is pretty and was fun to debug: |
|
for (( i=0; i<64; i++ )); do |
|
S1=$(( ( ((e>>6)|(e<<26)) ^ ((e>>11)|(e<<21)) ^ ((e>>25)|(e<<7)) ) & M )) |
|
ch=$(( (e & f) ^ ((~e) & g) )) |
|
t1=$(( (h + S1 + ch + K[i] + w[i]) & M )) |
|
S0=$(( ( ((a>>2)|(a<<30)) ^ ((a>>13)|(a<<19)) ^ ((a>>22)|(a<<10)) ) & M )) |
|
maj=$(( (a & b) ^ (a & c) ^ (b & c) )) |
|
t2=$(( (S0 + maj) & M )) |
|
h=$g; g=$f; f=$e; e=$(( (d + t1) & M )); d=$c; c=$b; b=$a; a=$(( (t1 + t2) & M )) |
|
done |
|
|
|
_H0=$(( (_H0 + a) & M )); _H1=$(( (_H1 + b) & M )) |
|
_H2=$(( (_H2 + c) & M )); _H3=$(( (_H3 + d) & M )) |
|
_H4=$(( (_H4 + e) & M )); _H5=$(( (_H5 + f) & M )) |
|
_H6=$(( (_H6 + g) & M )); _H7=$(( (_H7 + h) & M )) |
|
} |
|
|
|
# bytes accumulate in _BLK[0.._BN-1]. once 64 are buffered we compress. |
|
_ba_flush() { |
|
local -a w |
|
local j base |
|
for (( j=0; j<16; j++ )); do |
|
base=$(( j*4 )) |
|
w[j]=$(( (_BLK[base]<<24) | (_BLK[base+1]<<16) | (_BLK[base+2]<<8) | _BLK[base+3] )) |
|
done |
|
_sha256_compress "${w[@]}" |
|
_BN=0 |
|
} |
|
|
|
# $1 = byte value 0..255 |
|
_ba_addbyte() { |
|
_BLK[_BN]=$1 |
|
_BN=$(( _BN + 1 )) |
|
if (( _BN == 64 )); then _ba_flush; fi |
|
} |
|
|
|
# add every byte of the string $1 |
|
_ba_addchunk() { |
|
local s=$1 n=${#1} j v |
|
total=$(( total + n )) |
|
for (( j=0; j<n; j++ )); do |
|
printf -v v '%d' "'${s:j:1}" # numeric value of one byte |
|
_BLK[_BN]=$(( v & 0xff )) # mask: some printfs sign-extend |
|
_BN=$(( _BN + 1 )) |
|
if (( _BN == 64 )); then _ba_flush; fi |
|
|
|
done |
|
} |
|
|
|
# the public function |
|
basha256() { |
|
# treat the stream as raw bytes |
|
local LC_ALL=C LC_CTYPE=C |
|
local _H0=0x6a09e667 _H1=0xbb67ae85 _H2=0x3c6ef372 _H3=0xa54ff53a |
|
local _H4=0x510e527f _H5=0x9b05688c _H6=0x1f83d9ab _H7=0x5be0cd19 |
|
local -a _BLK=() |
|
local _BN=0 total=0 chunk bits k |
|
|
|
# Read stdin in NUL-delimited chunks, re-inserting each NUL we split on. |
|
while IFS= read -r -d '' chunk; do |
|
_ba_addchunk "$chunk" |
|
total=$(( total + 1 )); _ba_addbyte 0 |
|
done |
|
_ba_addchunk "$chunk" # trailing bytes after the last NUL |
|
|
|
# Padding: 0x80, then zeros until 56 bytes into the block... |
|
_ba_addbyte 128 |
|
while (( _BN != 56 )); do _ba_addbyte 0; done |
|
# ...then the 64-bit big-endian message length in bits. |
|
bits=$(( total * 8 )) |
|
for (( k=7; k>=0; k-- )); do _ba_addbyte $(( (bits >> (8*k)) & 0xff )); done |
|
|
|
printf '%08x%08x%08x%08x%08x%08x%08x%08x -\n' \ |
|
"$((_H0))" "$((_H1))" "$((_H2))" "$((_H3))" \ |
|
"$((_H4))" "$((_H5))" "$((_H6))" "$((_H7))" |
|
} |
|
|
|
# call when executed directly (./basha256.sh < file) |
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then |
|
basha256 |
|
fi |