diff --git a/Cargo.toml b/Cargo.toml index ec459971..321c9191 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ license = "MIT" readme = "README.md" [dependencies] +tiny-keccak = "~1.0.5" ring = "~0.6.2" diff --git a/src/hashes.rs b/src/hashes.rs index e4d0c23b..1f53e82b 100644 --- a/src/hashes.rs +++ b/src/hashes.rs @@ -11,14 +11,22 @@ pub enum Hash { SHA2256, /// SHA-512 (64-byte hash size) SHA2512, - /// Encoding unsupported + /// SHA3-512 (64-byte hash size) SHA3512, - /// Encoding unsupported + /// SHA3-384 (48-byte hash size) SHA3384, - /// Encoding unsupported + /// SHA3-256 (32-byte hash size) SHA3256, - /// Encoding unsupported + /// SHA3-224 (28-byte hash size) SHA3224, + // Keccak-224 (28-byte hash size) + Keccak224, + // Keccak-256 (32-byte hash size) + Keccak256, + // Keccak-384 (48-byte hash size) + Keccak384, + // Keccak-512 (64-byte hash size) + Keccak512, /// Encoding unsupported Blake2b, /// Encoding unsupported @@ -34,10 +42,14 @@ impl Hash { SHA1 => 0x11, SHA2256 => 0x12, SHA2512 => 0x13, - SHA3512 => 0x14, - SHA3384 => 0x15, - SHA3256 => 0x16, SHA3224 => 0x17, + SHA3256 => 0x16, + SHA3384 => 0x15, + SHA3512 => 0x14, + Keccak224 => 0x1A, + Keccak256 => 0x1B, + Keccak384 => 0x1C, + Keccak512 => 0x1D, Blake2b => 0x40, Blake2s => 0x41, } @@ -49,8 +61,18 @@ impl Hash { match *self { SHA1 => 20, - SHA2256 | Blake2s => 32, - SHA2512 | SHA3512 | SHA3384 | SHA3256 | SHA3224 | Blake2b => 64, + SHA2256 => 32, + SHA2512 => 64, + SHA3224 => 28, + SHA3256 => 32, + SHA3384 => 48, + SHA3512 => 64, + Keccak224 => 28, + Keccak256 => 32, + Keccak384 => 48, + Keccak512 => 64, + Blake2b => 64, + Blake2s => 32, } } @@ -67,6 +89,10 @@ impl Hash { SHA3384 => "SHA3-384", SHA3256 => "SHA3-256", SHA3224 => "SHA3-224", + Keccak224 => "Keccak-224", + Keccak256 => "Keccak-256", + Keccak384 => "Keccak-384", + Keccak512 => "Keccak-512", Blake2b => "Blake-2b", Blake2s => "Blake-2s", } @@ -83,6 +109,10 @@ impl Hash { 0x15 => SHA3384, 0x16 => SHA3256, 0x17 => SHA3224, + 0x1A => Keccak224, + 0x1B => Keccak256, + 0x1C => Keccak384, + 0x1D => Keccak512, 0x40 => Blake2b, 0x41 => Blake2s, _ => return Err(Error::UnkownCode), diff --git a/src/lib.rs b/src/lib.rs index 95aa677f..495ff3ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,11 @@ /// Representation of a Multiaddr. extern crate ring; +extern crate tiny_keccak; +use std::fmt::Write; + +use tiny_keccak::Keccak; use ring::digest; mod hashes; @@ -14,6 +18,36 @@ pub use hashes::*; mod errors; pub use errors::*; +// Helper macro for encoding input into output using either ring or tiny_keccak +macro_rules! encode { + (ring, $algorithm:ident, $input:expr, $output:expr) => ({ + let result = digest::digest(&digest::$algorithm, $input); + debug_assert!($output.len() == result.as_ref().len()); + $output.copy_from_slice(result.as_ref()); + }); + (tiny, $constructor:ident, $input:expr, $output:expr) => ({ + let mut kec = Keccak::$constructor(); + kec.update($input); + kec.finalize($output); + }) +} + +// And another one to keep the matching DRY +macro_rules! match_encoder { + ($hash:ident for ($input:expr, $output:expr) { + $( $hashtype:ident => $lib:ident :: $method:ident, )* + }) => ({ + match $hash { + $( + Hash::$hashtype => encode!($lib, $method, $input, $output), + )* + + _ => return Err(Error::UnsupportedType) + } + }) +} + + /// Encodes data into a multihash. /// /// The returned data is raw bytes. To make is more human-friendly, you can encode it (hex, @@ -36,26 +70,28 @@ pub use errors::*; /// ); /// ``` /// -pub fn encode(wanttype: Hash, input: &[u8]) -> Result, Error> { - let encoded = encode_digest(wanttype, input)?; - let mut bytes = Vec::with_capacity(encoded.len() + 2); - - bytes.push(wanttype.code()); - bytes.push(encoded.len() as u8); - bytes.extend(encoded); - - Ok(bytes) -} +pub fn encode(hash: Hash, input: &[u8]) -> Result, Error> { + let size = hash.size(); + let mut output = Vec::new(); + output.resize(2 + size as usize, 0); + output[0] = hash.code(); + output[1] = size; -fn encode_digest(wanttype: Hash, input: &[u8]) -> Result, Error> { - let digest_type = match wanttype { - Hash::SHA1 => &digest::SHA1, - Hash::SHA2256 => &digest::SHA256, - Hash::SHA2512 => &digest::SHA512, - _ => return Err(Error::UnsupportedType), - }; + match_encoder!(hash for (input, &mut output[2..]) { + SHA1 => ring::SHA1, + SHA2256 => ring::SHA256, + SHA2512 => ring::SHA512, + SHA3224 => tiny::new_sha3_224, + SHA3256 => tiny::new_sha3_256, + SHA3384 => tiny::new_sha3_384, + SHA3512 => tiny::new_sha3_512, + Keccak224 => tiny::new_keccak224, + Keccak256 => tiny::new_keccak256, + Keccak384 => tiny::new_keccak384, + Keccak512 => tiny::new_keccak512, + }); - Ok(digest::digest(digest_type, input).as_ref().to_owned()) + Ok(output) } /// Decodes bytes into a multihash @@ -108,7 +144,11 @@ pub struct Multihash<'a> { /// Convert bytes to a hex representation pub fn to_hex(bytes: &[u8]) -> String { - bytes.iter() - .map(|x| format!("{:02x}", x)) - .collect() + let mut hex = String::with_capacity(bytes.len() * 2); + + for byte in bytes { + write!(hex, "{:02x}", byte).expect("Can't fail on writing to string"); + } + + hex } diff --git a/tests/lib.rs b/tests/lib.rs index b13bdd7b..14137c98 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -14,50 +14,91 @@ fn hex_to_bytes(s: &str) -> Vec { } +macro_rules! assert_encode { + {$( $alg:ident, $data:expr, $expect:expr; )*} => { + $( + assert_eq!( + encode(Hash::$alg, $data).expect("Must be supported"), + hex_to_bytes($expect), + "{} encodes correctly", Hash::$alg.name() + ); + )* + } +} + #[test] -fn multihash_encode () { - assert_eq!( - encode(Hash::SHA1, b"beep boop").unwrap(), - hex_to_bytes("11147c8357577f51d4f0a8d393aa1aaafb28863d9421") - ); - assert_eq!( - encode(Hash::SHA2256, b"helloworld").unwrap(), - hex_to_bytes("1220936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af") - ); - assert_eq!( - encode(Hash::SHA2256, b"beep boop").unwrap(), - hex_to_bytes("122090ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c") - ); - assert_eq!( - encode(Hash::SHA2512, b"hello world").unwrap(), - hex_to_bytes("1340309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f") - ); +fn multihash_encode() { + assert_encode! { + SHA1, b"beep boop", "11147c8357577f51d4f0a8d393aa1aaafb28863d9421"; + SHA2256, b"helloworld", "1220936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af"; + SHA2256, b"beep boop", "122090ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c"; + SHA2512, b"hello world", "1340309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f"; + SHA3224, b"hello world", "171Cdfb7f18c77e928bb56faeb2da27291bd790bc1045cde45f3210bb6c5"; + SHA3256, b"hello world", "1620644bcc7e564373040999aac89e7622f3ca71fba1d972fd94a31c3bfbf24e3938"; + SHA3384, b"hello world", "153083bff28dde1b1bf5810071c6643c08e5b05bdb836effd70b403ea8ea0a634dc4997eb1053aa3593f590f9c63630dd90b"; + SHA3512, b"hello world", "1440840006653e9ac9e95117a15c915caab81662918e925de9e004f774ff82d7079a40d4d27b1b372657c61d46d470304c88c788b3a4527ad074d1dccbee5dbaa99a"; + Keccak224, b"hello world", "1A1C25f3ecfebabe99686282f57f5c9e1f18244cfee2813d33f955aae568"; + Keccak256, b"hello world", "1B2047173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"; + Keccak384, b"hello world", "1C3065fc99339a2a40e99d3c40d695b22f278853ca0f925cde4254bcae5e22ece47e6441f91b6568425adc9d95b0072eb49f"; + Keccak512, b"hello world", "1D403ee2b40047b8060f68c67242175660f4174d0af5c01d47168ec20ed619b0b7c42181f40aa1046f39e2ef9efc6910782a998e0013d172458957957fac9405b67d"; + } +} +macro_rules! assert_decode { + {$( $alg:ident, $hash:expr; )*} => { + $( + let hash = hex_to_bytes($hash); + assert_eq!( + decode(&hash).unwrap().alg, + Hash::$alg, + "{} decodes correctly", Hash::$alg.name() + ); + )* + } } #[test] -fn multihash_decode () { - let hash: Vec = encode(Hash::SHA1, b"helloworld").unwrap(); - assert_eq!( - decode(&hash).unwrap().alg, - Hash::SHA1 - ); +fn assert_decode() { + assert_decode! { + SHA1, "11147c8357577f51d4f0a8d393aa1aaafb28863d9421"; + SHA2256, "1220936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af"; + SHA2256, "122090ea688e275d580567325032492b597bc77221c62493e76330b85ddda191ef7c"; + SHA2512, "1340309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f"; + SHA3224, "171Cdfb7f18c77e928bb56faeb2da27291bd790bc1045cde45f3210bb6c5"; + SHA3256, "1620644bcc7e564373040999aac89e7622f3ca71fba1d972fd94a31c3bfbf24e3938"; + SHA3384, "153083bff28dde1b1bf5810071c6643c08e5b05bdb836effd70b403ea8ea0a634dc4997eb1053aa3593f590f9c63630dd90b"; + SHA3512, "1440840006653e9ac9e95117a15c915caab81662918e925de9e004f774ff82d7079a40d4d27b1b372657c61d46d470304c88c788b3a4527ad074d1dccbee5dbaa99a"; + Keccak224, "1A1C25f3ecfebabe99686282f57f5c9e1f18244cfee2813d33f955aae568"; + Keccak256, "1B2047173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"; + Keccak384, "1C3065fc99339a2a40e99d3c40d695b22f278853ca0f925cde4254bcae5e22ece47e6441f91b6568425adc9d95b0072eb49f"; + Keccak512, "1D403ee2b40047b8060f68c67242175660f4174d0af5c01d47168ec20ed619b0b7c42181f40aa1046f39e2ef9efc6910782a998e0013d172458957957fac9405b67d"; + } +} - let hash: Vec = encode(Hash::SHA2256, b"helloworld").unwrap(); - assert_eq!( - decode(&hash).unwrap().alg, - Hash::SHA2256 - ); +macro_rules! assert_roundtrip { + ($( $alg:ident ),*) => { + $( + { + let hash: Vec = encode(Hash::$alg, b"helloworld").unwrap(); + assert_eq!( + decode(&hash).unwrap().alg, + Hash::$alg + ); + } + )* + } +} - let hash: Vec = encode(Hash::SHA2512, b"helloworld").unwrap(); - assert_eq!( - decode(&hash).unwrap().alg, - Hash::SHA2512 +#[test] +fn assert_roundtrip() { + assert_roundtrip!( + SHA1, SHA2256, SHA2512, SHA3224, SHA3256, SHA3384, SHA3512, + Keccak224, Keccak256, Keccak384, Keccak512 ); } #[test] -fn hash_types () { +fn hash_types() { assert_eq!(Hash::SHA2256.size(), 32); assert_eq!(Hash::SHA2256.name(), "SHA2-256"); }