├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── compare_with_sha.rs └── src ├── hash_matrix.rs ├── lib.rs └── lookup_table.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bromberg_sl2" 3 | description = "Cayley hashing as in 'Navigating in the Cayley Graph of SL₂(𝔽ₚ)'" 4 | version = "0.6.0" 5 | authors = ["Ben Weinstein-Raun "] 6 | edition = "2018" 7 | license = "CC0-1.0" 8 | repository = "https://github.com/benwr/bromberg_sl2" 9 | categories = ["cryptography", "algorithms", "no-std"] 10 | keywords = ["hashing", "sl2", "homomorphic", "monoidal", "tillich-zemor"] 11 | 12 | [features] 13 | default = ["std"] 14 | std = ["rayon"] 15 | 16 | [profile.release] 17 | lto = true 18 | opt-level = 3 19 | codegen-units = 1 20 | 21 | [profile.bench] 22 | lto = true 23 | opt-level = 3 24 | codegen-units = 1 25 | 26 | [dependencies] 27 | digest = { version = "0.9.0", features = ["alloc"] } 28 | lazy_static = "^1.4" 29 | rayon = { version = "1.5.1", optional = true } 30 | serde = { version = "1.0", features = ["derive", "rc"] } 31 | seq-macro = "^0.2" 32 | 33 | [dev-dependencies] 34 | criterion = "0.3" 35 | num-bigint = "0.3" 36 | quickcheck = "0.9" 37 | sha3 = "0.9" 38 | 39 | [[bench]] 40 | name = "compare_with_sha" 41 | harness = false 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES 4 | NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE 5 | AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION 6 | ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE 7 | OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS 8 | LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION 9 | OR WORKS PROVIDED HEREUNDER. 10 | 11 | Statement of Purpose 12 | 13 | The laws of most jurisdictions throughout the world automatically confer exclusive 14 | Copyright and Related Rights (defined below) upon the creator and subsequent 15 | owner(s) (each and all, an "owner") of an original work of authorship and/or 16 | a database (each, a "Work"). 17 | 18 | Certain owners wish to permanently relinquish those rights to a Work for the 19 | purpose of contributing to a commons of creative, cultural and scientific 20 | works ("Commons") that the public can reliably and without fear of later claims 21 | of infringement build upon, modify, incorporate in other works, reuse and 22 | redistribute as freely as possible in any form whatsoever and for any purposes, 23 | including without limitation commercial purposes. These owners may contribute 24 | to the Commons to promote the ideal of a free culture and the further production 25 | of creative, cultural and scientific works, or to gain reputation or greater 26 | distribution for their Work in part through the use and efforts of others. 27 | 28 | For these and/or other purposes and motivations, and without any expectation 29 | of additional consideration or compensation, the person associating CC0 with 30 | a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 31 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 32 | and publicly distribute the Work under its terms, with knowledge of his or 33 | her Copyright and Related Rights in the Work and the meaning and intended 34 | legal effect of CC0 on those rights. 35 | 36 | 1. Copyright and Related Rights. A Work made available under CC0 may be protected 37 | by copyright and related or neighboring rights ("Copyright and Related Rights"). 38 | Copyright and Related Rights include, but are not limited to, the following: 39 | 40 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 41 | and translate a Work; 42 | 43 | ii. moral rights retained by the original author(s) and/or performer(s); 44 | 45 | iii. publicity and privacy rights pertaining to a person's image or likeness 46 | depicted in a Work; 47 | 48 | iv. rights protecting against unfair competition in regards to a Work, subject 49 | to the limitations in paragraph 4(a), below; 50 | 51 | v. rights protecting the extraction, dissemination, use and reuse of data 52 | in a Work; 53 | 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal protection 56 | of databases, and under any national implementation thereof, including any 57 | amended or successor version of such directive); and 58 | 59 | vii. other similar, equivalent or corresponding rights throughout the world 60 | based on applicable law or treaty, and any national implementations thereof. 61 | 62 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 63 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 64 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 65 | and Related Rights and associated claims and causes of action, whether now 66 | known or unknown (including existing as well as future claims and causes of 67 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 68 | duration provided by applicable law or treaty (including future time extensions), 69 | (iii) in any current or future medium and for any number of copies, and (iv) 70 | for any purpose whatsoever, including without limitation commercial, advertising 71 | or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the 72 | benefit of each member of the public at large and to the detriment of Affirmer's 73 | heirs and successors, fully intending that such Waiver shall not be subject 74 | to revocation, rescission, cancellation, termination, or any other legal or 75 | equitable action to disrupt the quiet enjoyment of the Work by the public 76 | as contemplated by Affirmer's express Statement of Purpose. 77 | 78 | 3. Public License Fallback. Should any part of the Waiver for any reason be 79 | judged legally invalid or ineffective under applicable law, then the Waiver 80 | shall be preserved to the maximum extent permitted taking into account Affirmer's 81 | express Statement of Purpose. In addition, to the extent the Waiver is so 82 | judged Affirmer hereby grants to each affected person a royalty-free, non 83 | transferable, non sublicensable, non exclusive, irrevocable and unconditional 84 | license to exercise Affirmer's Copyright and Related Rights in the Work (i) 85 | in all territories worldwide, (ii) for the maximum duration provided by applicable 86 | law or treaty (including future time extensions), (iii) in any current or 87 | future medium and for any number of copies, and (iv) for any purpose whatsoever, 88 | including without limitation commercial, advertising or promotional purposes 89 | (the "License"). The License shall be deemed effective as of the date CC0 90 | was applied by Affirmer to the Work. Should any part of the License for any 91 | reason be judged legally invalid or ineffective under applicable law, such 92 | partial invalidity or ineffectiveness shall not invalidate the remainder of 93 | the License, and in such case Affirmer hereby affirms that he or she will 94 | not (i) exercise any of his or her remaining Copyright and Related Rights 95 | in the Work or (ii) assert any associated claims and causes of action with 96 | respect to the Work, in either case contrary to Affirmer's express Statement 97 | of Purpose. 98 | 99 | 4. Limitations and Disclaimers. 100 | 101 | a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, 102 | licensed or otherwise affected by this document. 103 | 104 | b. Affirmer offers the Work as-is and makes no representations or warranties 105 | of any kind concerning the Work, express, implied, statutory or otherwise, 106 | including without limitation warranties of title, merchantability, fitness 107 | for a particular purpose, non infringement, or the absence of latent or other 108 | defects, accuracy, or the present or absence of errors, whether or not discoverable, 109 | all to the greatest extent permissible under applicable law. 110 | 111 | c. Affirmer disclaims responsibility for clearing rights of other persons 112 | that may apply to the Work or any use thereof, including without limitation 113 | any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims 114 | responsibility for obtaining any necessary consents, permissions or other 115 | rights required for any use of the Work. 116 | 117 | d. Affirmer understands and acknowledges that Creative Commons is not a party 118 | to this document and has no duty or obligation with respect to this CC0 or 119 | use of the Work. 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bromberg-Shpilrain-Vdovina SL₂ Homomorphic Hashing 2 | 3 | This is an implementation of the Tillich-Zémor-style hash function 4 | presented in the paper ["Navigating in the Cayley Graph of SL₂(𝔽ₚ)" 5 | ](https://link.springer.com/article/10.1007%2Fs00233-015-9766-5) by 6 | Bromberg, Shpilrain, and Vdovina. 7 | 8 | > ### Warning 9 | > 10 | > This module is not produced by cryptography experts, but by 11 | > [some random guy](http://benwr.net). Furthermore, the algorithm 12 | > was published in 2017, and is itself not at all battle-tested. Only 13 | > use this library if you either (a) know what you're doing and have 14 | > read and understood our code, and/or (b) are building something that 15 | > does not rely heavily on the cryptographic properties of the hash 16 | > function. 17 | > 18 | > If you _are_ a cryptography expert, we welcome any bug reports or 19 | > pull requests! We also welcome them if you're not a cryptography 20 | > expert; this library is quite simple, and should be easy to grok 21 | > over a coffee with a copy of the paper linked above in hand. 22 | 23 | # What is this library for? 24 | 25 | This library implements a putatively-strong hash function H with the 26 | useful property that it gives a monoid homomorphism. This means there 27 | is a cheap operation `*` such that given strings `s1` and `s2`, 28 | `H(s1 ++ s2) = H(s1) * H(s2)`. 29 | 30 | This property is especially useful for applications where some very 31 | long string may be constructed via many different routes, but you'd 32 | nonetheless like to be able to quickly rule out unequal strings. 33 | 34 | It also allows you to hash _parts_ of your data as you acquire them, 35 | and then merge them later in whatever order is convenient. This allows 36 | for very flexible hashing schemes. 37 | 38 | H has some other cool properties, and is in some limited but 39 | potentially-useful sense "provably secure". See Bromberg et al. for 40 | details. 41 | 42 | # How to use this library 43 | 44 | This library provides the means to construct `HashMatrix`es, using 45 | `hash()`, which takes a slice of bytes. These hashes can be compared, 46 | or serialized to hex strings using `to_hex`. 47 | 48 | ``` 49 | use bromberg_sl2::*; 50 | assert_eq!( 51 | hash("hello, world! It's fun to hash stuff!".as_ref()).to_hex(), 52 | "01c5cf590d32654c87228c0d66441b200aec1439e54e724f05cd3c6c260634e565594b61988933e826e9705de22884ce007df0f733a371516ddd4ac9237f7a46"); 53 | ``` 54 | 55 | Hashes may also be composed, using the `*` operator: 56 | 57 | ```rust 58 | use bromberg_sl2::*; 59 | assert_eq!( 60 | hash("hello, ".as_ref()) * hash("world!".as_ref()), 61 | hash("hello, world!".as_ref()) 62 | ); 63 | ``` 64 | 65 | # Technical Details 66 | 67 | We use the A(2) and B(2) matrices as generators, and 68 | p = 2^127 - 1 as our prime order, for fast modular arithmetic. 69 | 70 | We have not yet attempted to seriously optimize this library, and 71 | performance is a secondary goal. As of right now our procedure is 72 | about 1/3 as fast as SHA3-512. 73 | 74 | We needed an architecture-agnostic cryptographic hash procedure with 75 | a monoid homomorphism respecting string concatenation, written in a 76 | low-level language. While there are 77 | [a](https://github.com/srijs/hwsl2-core) 78 | [few](https://github.com/nspcc-dev/tzhash) 79 | [implementations](https://github.com/phlegmaticprogrammer/tillich_zemor_hash) 80 | of related algorithms, e.g. the venerable [but broken 81 | ](https://link.springer.com/chapter/10.1007/978-3-642-19574-7_20) Tillich-Zémor hash, 82 | from ["Hashing with SL₂" 83 | ](https://link.springer.com/chapter/10.1007/3-540-48658-5_5), 84 | none of them fulfill these desiderata. 85 | -------------------------------------------------------------------------------- /benches/compare_with_sha.rs: -------------------------------------------------------------------------------- 1 | use std::iter; 2 | 3 | use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId, Throughput}; 4 | use bromberg_sl2::*; 5 | use sha3::{Digest, Sha3_512}; 6 | 7 | // This isjust stolen straight from the Criterion documentation 8 | fn from_elem(c: &mut Criterion) { 9 | static KB: usize = 1024; 10 | let sizes = [KB, 2 * KB, 4 * KB, 8 * KB, 16 * KB, 4096 * KB]; 11 | 12 | let mut group = c.benchmark_group("byte_hashing"); 13 | for size in sizes.iter() { 14 | group.throughput(Throughput::Bytes(*size as u64)); 15 | group.bench_with_input(BenchmarkId::new("bromberg", size), size, |b, &size| { 16 | b.iter(|| black_box(iter::repeat(5u8).take(size).collect::>().bromberg_hash())); 17 | }); 18 | } 19 | for size in sizes.iter() { 20 | group.throughput(Throughput::Bytes(*size as u64)); 21 | group.bench_with_input(BenchmarkId::new("bromberg_strict", size), size, |b, &size| { 22 | b.iter(|| black_box(hash_strict(&iter::repeat(5u8).take(size).collect::>()))); 23 | }); 24 | } 25 | #[cfg(feature = "std")] 26 | { 27 | for size in sizes.iter() { 28 | group.throughput(Throughput::Bytes(*size as u64)); 29 | group.bench_with_input(BenchmarkId::new("bromberg_par", size), size, |b, &size| { 30 | b.iter(|| black_box(hash_par(&iter::repeat(5u8).take(size).collect::>()))); 31 | }); 32 | } 33 | } 34 | for size in sizes.iter() { 35 | group.throughput(Throughput::Bytes(*size as u64)); 36 | group.bench_with_input(BenchmarkId::new("sha", size), size, |b, &size| { 37 | b.iter(|| { 38 | let mut hasher = Sha3_512::new(); 39 | black_box(hasher.update(iter::repeat(5u8).take(size).collect::>())); 40 | black_box(hasher.finalize()); 41 | }); 42 | }); 43 | } 44 | group.finish(); 45 | } 46 | 47 | criterion_group!(benches, from_elem); 48 | criterion_main!(benches); 49 | -------------------------------------------------------------------------------- /src/hash_matrix.rs: -------------------------------------------------------------------------------- 1 | use alloc::string::String; 2 | use core::fmt::Debug; 3 | use core::ops::Mul; 4 | use digest::{consts::U64, generic_array}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[cfg(test)] 8 | use alloc::vec::Vec; 9 | 10 | #[derive(PartialEq, Eq, Debug)] 11 | // big-end first; does this matter? 12 | // TODO try using u64s or u32s instead for performance. 13 | struct U256(u128, u128); 14 | 15 | #[cfg(test)] 16 | use num_bigint::{BigUint, ToBigUint}; 17 | 18 | #[cfg(test)] 19 | impl ToBigUint for U256 { 20 | fn to_biguint(&self) -> Option { 21 | Some( 22 | self.0.to_biguint().unwrap() * (1.to_biguint().unwrap() << 128) 23 | + self.1.to_biguint().unwrap(), 24 | ) 25 | } 26 | } 27 | 28 | /// The type of hash values. Takes up 512 bits of space. 29 | /// Can be created only by composition of the provided 30 | /// [`BrombergHashable`](trait.BrombergHashable.html) 31 | /// instances, since not all 512-bit sequences are valid hashes 32 | /// (in fact, fewer than 1/4 of them will be valid). 33 | #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, Serialize, Deserialize)] 34 | pub struct HashMatrix(u128, u128, u128, u128); 35 | 36 | impl HashMatrix { 37 | #[inline] 38 | pub fn from_hex(hex: &str) -> Result { 39 | if !hex.is_ascii() || hex.len() != 128 { 40 | return Err(format!("invalid hex string: {:?}", hex)); 41 | } 42 | 43 | let hex_bytes = hex.as_bytes(); 44 | let a = hex_bytes_to_u128(&hex_bytes[..32])?; 45 | let b = hex_bytes_to_u128(&hex_bytes[32..64])?; 46 | let c = hex_bytes_to_u128(&hex_bytes[64..96])?; 47 | let d = hex_bytes_to_u128(&hex_bytes[96..])?; 48 | Ok(Self(a, b, c, d)) 49 | } 50 | 51 | #[must_use] 52 | #[inline] 53 | pub(crate) fn generic_array_digest(&self) -> generic_array::GenericArray { 54 | digest::generic_array::GenericArray::from_exact_iter( 55 | [self.0, self.1, self.2, self.3] 56 | .iter() 57 | .flat_map(|x| x.to_be_bytes()), 58 | ) 59 | .unwrap() 60 | } 61 | 62 | /// Produce a hex digest of the hash. This will be a 128 hex digits. 63 | #[inline] 64 | pub fn to_hex(self) -> String { 65 | format!( 66 | "{:032x}{:032x}{:032x}{:032x}", 67 | self.0, self.1, self.2, self.3 68 | ) 69 | } 70 | } 71 | 72 | impl Default for HashMatrix { 73 | fn default() -> Self { 74 | I 75 | } 76 | } 77 | 78 | impl Mul for HashMatrix { 79 | type Output = Self; 80 | #[inline] 81 | fn mul(self, rhs: Self) -> Self { 82 | matmul(self, rhs) 83 | } 84 | } 85 | 86 | pub(crate) const A: HashMatrix = HashMatrix(1, 2, 0, 1); 87 | 88 | pub(crate) const B: HashMatrix = HashMatrix(1, 0, 2, 1); 89 | 90 | pub static I: HashMatrix = HashMatrix(1, 0, 0, 1); 91 | 92 | const SUCC_P: u128 = 1 << 127; 93 | const P: u128 = SUCC_P - 1; 94 | 95 | #[inline] 96 | const fn mul(x: u128, y: u128) -> U256 { 97 | // this could probably be made much faster, though I'm not sure. 98 | let x_lo = x & 0xffff_ffff_ffff_ffff; 99 | let y_lo = y & 0xffff_ffff_ffff_ffff; 100 | 101 | let x_hi = x >> 64; 102 | let y_hi = y >> 64; 103 | 104 | let x_hi_y_lo = x_hi * y_lo; 105 | let y_hi_x_lo = y_hi * x_lo; 106 | 107 | let (lo_sum_1, carry_bool_1) = (x_hi_y_lo << 64).overflowing_add(y_hi_x_lo << 64); 108 | let (lo_sum_2, carry_bool_2) = lo_sum_1.overflowing_add(x_lo * y_lo); 109 | let carry = carry_bool_1 as u128 + carry_bool_2 as u128; 110 | 111 | U256( 112 | (x_hi * y_hi) + (x_hi_y_lo >> 64) + (y_hi_x_lo >> 64) + carry, 113 | lo_sum_2, 114 | ) 115 | } 116 | 117 | #[inline] 118 | const fn add(x: U256, y: U256) -> U256 { 119 | // NOTE: x and y are guaranteed to be <= 120 | // (2^127 - 2)^2 = 2^254 - 4 * 2^127 + 4, 121 | // so I think we don't have to worry about carries out of here. 122 | let (low, carry) = x.1.overflowing_add(y.1); 123 | let high = x.0 + y.0 + carry as u128; 124 | U256(high, low) 125 | } 126 | 127 | // algorithm as described by Dresdenboy in "Fast calculations 128 | // modulo small mersenne primes like M61" at 129 | // https://www.mersenneforum.org/showthread.php?t=1955 130 | // I tried using a handwritten version of this that avoided the U256s, 131 | // but it was like half as fast somehow. 132 | #[inline] 133 | const fn mod_p_round_1(n: U256) -> U256 { 134 | let low_bits = n.1 & P; // 127 bits of input 135 | let intermediate_bits = (n.0 << 1) | (n.1 >> 127); // 128 of the 129 additional bits 136 | let high_bit = n.0 >> 127; 137 | let (sum, carry_bool) = low_bits.overflowing_add(intermediate_bits); 138 | U256(carry_bool as u128 + high_bit, sum) 139 | } 140 | 141 | #[inline] 142 | const fn mod_p_round_2(n: U256) -> u128 { 143 | let low_bits = n.1 & P; // 127 bits of input 144 | let intermediate_bits = (n.0 << 1) | (n.1 >> 127); // 128 of the 129 additional bits 145 | low_bits + intermediate_bits 146 | } 147 | 148 | #[inline] 149 | const fn mod_p_round_3(n: u128) -> u128 { 150 | let low_bits = n & P; // 127 bits of input 151 | let intermediate_bit = n >> 127; // 128 of the 129 additional bits 152 | low_bits + intermediate_bit 153 | } 154 | 155 | #[inline] 156 | const fn constmod_p(n: U256) -> u128 { 157 | let n1 = mod_p_round_1(n); 158 | let n2 = mod_p_round_2(n1); 159 | let n3 = mod_p_round_3(n2); 160 | 161 | ((n3 + 1) & P).saturating_sub(1) 162 | } 163 | 164 | #[inline] 165 | fn mod_p(mut n: U256) -> u128 { 166 | // n is at most 255 bits wide 167 | if n.0 != 0 { 168 | n = mod_p_round_1(n); 169 | } 170 | // n is at most 129 bits wide 171 | let mut n_small = if n.0 != 0 || (n.1 > P) { 172 | mod_p_round_2(n) 173 | } else { 174 | n.1 175 | }; 176 | // n is at most 128 bits wide 177 | if n_small > P { 178 | n_small = mod_p_round_3(n_small); 179 | } 180 | // n is at most 127 bits wide 181 | 182 | if n_small == P { 183 | 0 184 | } else { 185 | n_small 186 | } 187 | } 188 | 189 | #[inline] 190 | pub fn matmul(a: HashMatrix, b: HashMatrix) -> HashMatrix { 191 | HashMatrix( 192 | mod_p(add(mul(a.0, b.0), mul(a.1, b.2))), 193 | mod_p(add(mul(a.0, b.1), mul(a.1, b.3))), 194 | mod_p(add(mul(a.2, b.0), mul(a.3, b.2))), 195 | mod_p(add(mul(a.2, b.1), mul(a.3, b.3))), 196 | ) 197 | } 198 | 199 | /// Identical results to the `*` operator, but slower. Exposed to provide a 200 | /// `const` version in case you'd like to compile certain hashes into your 201 | /// binaries. 202 | #[must_use] 203 | #[inline] 204 | pub const fn constmatmul(a: HashMatrix, b: HashMatrix) -> HashMatrix { 205 | HashMatrix( 206 | constmod_p(add(mul(a.0, b.0), mul(a.1, b.2))), 207 | constmod_p(add(mul(a.0, b.1), mul(a.1, b.3))), 208 | constmod_p(add(mul(a.2, b.0), mul(a.3, b.2))), 209 | constmod_p(add(mul(a.2, b.1), mul(a.3, b.3))), 210 | ) 211 | } 212 | 213 | fn hex_bytes_to_u128(hex_bytes: &[u8]) -> Result { 214 | let mut hex_bytes = hex_bytes.iter().copied(); 215 | let mut result = [0u8; 16]; 216 | for byte in result.iter_mut() { 217 | let digit1 = hex_digit_to_u8(hex_bytes.next().unwrap())?; 218 | let digit2 = hex_digit_to_u8(hex_bytes.next().unwrap())?; 219 | *byte = (digit1 << 4) | digit2; 220 | } 221 | Ok(u128::from_be_bytes(result)) 222 | } 223 | 224 | fn hex_digit_to_u8(hex_digit: u8) -> Result { 225 | match hex_digit { 226 | b'A'..=b'F' => Ok(hex_digit - b'A' + 10), 227 | b'a'..=b'f' => Ok(hex_digit - b'a' + 10), 228 | b'0'..=b'9' => Ok(hex_digit - b'0'), 229 | _ => Err(format!("invalid hex character: {:?}", hex_digit)), 230 | } 231 | } 232 | 233 | #[cfg(test)] 234 | mod tests { 235 | use super::*; 236 | use crate::*; 237 | 238 | #[test] 239 | fn it_works() { 240 | assert_eq!(mul(1 << 127, 2), U256(1, 0)); 241 | assert_eq!( 242 | mul(1 << 127, 1 << 127), 243 | U256(85070591730234615865843651857942052864, 0) 244 | ); 245 | assert_eq!(mul(4, 4), U256(0, 16)); 246 | assert_eq!( 247 | mul((1 << 127) + 4, (1 << 127) + 4), 248 | U256(85070591730234615865843651857942052868, 16) 249 | ); 250 | 251 | assert_eq!(mod_p(U256(0, P)), 0); 252 | assert_eq!(mod_p(U256(0, P + 1)), 1); 253 | assert_eq!(mod_p(U256(0, 0)), 0); 254 | assert_eq!(mod_p(U256(0, 1)), 1); 255 | assert_eq!(mod_p(U256(0, P - 1)), P - 1); 256 | assert_eq!(mod_p(U256(0, 1 << 127)), 1); 257 | assert_eq!(mod_p(U256(1, P)), 2); 258 | assert_eq!(mod_p(U256(1, 0)), 2); 259 | assert_eq!(mod_p(U256(P, 0)), 0); 260 | assert_eq!(mod_p(U256(P, P)), 0); 261 | assert_eq!(mod_p(U256(0, u128::MAX)), 1); 262 | 263 | assert_eq!( 264 | HashMatrix(1, 0, 0, 1) * HashMatrix(1, 0, 0, 1), 265 | HashMatrix(1, 0, 0, 1) 266 | ); 267 | assert_eq!( 268 | HashMatrix(2, 0, 0, 2) * HashMatrix(2, 0, 0, 2), 269 | HashMatrix(4, 0, 0, 4) 270 | ); 271 | assert_eq!( 272 | HashMatrix(0, 1, 1, 0) * HashMatrix(2, 0, 0, 2), 273 | HashMatrix(0, 2, 2, 0) 274 | ); 275 | assert_eq!( 276 | HashMatrix(0, 1, 1, 0) * HashMatrix(2, 0, 0, 2), 277 | HashMatrix(0, 2, 2, 0) 278 | ); 279 | assert_eq!( 280 | HashMatrix(1, 0, 0, 1) * HashMatrix(P, 0, 0, P), 281 | HashMatrix(0, 0, 0, 0) 282 | ); 283 | assert_eq!( 284 | HashMatrix(1, 0, 0, 1) * HashMatrix(P + 1, P + 5, 2, P), 285 | HashMatrix(1, 5, 2, 0) 286 | ); 287 | assert_eq!( 288 | HashMatrix(P + 1, P + 3, P + 4, P + 5) * HashMatrix(P + 1, P, P, P + 1), 289 | HashMatrix(1, 3, 4, 5) 290 | ); 291 | } 292 | 293 | #[test] 294 | fn test_hex_encoding_and_decoding() { 295 | let hash = HashMatrix(0, 0, 0, 0); 296 | assert_eq!(HashMatrix::from_hex(&hash.to_hex()).unwrap(), hash); 297 | 298 | let hash = HashMatrix(0, 0, 0, 1); 299 | assert_eq!(HashMatrix::from_hex(&hash.to_hex()).unwrap(), hash); 300 | 301 | let hash = HashMatrix(0, 0, 0, 31); 302 | assert_eq!(HashMatrix::from_hex(&hash.to_hex()).unwrap(), hash); 303 | 304 | let hash = HashMatrix(0, 0, 0, 89); 305 | assert_eq!(HashMatrix::from_hex(&hash.to_hex()).unwrap(), hash); 306 | 307 | let hash = HashMatrix(0, 0, 0, 1 << 34); 308 | assert_eq!(HashMatrix::from_hex(&hash.to_hex()).unwrap(), hash); 309 | 310 | let hash = HashMatrix(0, 1 << 31, 0, 1 << 34); 311 | assert_eq!(HashMatrix::from_hex(&hash.to_hex()).unwrap(), hash); 312 | } 313 | 314 | use quickcheck::*; 315 | 316 | quickcheck! { 317 | fn composition(a: Vec, b: Vec) -> bool { 318 | let mut a = a; 319 | let mut b = b; 320 | let h1 = hash(&a) * hash(&b); 321 | a.append(&mut b); 322 | hash(&a) == h1 323 | } 324 | } 325 | 326 | quickcheck! { 327 | fn hex_encoding_and_decoding(bytes: Vec) -> bool { 328 | let hash = hash(&bytes); 329 | HashMatrix::from_hex(&hash.to_hex()).unwrap() == hash 330 | } 331 | } 332 | 333 | quickcheck! { 334 | fn mul_check(a: u128, b: u128) -> bool { 335 | use num_bigint::*; 336 | let res = mul(a, b); 337 | 338 | a.to_biguint().unwrap() * b.to_biguint().unwrap() 339 | == res.to_biguint().unwrap() 340 | } 341 | } 342 | 343 | quickcheck! { 344 | fn add_check(a: u128, b: u128, c: u128, d: u128) -> bool { 345 | let res = add(mul(a, b), mul(c, d)); 346 | 347 | let big_res = a.to_biguint().unwrap() * b.to_biguint().unwrap() 348 | + c.to_biguint().unwrap() * d.to_biguint().unwrap(); 349 | 350 | res.to_biguint().unwrap() == big_res 351 | } 352 | } 353 | 354 | quickcheck! { 355 | fn mod_p_check(a: u128, b: u128, c: u128, d: u128) -> bool { 356 | let res = mod_p(add(mul(a, b), mul(c, d))); 357 | 358 | let big_res = (a.to_biguint().unwrap() * b.to_biguint().unwrap() 359 | + c.to_biguint().unwrap() * d.to_biguint().unwrap()) 360 | % P.to_biguint().unwrap(); 361 | 362 | res.to_biguint().unwrap() == big_res 363 | } 364 | } 365 | 366 | quickcheck! { 367 | fn collision_search(a: Vec, b: Vec) -> bool { 368 | let ares = hash(&a); 369 | let bres = hash(&b); 370 | ares != bres || a == b 371 | } 372 | } 373 | 374 | #[cfg(feature = "std")] 375 | quickcheck! { 376 | fn par_equiv(a: Vec) -> bool { 377 | let h0 = hash(&a); 378 | let h1 = hash_par(&a); 379 | h0 == h1 380 | } 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | This is an implementation of the Tillich-Zémor-style hash function 3 | presented in the paper ["Navigating in the Cayley Graph of SL₂(𝔽ₚ)" 4 | ](https://link.springer.com/article/10.1007%2Fs00233-015-9766-5) by 5 | Bromberg, Shpilrain, and Vdovina. 6 | 7 | > ### Warning 8 | > 9 | > This module is not produced by cryptography experts, but by 10 | > [some random guy](http://benwr.net). Furthermore, the algorithm 11 | > was published in 2017, and is itself not at all battle-tested. Only 12 | > use this library if you either (a) know what you're doing and have 13 | > read and understood our code, and/or (b) are building something that 14 | > does not rely heavily on the cryptographic properties of the hash 15 | > function. 16 | > 17 | > If you _are_ a cryptography expert, we welcome any bug reports or 18 | > pull requests! We also welcome them if you're not a cryptography 19 | > expert; this library is quite simple, and should be easy to grok 20 | > over a coffee with a copy of the paper linked above in hand. 21 | 22 | # What is this library for? 23 | 24 | This library implements a putatively-strong hash function H with the 25 | useful property that it gives a monoid homomorphism. This means there 26 | is a cheap operation `*` such that given strings `s1` and `s2`, 27 | `H(s1 ++ s2) = H(s1) * H(s2)`. 28 | 29 | This property is especially useful for applications where some very 30 | long string may be constructed via many different routes, but you'd 31 | nonetheless like to be able to quickly rule out unequal strings. 32 | 33 | It also allows you to hash _parts_ of your data as you acquire them, 34 | and then merge them later in whatever order is convenient. This allows 35 | for very flexible hashing schemes. 36 | 37 | H has some other cool properties, and is in some limited but 38 | potentially-useful sense "provably secure". See Bromberg et al. for 39 | details. 40 | 41 | # How to use this library 42 | 43 | This library provides the means to construct 44 | [`HashMatrix`](struct.HashMatrix.html)es, using [`hash`](fn.hash.html), 45 | which takes a slice of bytes. These hashes can be compared, 46 | or serialized to hex strings using 47 | [`to_hex`](struct.HashMatrix.html#method.to_hex). 48 | 49 | ``` 50 | use bromberg_sl2::*; 51 | assert_eq!( 52 | hash("hello, world! It's fun to hash stuff!".as_ref()).to_hex(), 53 | "01c5cf590d32654c87228c0d66441b200aec1439e54e724f05cd3c6c260634e565594b61988933e826e9705de22884ce007df0f733a371516ddd4ac9237f7a46"); 54 | ``` 55 | 56 | Hashes may also be composed, using the `*` operator: 57 | 58 | ``` 59 | use bromberg_sl2::*; 60 | assert_eq!( 61 | hash("hello, ".as_ref()) * hash("world!".as_ref()), 62 | hash("hello, world!".as_ref()) 63 | ); 64 | ``` 65 | 66 | # Technical Details 67 | 68 | We use the A(2) and B(2) matrices as generators, and 69 | p = 2^127 - 1 as our prime order, for fast modular arithmetic. 70 | 71 | We have not yet attempted to seriously optimize this library, and 72 | performance is a secondary goal. As of right now our procedure is 73 | about 1/3 as fast as SHA3-512. 74 | 75 | We needed an architecture-agnostic cryptographic hash procedure with a 76 | monoid homomorphism respecting string concatenation, written in a 77 | low-level language. While there are 78 | [a](https://github.com/srijs/hwsl2-core) 79 | [few](https://github.com/nspcc-dev/tzhash) 80 | [implementations](https://github.com/phlegmaticprogrammer/tillich_zemor_hash) 81 | of related algorithms, e.g. the venerable [but broken 82 | ](https://link.springer.com/chapter/10.1007/978-3-642-19574-7_20) Tillich-Zémor hash, 83 | from ["Hashing with SL₂" 84 | ](https://link.springer.com/chapter/10.1007/3-540-48658-5_5), 85 | none of them fulfill these desiderata. 86 | */ 87 | 88 | #![no_std] 89 | 90 | #[macro_use] 91 | extern crate alloc; 92 | 93 | // Re-export digest traits 94 | pub use digest::{ 95 | self, generic_array::GenericArray, Digest, DynDigest, FixedOutput, FixedOutputDirty, Reset, 96 | Update, 97 | }; 98 | 99 | pub use crate::hash_matrix::{constmatmul, HashMatrix}; 100 | 101 | use crate::lookup_table::{BYTE_LOOKUPS, WYDE_LOOKUPS}; 102 | 103 | pub use crate::hash_matrix::I; 104 | 105 | mod hash_matrix; 106 | mod lookup_table; 107 | 108 | /// The main export of this library: Give me a byte 109 | /// stream and I'll give you a hash. 110 | #[must_use] 111 | #[inline] 112 | pub fn hash(bytes: &[u8]) -> HashMatrix { 113 | let mut acc = I; 114 | for bs in bytes.chunks(2) { 115 | if bs.len() == 2 { 116 | acc = acc * WYDE_LOOKUPS[(((bs[0] as usize) << 8) | (bs[1] as usize))]; 117 | } else { 118 | acc = acc * BYTE_LOOKUPS[bs[0] as usize]; 119 | } 120 | } 121 | acc 122 | } 123 | 124 | #[must_use] 125 | #[inline] 126 | #[cfg(feature = "std")] 127 | /// Same as `hash` but computes the hash of the byte stream in parallel. 128 | /// 129 | /// The number of threads used is set automatically but can be overridden using the 130 | /// `RAYON_NUM_THREADS` environment variable 131 | pub fn hash_par(bytes: &[u8]) -> HashMatrix { 132 | use rayon::prelude::*; 133 | 134 | bytes 135 | .par_chunks(2) 136 | .fold( 137 | || I, 138 | |acc, bs| { 139 | if bs.len() == 2 { 140 | acc * WYDE_LOOKUPS[(((bs[0] as usize) << 8) | (bs[1] as usize))] 141 | } else { 142 | acc * BYTE_LOOKUPS[bs[0] as usize] 143 | } 144 | }, 145 | ) 146 | .reduce( 147 | || I, 148 | |mut acc, h| { 149 | acc = acc * h; 150 | acc 151 | }, 152 | ) 153 | } 154 | 155 | /// This procedure implements the same hash function as `hash()`, but 156 | /// with a different performance tradeoff. The first time it's invoked, 157 | /// `hash` computes a 4MiB table of all the hashes for every pair of 158 | /// bytes, which are then used to double hashing speed. For applications 159 | /// that need to do a lot of hashing, this is nearly twice as fast as 160 | /// `hash()`, but it also requires much more memory and initialization 161 | /// time. As a rule of thumb, if you're going to hash less than 100KiB 162 | /// during your program's execution, you should probably use 163 | /// `hash_strict`. 164 | #[must_use] 165 | #[inline] 166 | pub fn hash_strict(bytes: &[u8]) -> HashMatrix { 167 | let mut acc = I; 168 | for b in bytes { 169 | acc = acc * BYTE_LOOKUPS[*b as usize]; 170 | } 171 | acc 172 | } 173 | 174 | /// Things that can be hashed using this crate. 175 | pub trait BrombergHashable { 176 | fn bromberg_hash(&self) -> HashMatrix; 177 | } 178 | 179 | impl BrombergHashable for [u8] { 180 | #[inline] 181 | fn bromberg_hash(&self) -> HashMatrix { 182 | hash(self) 183 | } 184 | } 185 | 186 | impl BrombergHashable for &T { 187 | #[inline] 188 | fn bromberg_hash(&self) -> HashMatrix { 189 | (**self).bromberg_hash() 190 | } 191 | } 192 | 193 | impl BrombergHashable for &mut T { 194 | #[inline] 195 | fn bromberg_hash(&self) -> HashMatrix { 196 | (**self).bromberg_hash() 197 | } 198 | } 199 | 200 | impl BrombergHashable for alloc::boxed::Box { 201 | #[inline] 202 | fn bromberg_hash(&self) -> HashMatrix { 203 | (**self).bromberg_hash() 204 | } 205 | } 206 | 207 | impl BrombergHashable for alloc::rc::Rc { 208 | #[inline] 209 | fn bromberg_hash(&self) -> HashMatrix { 210 | (**self).bromberg_hash() 211 | } 212 | } 213 | 214 | impl Update for HashMatrix { 215 | fn update(&mut self, data: impl AsRef<[u8]>) { 216 | *self = *self * data.as_ref().bromberg_hash(); 217 | } 218 | } 219 | 220 | impl Reset for HashMatrix { 221 | fn reset(&mut self) { 222 | *self = I; 223 | } 224 | } 225 | 226 | impl FixedOutputDirty for HashMatrix { 227 | type OutputSize = digest::consts::U64; 228 | 229 | fn finalize_into_dirty(&mut self, out: &mut GenericArray) { 230 | *out = self.generic_array_digest(); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/lookup_table.rs: -------------------------------------------------------------------------------- 1 | use crate::hash_matrix::{constmatmul, HashMatrix, A, B}; 2 | use alloc::vec::Vec; 3 | use lazy_static::lazy_static; 4 | use seq_macro::seq; 5 | 6 | const BIT_LOOKUPS: [HashMatrix; 2] = [B, A]; 7 | 8 | const fn mul_from_byte(b: u8) -> HashMatrix { 9 | let bit0 = (b & 1) as usize; 10 | let bit1 = ((b & (1 << 1)) >> 1) as usize; 11 | let bit2 = ((b & (1 << 2)) >> 2) as usize; 12 | let bit3 = ((b & (1 << 3)) >> 3) as usize; 13 | let bit4 = ((b & (1 << 4)) >> 4) as usize; 14 | let bit5 = ((b & (1 << 5)) >> 5) as usize; 15 | let bit6 = ((b & (1 << 6)) >> 6) as usize; 16 | let bit7 = ((b & (1 << 7)) >> 7) as usize; 17 | 18 | let m0 = BIT_LOOKUPS[bit0]; 19 | let m1 = BIT_LOOKUPS[bit1]; 20 | let m2 = BIT_LOOKUPS[bit2]; 21 | let m3 = BIT_LOOKUPS[bit3]; 22 | let m4 = BIT_LOOKUPS[bit4]; 23 | let m5 = BIT_LOOKUPS[bit5]; 24 | let m6 = BIT_LOOKUPS[bit6]; 25 | let m7 = BIT_LOOKUPS[bit7]; 26 | 27 | constmatmul( 28 | constmatmul(constmatmul(m7, m6), constmatmul(m5, m4)), 29 | constmatmul(constmatmul(m3, m2), constmatmul(m1, m0)), 30 | ) 31 | } 32 | 33 | const fn mul_from_wyde(d: u16) -> HashMatrix { 34 | constmatmul(mul_from_byte((d >> 8) as u8), mul_from_byte(d as u8)) 35 | } 36 | 37 | pub(crate) static BYTE_LOOKUPS: [HashMatrix; 256] = seq!(N in 0..256 { 38 | [ 39 | #( mul_from_byte(N), )* 40 | ] 41 | }); 42 | 43 | lazy_static! { 44 | pub(crate) static ref WYDE_LOOKUPS: Vec = { 45 | let mut l = Vec::with_capacity(65536); 46 | let mut i: u32 = 0; 47 | while i < 65536 { 48 | l.push(mul_from_wyde(i as u16)); 49 | i += 1; 50 | } 51 | l 52 | }; 53 | } 54 | --------------------------------------------------------------------------------