diff --git a/manual.pdf b/manual.pdf index f7ad06a..8a4192d 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/manual.typ b/manual.typ index e052c0a..c1668d3 100644 --- a/manual.typ +++ b/manual.typ @@ -13,6 +13,12 @@ ) #tidy.show-module(sha-doc) +#let md-doc = mod( + read("src/md.typ"), + name: "md" +) +#tidy.show-module(md-doc) + #let misc-doc = mod( read("src/misc.typ"), name: "misc" diff --git a/src/lib.typ b/src/lib.typ index 2a16f0b..53a1165 100644 --- a/src/lib.typ +++ b/src/lib.typ @@ -1,4 +1,5 @@ #import "misc.typ": * -#import "sha.typ" +#import "sha.typ": * +#import "md.typ": * #import "utils.typ": * #import "base.typ": * \ No newline at end of file diff --git a/src/md.typ b/src/md.typ new file mode 100644 index 0000000..f1ffb48 --- /dev/null +++ b/src/md.typ @@ -0,0 +1,259 @@ +#import "utils.typ": * + +#let rots = ( + 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 +) + +#let _md5-const(i) = { + return int( + calc.floor( + calc.abs( + calc.sin(i + 1) * max-32 + ) + ) + ) +} + +#let md5-default-iv = ( + 0x67452301, + 0xEFCDAB89, + 0x98BADCFE, + 0x10325476 +) +#let md4-default-iv = md5-default-iv + +/// Message Digest 5 +/// #test( +/// `jumble.bytes-to-hex(jumble.md5("Hello World!")) == "ed076287532e86365e841e92bfc50d8c"` +/// ) +/// ```example +/// #bytes-to-hex(md5("Hello World!")) +/// ``` +/// -> bytes +#let md5( + message, + iv: md5-default-iv +) = { + let message = if type(message) == str {bytes(message)} else {message} + assert(type(message) == bytes, message: "message must be a string or bytes, but is " + repr(type(message))) + + // Complete message to multiple of 512 bits + let bin-str = "" + for char in message { + bin-str += z-fill(str(char, base: 2), 8) + } + let l = bin-str.len() + bin-str += "1" + let padding = calc.rem-euclid(448 - bin-str.len(), 512) + if padding != 0 { + bin-str += "0" * padding + } + let len-bin = z-fill(str(l, base: 2), 64) + // Reverse order to little-endian + len-bin = len-bin.clusters() + .chunks(8) + .rev() + .flatten() + .join() + bin-str += len-bin + let bin = bin-str.clusters().map(int) + + // Split into blocks of 16 32-bit words + let words = bin.chunks(32).map(bin-to-int).map(switch-endianness) + let blocks = words.chunks(16) + + let vec = iv + + for block in blocks { + let (A, B, C, D) = vec + for i in range(64) { + let (f, g) = if i < 16 {( + B.bit-and(C).bit-or(B.bit-not().bit-and(mask-32).bit-and(D)), + i + )} else if i < 32 {( + D.bit-and(B).bit-or(D.bit-not().bit-and(mask-32).bit-and(C)), + calc.rem(5 * i + 1, 16) + )} else if i < 48 {( + B.bit-xor(C).bit-xor(D), + calc.rem(3 * i + 5, 16) + )} else {( + C.bit-xor(B.bit-or(D.bit-not().bit-and(mask-32))), + calc.rem(7 * i, 16) + )} + let temp = circular-shift( + calc.rem(A + f + _md5-const(i) + block.at(g), max-32), + n: rots.at(i) + ) + B + (A, B, C, D) = ( + D, + calc.rem(temp, max-32), + B, + C + ) + } + + vec = vec.zip((A, B, C, D)).map(p => { + calc.rem(p.sum(), max-32) + }) + } + + let digest-bytes = () + for n in vec { + digest-bytes += ( + n.bit-and(0xff), + n.bit-rshift(8).bit-and(0xff), + n.bit-rshift(16).bit-and(0xff), + n.bit-rshift(24) + ) + } + + return bytes(digest-bytes) +} + +#let _md4-round1-shift = (3, 7, 11, 19) +#let _md4-round2-shift = (3, 5, 9, 13) +#let _md4-round3-shift = (3, 9, 11, 15) + +#let _md4-f(x, y, z) = { + return x.bit-and(y).bit-or(x.bit-not().bit-and(mask-32).bit-and(z)) +} + +#let _md4-g(x, y, z) = { + let xy = x.bit-and(y) + let xz = x.bit-and(z) + let yz = y.bit-and(z) + return xy.bit-or(xz).bit-or(yz) +} + +#let _md4-h(x, y, z) = { + return x.bit-xor(y).bit-xor(z) +} + +#let _md4-round1(a, b, c, d, x, s) = { + let temp = a + _md4-f(b, c, d) + x + return circular-shift( + temp.bit-and(mask-32), + n: s + ) +} + +#let _md4-round2(a, b, c, d, x, s) = { + let temp = a + _md4-g(b, c, d) + x + 0x5a827999 + return circular-shift( + temp.bit-and(mask-32), + n: s + ) +} + +#let _md4-round3(a, b, c, d, x, s) = { + let temp = a + _md4-h(b, c, d) + x + 0x6ed9eba1 + return circular-shift( + temp.bit-and(mask-32), + n: s + ) +} + +/// Message Digest 4 +/// #test( +/// `jumble.bytes-to-hex(jumble.md4("Hello World!")) == "b2a5cc34fc21a764ae2fad94d56fadf6"` +/// ) +/// ```example +/// #bytes-to-hex(md4("Hello World!")) +/// ``` +/// -> bytes +#let md4( + /// -> str | bytes + message, + /// -> array + iv: md4-default-iv +) = { + let message = if type(message) == str {bytes(message)} else {message} + assert(type(message) == bytes, message: "message must be a string or bytes, but is " + repr(type(message))) + + // Complete message to multiple of 512 bits + let bin-str = "" + for char in message { + bin-str += z-fill(str(char, base: 2), 8) + } + let l = bin-str.len() + bin-str += "1" + let padding = calc.rem-euclid(448 - bin-str.len(), 512) + if padding != 0 { + bin-str += "0" * padding + } + let len-bin = z-fill(str(l, base: 2), 64) + // Reverse order to little-endian + len-bin = len-bin.clusters() + .chunks(8) + .rev() + .flatten() + .join() + bin-str += len-bin + let bin = bin-str.clusters().map(int) + + // Split into blocks of 16 32-bit words + let words = bin.chunks(32).map(bin-to-int).map(switch-endianness) + let blocks = words.chunks(16) + + let vec = iv + + for block in blocks { + let vec2 = vec + + for i in range(16) { + vec2.at(0) = _md4-round1( + ..vec2, + block.at(i), + _md4-round1-shift.at(calc.rem(i, 4)) + ) + vec2.insert(0, vec2.pop()) + } + + for i in range(16) { + let j = calc.rem(i * 4, 16) + calc.div-euclid(i, 4) + vec2.at(0) = _md4-round2( + ..vec2, + block.at(j), + _md4-round2-shift.at(calc.rem(i, 4)) + ) + vec2.insert(0, vec2.pop()) + } + + for i in range(16) { + let j = bin-to-int( + z-fill( + str(i, base: 2), + 4 + ).clusters() + .map(int) + .rev() + ) + + vec2.at(0) = _md4-round3( + ..vec2, + block.at(j), + _md4-round3-shift.at(calc.rem(i, 4)) + ) + vec2.insert(0, vec2.pop()) + } + + vec = vec.zip(vec2).map(p => { + calc.rem(p.sum(), max-32) + }) + } + + let digest-bytes = () + for n in vec { + digest-bytes += ( + n.bit-and(0xff), + n.bit-rshift(8).bit-and(0xff), + n.bit-rshift(16).bit-and(0xff), + n.bit-rshift(24), + ) + } + + return bytes(digest-bytes) +} \ No newline at end of file diff --git a/src/sha.typ b/src/sha.typ index ec09ff4..dd2b9f0 100644 --- a/src/sha.typ +++ b/src/sha.typ @@ -42,15 +42,18 @@ /// -> bytes #let sha1( /// Message to hash - /// -> str + /// -> str | bytes message, /// Initial vector /// -> array iv: sha1-default-iv ) = { + let message = if type(message) == str {bytes(message)} else {message} + assert(type(message) == bytes, message: "message must be a string or bytes, but is " + repr(type(message))) + // Complete message to multiple of 512 bits let bin-str = "" - for char in bytes(message) { + for char in message { bin-str += z-fill(str(char, base: 2), 8) } let l = bin-str.len() diff --git a/src/utils.typ b/src/utils.typ index 3b7cd2f..0f05eda 100644 --- a/src/utils.typ +++ b/src/utils.typ @@ -97,4 +97,23 @@ } let high-bits = x.bit-rshift(32 - n) return x.bit-lshift(n).bit-or(high-bits).bit-and(mask-32) +} + +/// Switches the endianness of the given value (32-bit integer) +/// -> number +#let switch-endianness( + /// -> number + value +) = { + let a = value.bit-rshift(24) + let b = value.bit-rshift(16).bit-and(0xff) + let c = value.bit-rshift(8).bit-and(0xff) + let d = value.bit-and(0xff) + + let a2 = d.bit-lshift(24) + let b2 = c.bit-lshift(16) + let c2 = b.bit-lshift(8) + let d2 = a + + return a2.bit-or(b2).bit-or(c2).bit-or(d2) } \ No newline at end of file