commit 226a728e82f73c65db7c8e0dd13b4c7cfe409439 Author: LordBaryhobal Date: Sat Dec 21 14:17:09 2024 +0100 initial commit (b32, sha1, hmac) diff --git a/manual.pdf b/manual.pdf new file mode 100644 index 0000000..f7ad06a Binary files /dev/null and b/manual.pdf differ diff --git a/manual.typ b/manual.typ new file mode 100644 index 0000000..e052c0a --- /dev/null +++ b/manual.typ @@ -0,0 +1,34 @@ +#import "@preview/tidy:0.4.0" + +#import "src/lib.typ" + +#let mod = tidy.parse-module.with( + scope: (jumble: lib), + preamble: "#import jumble: *;" +) + +#let sha-doc = mod( + read("src/sha.typ"), + name: "sha" +) +#tidy.show-module(sha-doc) + +#let misc-doc = mod( + read("src/misc.typ"), + name: "misc" +) +#tidy.show-module(misc-doc) + +#let base-doc = mod( + read("src/base.typ"), + name: "base" +) +#tidy.show-module(base-doc) + +#pagebreak() + +#let utils-doc = mod( + read("src/utils.typ"), + name: "utils" +) +#tidy.show-module(utils-doc) \ No newline at end of file diff --git a/src/base.typ b/src/base.typ new file mode 100644 index 0000000..8aaedf2 --- /dev/null +++ b/src/base.typ @@ -0,0 +1,62 @@ +#import "utils.typ": z-fill, bin-to-int +#let b32-alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + +/// Decodes a base32-encoded value +/// #test( +/// `jumble.b32-decode("LFHVKUCJ") == bytes("YOUPI")` +/// ) +/// ```example +/// #str(b32-decode("LFHVKUCJ")) +/// ``` +/// -> bytes +#let b32-decode( + /// -> str + encoded +) = { + let decoded-bin = () + for char in encoded { + if char == "=" { + break + } + let i = b32-alphabet.position(char) + decoded-bin += z-fill(str(i, base: 2), 5).clusters().map(int) + } + decoded-bin = decoded-bin.slice( + 0, + calc.div-euclid(decoded-bin.len(), 8) * 8 + ) + let decoded-bytes = decoded-bin.chunks(8).map(bin-to-int) + return bytes(decoded-bytes) +} + +/// Encodes a value in base32 +/// #test( +/// `jumble.b32-encode(bytes("YOUPI")) == "LFHVKUCJ"` +/// ) +/// ```example +/// #b32-encode(bytes("YOUPI")) +/// ``` +/// -> str +#let b32-encode( + /// -> bytes + decoded +) = { + let encoded = "" + let decoded-bin = array(decoded).map(b => z-fill(str(b, base: 2), 8)).join() + decoded-bin = decoded-bin.clusters().map(int) + let groups = decoded-bin.chunks(40) + for group in groups { + let chars = group.chunks(5) + if chars.last().len() != 5 { + chars.last() += (0,) * (5 - chars.last().len()) + } + let chars = chars.map(bin-to-int) + .map(c => b32-alphabet.at(c)) + encoded += chars.join() + let pad = 8 - chars.len() + if pad != 0 { + encoded += "=" * pad + } + } + return encoded +} \ No newline at end of file diff --git a/src/lib.typ b/src/lib.typ new file mode 100644 index 0000000..2a16f0b --- /dev/null +++ b/src/lib.typ @@ -0,0 +1,4 @@ +#import "misc.typ": * +#import "sha.typ" +#import "utils.typ": * +#import "base.typ": * \ No newline at end of file diff --git a/src/misc.typ b/src/misc.typ new file mode 100644 index 0000000..e38a3c3 --- /dev/null +++ b/src/misc.typ @@ -0,0 +1,48 @@ +#import "utils.typ": xor-bytes +#import "sha.typ": sha1 + +#let _compute-block-sized-key(key, hash-func: sha1, block-size: 64) = { + if key.len() > block-size { + key = hash-func(key) + } + + if key.len() < block-size { + key = bytes((0,) * (block-size - key.len())) + key + } + + return key +} + +/// Hash-based Message Authentication Code +/// ```example +/// #bytes-to-hex(hmac("Key", "Hello World!")) +/// ``` +/// -> bytes +#let hmac( + /// Hashing key + /// -> str | bytes + key, + /// Message to hash + /// -> str | bytes + message, + /// Hashing function + /// -> function + hash-func: sha1, + /// Block size + /// -> number + block-size: 64 +) = { + let key = if type(key) == str {bytes(key)} else {key} + let message = if type(message) == str {bytes(message)} else {message} + assert(type(key) == bytes, message: "key must be a string or bytes, but is " + repr(type(key))) + assert(type(message) == bytes, message: "message must be a string or bytes, but is " + repr(type(message))) + + let block-sized-key = _compute-block-sized-key(key, hash-func: hash-func, block-size: block-size) + + let i-pad = bytes((0x36,) * block-size) + let o-pad = bytes((0x5c,) * block-size) + let i-key-pad = xor-bytes(key, i-pad) + let o-key-pad = xor-bytes(key, o-pad) + + return hash-func(o-key-pad + hash-func(i-key-pad + message)) +} \ No newline at end of file diff --git a/src/sha.typ b/src/sha.typ new file mode 100644 index 0000000..ec09ff4 --- /dev/null +++ b/src/sha.typ @@ -0,0 +1,115 @@ +#import "utils.typ": * + +#let sha1-default-iv = ( + 0x67452301, + 0xefcdab89, + 0x98badcfe, + 0x10325476, + 0xc3d2e1f0 +) + +#let _sha1-const(i) = { + if i < 20 { + return 0x5a827999 + } else if i < 40 { + return 0x6ed9eba1 + } else if i < 60 { + return 0x8f1bbcdc + } else { + return 0xca62c1d6 + } +} + +#let _sha1-func(i, b, c, d) = { + if i < 20 { + return b.bit-and(c).bit-or(b.bit-not().bit-and(mask-32).bit-and(d)) + } else if i < 40 { + return b.bit-xor(c).bit-xor(d) + } else if i < 60 { + let bc = b.bit-and(c) + let bd = b.bit-and(d) + let cd = c.bit-and(d) + return bc.bit-or(bd).bit-or(cd) + } else { + return b.bit-xor(c).bit-xor(d) + } +} + +/// Secure Hash Algorithm 1 +/// ```example +/// #bytes-to-hex(sha1("Hello World!")) +/// ``` +/// -> bytes +#let sha1( + /// Message to hash + /// -> str + message, + /// Initial vector + /// -> array + iv: sha1-default-iv +) = { + // Complete message to multiple of 512 bits + let bin-str = "" + for char in bytes(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 + } + bin-str += z-fill(str(l, base: 2), 64) + let bin = bin-str.clusters().map(int) + + // Split into blocks of 16 32-bit words + let words = bin.chunks(32).map(bin-to-int) + let blocks = words.chunks(16) + + let vec = iv + + for block in blocks { + // Expand + for i in range(16, 80) { + let chosen-words = ( + block.at(i - 3), + block.at(i - 8), + block.at(i - 14), + block.at(i - 16) + ) + let word = circular-shift( + chosen-words.fold(0, (a, b) => a.bit-xor(b)) + ) + block.push(word) + } + + // Compress + let (A, B, C, D, E) = vec + for i in range(80) { + let temp = ( + circular-shift(A, n: 5) + + _sha1-func(i, B, C, D) + + E + + block.at(i) + + _sha1-const(i) + ) + temp = calc.rem(temp, max-32) + (A, B, C, D, E) = (temp, A, circular-shift(B, n: 30), C, D) + } + vec = vec.zip((A, B, C, D, E)).map(p => { + calc.rem(p.sum(), max-32) + }) + } + + let digest-bytes = () + for n in vec { + digest-bytes += ( + n.bit-rshift(24), + n.bit-rshift(16).bit-and(0xff), + n.bit-rshift(8).bit-and(0xff), + n.bit-and(0xff) + ) + } + + return bytes(digest-bytes) +} \ No newline at end of file diff --git a/src/utils.typ b/src/utils.typ new file mode 100644 index 0000000..3b7cd2f --- /dev/null +++ b/src/utils.typ @@ -0,0 +1,100 @@ +/// Applies the XOR operation between two byte arrays +/// ```example +/// #let a = bytes((0b010, 0b0111)) +/// #let b = bytes((0b011, 0b0101)) +/// #array(xor-bytes(a, b)).map( +/// b => z-fill(str(b, base: 2), 3) +/// ) +/// ``` +/// -> bytes +#let xor-bytes( + /// First byte array + /// -> bytes + bytes-a, + /// Second byte array + /// -> bytes + bytes-b +) = { + let length = calc.max(bytes-a.len(), bytes-b.len()) + let bytes-c = () + for i in range(length) { + bytes-c.push( + bytes-a.at(i, default: 0).bit-xor(bytes-b.at(i, default: 0)) + ) + } + return bytes(bytes-c) +} + +/// Pads a string with 0s on the left to reach a certain length +/// ```example +/// #z-fill("1011", 8) +/// ``` +/// -> str +#let z-fill( + /// -> str + string, + /// -> number + length +) = { + return "0" * (length - string.len()) + string +} + +/// Converts a byte array to a hexadecimal string +/// ```example +/// #let b = bytes((0xfa, 0xca, 0xde)) +/// #bytes-to-hex(b) +/// ``` +/// -> str +#let bytes-to-hex( + /// -> bytes + bytes +) = { + let res = "" + for byte in bytes { + res += z-fill(str(byte, base: 16), 2) + } + return res +} + +/// Converts an array of bits into an integer +/// ```example +/// #let bits = (0, 0, 1, 0, 1, 0, 1, 0) +/// #bin-to-int(bits) +/// ``` +/// -> number +#let bin-to-int( + /// Bit array + /// -> array + bin +) = { + return bin.fold( + 0, + (v, b) => v.bit-lshift(1).bit-or(b) + ) +} + +#let max-32 = 1.bit-lshift(32) +#let mask-32 = max-32 - 1 + +/// Rotates a number to the left (wrapping the leftmost bits to the right) +/// ```example +/// #let a = 42 +/// #let b = circular-shift(a, n: 20) +/// #let c = circular-shift(b, n: 11) +/// #b, #c +/// ``` +/// -> number +#let circular-shift( + /// Number to rotate + /// -> number + x, + /// Shift amount + /// -> number + n: 1 +) = { + if n < 0 { + return circular-shift(x, n: 32 + n) + } + let high-bits = x.bit-rshift(32 - n) + return x.bit-lshift(n).bit-or(high-bits).bit-and(mask-32) +} \ No newline at end of file diff --git a/typst.toml b/typst.toml new file mode 100644 index 0000000..131d1e2 --- /dev/null +++ b/typst.toml @@ -0,0 +1,14 @@ +[package] +name = "jumble" +version = "0.0.1" +compiler = "0.12.0" +repository = "https://git.kb28.ch/HEL/jumble" +entrypoint = "src/lib.typ" +authors = [ + "Louis Heredero " +] +categories = ["scripting", "utility"] +license = "Apache-2.0" +description = "A package to hash " +keywords = ["hash", "algorithm", "cryptography", "md5", "sha1"] +exclude = [ "gallery", "gallery.bash", "docs" ] \ No newline at end of file