Implement hashing algorithms + HMac support (#193)

This commit is contained in:
Micah 2024-06-05 07:30:50 -07:00 committed by GitHub
parent cf513c6724
commit 5a292aabc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 457 additions and 0 deletions

View file

@ -8,6 +8,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
- Added a builtin API for hashing and calculating HMACs as part of the `serde` library
Basic usage:
```lua
local serde = require("@lune/serde")
local hash = serde.hash("sha256", "a message to hash")
local hmac = serde.hmac("sha256", "a message to hash", "a secret string")
print(hash)
print(hmac)
```
The returned hashes are sequences of lowercase hexadecimal digits. The following algorithms are supported:
`md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `blake3`
## `0.8.5` - June 1st, 2024
### Changed

48
Cargo.lock generated
View file

@ -273,6 +273,7 @@ dependencies = [
"cc",
"cfg-if",
"constant_time_eq 0.3.0",
"digest",
]
[[package]]
@ -1339,6 +1340,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keccak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654"
dependencies = [
"cpufeatures",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -1582,13 +1592,20 @@ name = "lune-std-serde"
version = "0.1.0"
dependencies = [
"async-compression",
"blake3",
"bstr",
"digest",
"hmac",
"lune-utils",
"lz4",
"md-5",
"mlua",
"serde",
"serde_json",
"serde_yaml",
"sha1 0.10.6",
"sha2",
"sha3",
"tokio",
"toml",
]
@ -1666,6 +1683,16 @@ dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.7.2"
@ -2665,6 +2692,27 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "sha2"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest",
"keccak",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"

View file

@ -29,6 +29,16 @@ serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = "0.9"
toml = { version = "0.8", features = ["preserve_order"] }
digest = "0.10.7"
hmac = "0.12.1"
md-5 = "0.10.6"
sha1 = "0.10.6"
sha2 = "0.10.8"
sha3 = "0.10.8"
# This feature MIGHT break due to the unstable nature of the digest crate.
# Check before updating it.
blake3 = { version = "1.5.0", features = ["traits-preview"] }
tokio = { version = "1", default-features = false, features = [
"rt",
"io-util",

View file

@ -0,0 +1,234 @@
use std::fmt::Write;
use bstr::BString;
use md5::Md5;
use mlua::prelude::*;
use blake3::Hasher as Blake3;
use sha1::Sha1;
use sha2::{Sha224, Sha256, Sha384, Sha512};
use sha3::{Sha3_224, Sha3_256, Sha3_384, Sha3_512};
pub struct HashOptions {
algorithm: HashAlgorithm,
message: BString,
secret: Option<BString>,
// seed: Option<BString>,
}
#[derive(Debug, Clone, Copy)]
enum HashAlgorithm {
Md5,
Sha1,
// SHA-2 variants
Sha2_224,
Sha2_256,
Sha2_384,
Sha2_512,
// SHA-3 variants
Sha3_224,
Sha3_256,
Sha3_384,
Sha3_512,
// Blake3
Blake3,
}
impl HashAlgorithm {
pub fn list_all_as_string() -> String {
[
"md5", "sha1", "sha224", "sha256", "sha384", "sha512", "sha3-224", "sha3-256",
"sha3-384", "sha3-512", "blake3",
]
.join(", ")
}
}
impl HashOptions {
/**
Computes the hash for the `message` using whatever `algorithm` is
contained within this struct and returns it as a string of hex digits.
*/
#[inline]
#[must_use = "hashing a message is useless without using the resulting hash"]
pub fn hash(self) -> String {
use digest::Digest;
let message = self.message;
let bytes = match self.algorithm {
HashAlgorithm::Md5 => Md5::digest(message).to_vec(),
HashAlgorithm::Sha1 => Sha1::digest(message).to_vec(),
HashAlgorithm::Sha2_224 => Sha224::digest(message).to_vec(),
HashAlgorithm::Sha2_256 => Sha256::digest(message).to_vec(),
HashAlgorithm::Sha2_384 => Sha384::digest(message).to_vec(),
HashAlgorithm::Sha2_512 => Sha512::digest(message).to_vec(),
HashAlgorithm::Sha3_224 => Sha3_224::digest(message).to_vec(),
HashAlgorithm::Sha3_256 => Sha3_256::digest(message).to_vec(),
HashAlgorithm::Sha3_384 => Sha3_384::digest(message).to_vec(),
HashAlgorithm::Sha3_512 => Sha3_512::digest(message).to_vec(),
HashAlgorithm::Blake3 => Blake3::digest(message).to_vec(),
};
// We don't want to return raw binary data generally, since that's not
// what most people want a hash for. So we have to make a hex string.
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
})
}
/**
Computes the HMAC for the `message` using whatever `algorithm` and
`secret` are contained within this struct. The computed value is
returned as a string of hex digits.
# Errors
If the `secret` is not provided or is otherwise invalid.
*/
#[inline]
pub fn hmac(self) -> LuaResult<String> {
use hmac::{Hmac, Mac, SimpleHmac};
let secret = self
.secret
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #3 missing or nil".to_string()),
})?;
/*
These macros exist to remove what would ultimately be dozens of
repeating lines. Essentially, there's several step to processing
HMacs, which expands into the 3 lines you see below. However,
the Hmac struct is specialized towards eager block-based processes.
In order to support anything else, like blake3, there's a second
type named `SimpleHmac`. This results in duplicate macros like
there are below.
*/
macro_rules! hmac {
($Type:ty) => {{
let mut mac: Hmac<$Type> = Hmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
macro_rules! hmac_no_blocks {
($Type:ty) => {{
let mut mac: SimpleHmac<$Type> =
SimpleHmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
let bytes = match self.algorithm {
HashAlgorithm::Md5 => hmac!(Md5),
HashAlgorithm::Sha1 => hmac!(Sha1),
HashAlgorithm::Sha2_224 => hmac!(Sha224),
HashAlgorithm::Sha2_256 => hmac!(Sha256),
HashAlgorithm::Sha2_384 => hmac!(Sha384),
HashAlgorithm::Sha2_512 => hmac!(Sha512),
HashAlgorithm::Sha3_224 => hmac!(Sha3_224),
HashAlgorithm::Sha3_256 => hmac!(Sha3_256),
HashAlgorithm::Sha3_384 => hmac!(Sha3_384),
HashAlgorithm::Sha3_512 => hmac!(Sha3_512),
HashAlgorithm::Blake3 => hmac_no_blocks!(Blake3),
};
Ok(bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
}))
}
}
impl<'lua> FromLua<'lua> for HashAlgorithm {
fn from_lua(value: LuaValue<'lua>, _lua: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::String(str) = value {
/*
Casing tends to vary for algorithms, so rather than force
people to remember it we'll just accept any casing.
*/
let str = str.to_str()?.to_ascii_lowercase();
match str.as_str() {
"md5" => Ok(Self::Md5),
"sha1" => Ok(Self::Sha1),
"sha224" => Ok(Self::Sha2_224),
"sha256" => Ok(Self::Sha2_256),
"sha384" => Ok(Self::Sha2_384),
"sha512" => Ok(Self::Sha2_512),
"sha3-224" => Ok(Self::Sha3_224),
"sha3-256" => Ok(Self::Sha3_256),
"sha3-384" => Ok(Self::Sha3_384),
"sha3-512" => Ok(Self::Sha3_512),
"blake3" => Ok(Self::Blake3),
_ => Err(LuaError::FromLuaConversionError {
from: "string",
to: "HashAlgorithm",
message: Some(format!(
"Invalid hashing algorithm '{str}', valid kinds are:\n{}",
HashAlgorithm::list_all_as_string()
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "HashAlgorithm",
message: None,
})
}
}
}
impl<'lua> FromLuaMulti<'lua> for HashOptions {
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let algorithm = values
.pop_front()
.map(|value| HashAlgorithm::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "HashAlgorithm",
message: Some("Argument #1 missing or nil".to_string()),
})?;
let message = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #2 missing or nil".to_string()),
})?;
let secret = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?;
// let seed = values
// .pop_front()
// .map(|value| BString::from_lua(value, lua))
// .transpose()?;
Ok(HashOptions {
algorithm,
message,
secret,
// seed,
})
}
}

View file

@ -7,9 +7,11 @@ use lune_utils::TableBuilder;
mod compress_decompress;
mod encode_decode;
mod hash;
pub use self::compress_decompress::{compress, decompress, CompressDecompressFormat};
pub use self::encode_decode::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
pub use self::hash::HashOptions;
/**
Creates the `serde` standard library module.
@ -24,6 +26,8 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.with_function("decode", serde_decode)?
.with_async_function("compress", serde_compress)?
.with_async_function("decompress", serde_decompress)?
.with_function("hash", hash_message)?
.with_function("hmac", hmac_message)?
.build_readonly()
}
@ -55,3 +59,11 @@ async fn serde_decompress(
let bytes = decompress(bs, format).await?;
lua.create_string(bytes)
}
fn hash_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hash())
}
fn hmac_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hmac()?)
}

View file

@ -230,6 +230,8 @@ create_tests! {
serde_json_encode: "serde/json/encode",
serde_toml_decode: "serde/toml/decode",
serde_toml_encode: "serde/toml/encode",
serde_hashing_hash: "serde/hashing/hash",
serde_hashing_hmac: "serde/hashing/hmac",
}
#[cfg(feature = "std-stdio")]

View file

@ -0,0 +1,48 @@
local serde = require("@lune/serde")
local TEST_INPUT =
"Luau is a fast, small, safe, gradually typed embeddable scripting language derived from Lua."
local function test_case_hash(algorithm: serde.HashAlgorithm, expected: string)
assert(
serde.hash(algorithm, TEST_INPUT) == expected,
`hashing algorithm '{algorithm}' did not hash test string correctly`
)
assert(
serde.hash(algorithm, buffer.fromstring(TEST_INPUT)) == expected,
`hashing algorithm '{algorithm}' did not hash test buffer correctly`
)
end
test_case_hash("blake3", "eccfe3a6696b2a1861c64cc78663cff51301058e5dc22bb6249e7e1e0173d7fe")
test_case_hash("md5", "2aed9e020b49d219dc383884c5bd7acd")
test_case_hash("sha1", "9dce74190857f36e6d3f5e8eb7fe704a74060726")
test_case_hash("sha224", "f7ccd8a5f2697df8470b66f03824e073075292a1fab40d3a2ddc2e83")
test_case_hash("sha256", "f1d149bfd1ea38833ae6abf2a6fece1531532283820d719272e9cf3d9344efea")
test_case_hash(
"sha384",
"f6da4b47846c6016a9b32f01b861e45195cf1fa6fc5c9dd2257f7dc1c14092f11001839ec1223c30ab7adb7370812863"
)
test_case_hash(
"sha512",
"49fd834fdf3d4eaf4d4aff289acfc24d649f81cee7a5a7940e5c86854e04816f0a97c53f2ca4908969a512ec5ad1dc466422e3928f5ce3da9913959315df807c"
)
test_case_hash("sha3-224", "56a4dd1ff1bd9baff7f8bbe380dbf2c75b073161693f94ebf91aeee5")
test_case_hash("sha3-256", "ee01be10e0dc133cd702999e854b396f40b039d5ba6ddec9d04bf8623ba04dd7")
test_case_hash(
"sha3-384",
"e992f31e638b47802f33a4327c0a951823e32491ddcef5af9ce18cff84475c98ced23928d47ef51a8a4299dfe2ece361"
)
test_case_hash(
"sha3-512",
"08bd02aca3052b7740de80b8e8b9969dc9059a4bfae197095430e0aa204fbd3afb11731b127559b90c2f7e295835ea844ddbb29baf2fdb1d823046052c120fc9"
)
local failed = pcall(serde.hash, "a random string" :: any, "input that shouldn't be hashed")
assert(failed == false, "serde.hash shouldn't allow invalid algorithms passed to it!")
assert(
serde.hash("sha256", "\0oh no invalid utf-8\127\0\255")
== "c18ed3188f9e93f9ecd3582d7398c45120b0b30a0e26243809206228ab711b78",
"serde.hash should hash invalid UTF-8 just fine"
)

View file

@ -0,0 +1,60 @@
local serde = require("@lune/serde")
local INPUT_STRING = "important data to verify the integrity of"
-- if you read this string, you're obligated to keep it a secret! :-)
local SECRET_STRING = "don't read this we operate on the honor system"
local function test_case_hmac(algorithm: serde.HashAlgorithm, expected: string)
assert(
serde.hmac(algorithm, INPUT_STRING, SECRET_STRING) == expected,
`HMAC test for algorithm '{algorithm}' was not correct with string input and string secret`
)
assert(
serde.hmac(algorithm, INPUT_STRING, buffer.fromstring(SECRET_STRING)) == expected,
`HMAC test for algorithm '{algorithm}' was not correct with string input and buffer secret`
)
assert(
serde.hmac(algorithm, buffer.fromstring(INPUT_STRING), SECRET_STRING) == expected,
`HMAC test for algorithm '{algorithm}' was not correct with buffer input and string secret`
)
assert(
serde.hmac(algorithm, buffer.fromstring(INPUT_STRING), buffer.fromstring(SECRET_STRING))
== expected,
`HMAC test for algorithm '{algorithm}' was not correct with buffer input and buffer secret`
)
end
test_case_hmac("blake3", "1d9c1b9405567fc565c2c3c6d6c0e170be72a2623d29911f43cb2ce42a373c01")
test_case_hmac("md5", "525379669c93ab5f59d2201024145b79")
test_case_hmac("sha1", "75227c11ed65133788feab0ce7eb8efc8c1f0517")
test_case_hmac("sha224", "47a4857d7d7e1070f47f76558323e03471a918facaf3667037519c29")
test_case_hmac("sha256", "4a4816ab8d4b780a8cf131e34a3df25e4c7bc4eba453cd86e50271aab4e95f45")
test_case_hmac(
"sha384",
"6b24aeae78d0f84ec8a4669b24bda1131205535233c344f4262c1f90f29af04c5537612c269bbab8aaca9d8293f4a280"
)
test_case_hmac(
"sha512",
"9fffa071241e2f361f8a47a97d251c1d4aae37498efbc49745bf9916d8431f1f361080d350067ed65744d3da42956da33ec57b04901a5fd63a891381a1485ef7"
)
test_case_hmac("sha3-224", "ea102dfaa74aa285555bdba29a04429dfd4e997fa40322459094929f")
test_case_hmac("sha3-256", "17bde287e4692e5b7f281e444efefe92e00696a089570bd6814fd0e03d7763d2")
test_case_hmac(
"sha3-384",
"24f68401653d25f36e7ee8635831215f8b46710d4e133c9d1e091e5972c69b0f1d0cb80f5507522fa174d5c4746963c1"
)
test_case_hmac(
"sha3-512",
"d2566d156c254ced0101159f97187dbf48d900b8361fa5ebdd7e81409856b1b6a21d93a1fb6e8f700e75620d244ab9e894454030da12d158e9362ffe090d2669"
)
local failed =
pcall(serde.hmac, "a random string" :: any, "input that shouldn't be hashed", "not a secret")
assert(failed == false, "serde.hmac shouldn't allow invalid algorithms passed to it!")
assert(
serde.hmac("sha256", "\0oh no invalid utf-8\127\0\255", SECRET_STRING)
== "1f0d7f65016e9e4c340e3ba23da2483a7dc101ce8a9405f834c23f2e19232c3d",
"serde.hmac should hash invalid UTF-8 just fine"
)

View file

@ -2,6 +2,19 @@ export type EncodeDecodeFormat = "json" | "yaml" | "toml"
export type CompressDecompressFormat = "brotli" | "gzip" | "lz4" | "zlib"
export type HashAlgorithm =
"md5"
| "sha1"
| "sha224"
| "sha256"
| "sha384"
| "sha512"
| "sha3-224"
| "sha3-256"
| "sha3-384"
| "sha3-512"
| "blake3"
--[=[
@class Serde
@ -120,4 +133,16 @@ function serde.decompress(format: CompressDecompressFormat, s: buffer | string):
return nil :: any
end
function serde.hash(algorithm: HashAlgorithm, message: string | buffer): string
return nil :: any
end
function serde.hmac(
algorithm: HashAlgorithm,
message: string | buffer,
secret: string | buffer
): string
return nil :: any
end
return serde