Module: Baseh::Feistel
- Defined in:
- lib/baseh/feistel.rb
Overview
Balanced Feistel network with cycle walking, spec section 7.3. HMAC-SHA-256 comes from OpenSSL; HMAC and SHA-256 are never implemented by hand (section 7.5).
Constant Summary collapse
- TAG =
"BASEH-FEISTEL-V1".b
- MAX_WALKS =
1000
Class Method Summary collapse
-
.bit_length(capacity) ⇒ Object
ceil(log2(capacity)); capacity >= 2 so bits >= 1.
- .hmac(key_bytes, message) ⇒ Object
-
.inverse_permute(value, capacity, profile_id:, key_bytes:, rounds:) ⇒ Object
Inverse permutation with cycle walking.
-
.low_bits(digest, n) ⇒ Object
Low n bits of the HMAC-SHA-256 digest: first ceil(n / 8) bytes read as a big-endian integer and masked with 2^n - 1.
-
.permute(value, capacity, profile_id:, key_bytes:, rounds:) ⇒ Object
Forward permutation with cycle walking.
-
.round_message(profile_id, round, right, wr) ⇒ Object
Normative round message, spec 7.3 step 4.
- .run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1) ⇒ Object
- .run_rounds(left, right, profile_id, key_bytes, rounds, w0, w1) ⇒ Object
- .to_be(value, byte_count) ⇒ Object
- .walk(value, capacity, profile_id, key_bytes, rounds) ⇒ Object
Class Method Details
.bit_length(capacity) ⇒ Object
ceil(log2(capacity)); capacity >= 2 so bits >= 1.
16 17 18 |
# File 'lib/baseh/feistel.rb', line 16 def bit_length(capacity) (capacity - 1).bit_length end |
.hmac(key_bytes, message) ⇒ Object
51 52 53 |
# File 'lib/baseh/feistel.rb', line 51 def hmac(key_bytes, ) OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"), key_bytes, ) end |
.inverse_permute(value, capacity, profile_id:, key_bytes:, rounds:) ⇒ Object
Inverse permutation with cycle walking.
91 92 93 94 95 |
# File 'lib/baseh/feistel.rb', line 91 def inverse_permute(value, capacity, profile_id:, key_bytes:, rounds:) walk(value, capacity, profile_id, key_bytes, rounds) do |left, right, w0, w1| run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1) end end |
.low_bits(digest, n) ⇒ Object
Low n bits of the HMAC-SHA-256 digest: first ceil(n / 8) bytes read as a big-endian integer and masked with 2^n - 1.
22 23 24 25 26 |
# File 'lib/baseh/feistel.rb', line 22 def low_bits(digest, n) byte_count = (n + 7) / 8 v = digest.byteslice(0, byte_count).unpack("C*").inject(0) { |acc, b| (acc << 8) | b } v & ((1 << n) - 1) end |
.permute(value, capacity, profile_id:, key_bytes:, rounds:) ⇒ Object
Forward permutation with cycle walking.
84 85 86 87 88 |
# File 'lib/baseh/feistel.rb', line 84 def permute(value, capacity, profile_id:, key_bytes:, rounds:) walk(value, capacity, profile_id, key_bytes, rounds) do |left, right, w0, w1| run_rounds(left, right, profile_id, key_bytes, rounds, w0, w1) end end |
.round_message(profile_id, round, right, wr) ⇒ Object
Normative round message, spec 7.3 step 4.
41 42 43 44 45 46 47 48 49 |
# File 'lib/baseh/feistel.rb', line 41 def (profile_id, round, right, wr) "".b .concat(TAG) .concat(0.chr(Encoding::BINARY)) .concat(profile_id) .concat(0.chr(Encoding::BINARY)) .concat(round.chr(Encoding::BINARY)) .concat(to_be(right, (wr + 7) / 8)) end |
.run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/baseh/feistel.rb', line 69 def run_inverse(left, right, profile_id, key_bytes, rounds, w0, w1) (rounds - 1).downto(0) do |i| even = i.even? wr = even ? w1 : w0 wl = even ? w0 : w1 f = low_bits(hmac(key_bytes, (profile_id, i, left, wr)), wl) prev_right = left prev_left = right ^ f left = prev_left right = prev_right end [left, right] end |
.run_rounds(left, right, profile_id, key_bytes, rounds, w0, w1) ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/baseh/feistel.rb', line 55 def run_rounds(left, right, profile_id, key_bytes, rounds, w0, w1) rounds.times do |i| even = i.even? wr = even ? w1 : w0 wl = even ? w0 : w1 f = low_bits(hmac(key_bytes, (profile_id, i, right, wr)), wl) new_left = right new_right = left ^ f left = new_left right = new_right end [left, right] end |
.to_be(value, byte_count) ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/baseh/feistel.rb', line 28 def to_be(value, byte_count) return "".b if byte_count.zero? bytes = Array.new(byte_count) v = value (byte_count - 1).downto(0) do |i| bytes[i] = v & 0xff v >>= 8 end bytes.pack("C*") end |
.walk(value, capacity, profile_id, key_bytes, rounds) ⇒ Object
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/baseh/feistel.rb', line 97 def walk(value, capacity, profile_id, key_bytes, rounds) bits = bit_length(capacity) w1 = bits / 2 w0 = bits - w1 v = value MAX_WALKS.times do left = v >> w1 right = v & ((1 << w1) - 1) out_left, out_right = yield(left, right, w0, w1) combined = (out_left << w1) | out_right return combined if combined < capacity v = combined end raise BasehError.new( "PERMUTATION_FAILURE", "Feistel cycle walking exceeded 1000 iterations", safe_for_customer: false ) end |