Module: Codex32
- Included in:
- Share
- Defined in:
- lib/codex32.rb,
lib/codex32/share.rb,
lib/codex32/errors.rb,
lib/codex32/version.rb,
sig/codex32.rbs
Overview
Codex32 library.
Defined Under Namespace
Modules: Errors Classes: Share
Constant Summary collapse
- HRP =
"ms"- SEPARATOR =
"1"- CHARSET =
stree-ignore
%w[q p z r y 9 x 8 g f 2 t v d w 0 s 3 j n 5 4 k h c e 6 m u a 7 l].freeze
- BECH32_INV =
stree-ignore
[0, 1, 20, 24, 10, 8, 12, 29, 5, 11, 4, 9, 6, 28, 26, 31, 22, 18, 17, 23, 2, 25, 16, 19, 3, 21, 14, 30, 13, 7, 27, 15].freeze
- MS32_CONST =
0x10ce0795c2fd1e62a- MS32_LONG_CONST =
0x43381e570bf4798ab26- SECRET_INDEX =
"s"- MIN_DATA_LENGTH =
Minimum/maximum length of the data part (the part after the separator). 45 = threshold(1) + id(4) + index(1) + payload(26) + checksum(13) 124 = 127 (maximum length of a codex32 string) - "ms1"
45- MAX_DATA_LENGTH =
124- MIN_SEED_LENGTH =
Minimum/maximum byte length of a master seed (128 bits to 512 bits).
16- MAX_SEED_LENGTH =
64- VERSION =
"0.2.0"
Class Method Summary collapse
-
.array_to_bech32(data) ⇒ String
Convert array to bech32 string.
- .bech32_lagrange(data, x) ⇒ Object
- .bech32_mul(a, b) ⇒ Object
-
.bech32_to_array(bech32_str) ⇒ Array(Integer)
Convert bech32 string to array.
-
.convert_bits(data, from, to, padding: true) ⇒ Array
Convert a
datawhere each byte is encodingfrombits to a byte slice where each byte is encodingtobits. -
.from(seed:, id:, share_index:, threshold: 0) ⇒ Codex32::Share
Create codex32 string.
-
.generate_share(shares, share_index) ⇒ Codex32::Share
Recover secret using
shares. -
.interpolate_at(data, x) ⇒ Object
Interpolate a set of shares to derive a share at a specific index.
- .long_polymod(data) ⇒ Array(Integer)
-
.parse(codex32) ⇒ Codex32::Share
Parse codex32 string.
- .polymod(data) ⇒ Array(Integer)
-
.random_payload(byte_length) ⇒ String
Generate a random payload which encodes
byte_lengthbytes. -
.split(seed:, id:, threshold:, share_indexes:) ⇒ Array(Codex32::Share)
Split
seedintoshare_indexes.lengthshares, anythresholdof which recover the seed. -
.valid_checksum?(data) ⇒ Boolean
Check whether checksum is valid or not.
-
.validate_seed!(seed) ⇒ Object
Validate that
seedis a hex string which encodes a master seed of 128 to 512 bits. -
.verify_shares!(shares, secret) ⇒ Object
Check that
sharesactually recoversecret.
Class Method Details
.array_to_bech32(data) ⇒ String
Convert array to bech32 string.
230 231 232 |
# File 'lib/codex32.rb', line 230 def array_to_bech32(data) data.map { |d| CHARSET[d] }.join end |
.bech32_lagrange(data, x) ⇒ Object
327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/codex32.rb', line 327 def bech32_lagrange(data, x) n = 1 c = [] data.each do |i| n = bech32_mul(n, i ^ x) m = 1 data.each { |j| m = bech32_mul(m, (i == j ? x : i) ^ j) } c << m end c.map { |i| bech32_mul(n, BECH32_INV[i]) } end |
.bech32_mul(a, b) ⇒ Object
339 340 341 342 343 344 345 346 347 |
# File 'lib/codex32.rb', line 339 def bech32_mul(a, b) result = 0 5.times do |i| result ^= ((b >> i) & 1).zero? ? 0 : a a *= 2 a ^= a >= 32 ? 41 : 0 end result end |
.bech32_to_array(bech32_str) ⇒ Array(Integer)
Convert bech32 string to array.
177 178 179 180 181 182 183 |
# File 'lib/codex32.rb', line 177 def bech32_to_array(bech32_str) bech32_str.downcase.each_char.map do |c| i = CHARSET.index(c) raise Errors::InvalidBech32Character if i.nil? i end end |
.convert_bits(data, from, to, padding: true) ⇒ Array
Convert a data where each byte is encoding from bits to a byte slice where each byte is encoding to bits.
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/codex32.rb', line 278 def convert_bits(data, from, to, padding: true) acc = 0 bits = 0 ret = [] maxv = (1 << to) - 1 max_acc = (1 << (from + to - 1)) - 1 data.each do |v| if v.negative? || (v >> from) != 0 raise ArgumentError, "#{v} does not fit in #{from} bits." end acc = ((acc << from) | v) & max_acc bits += from while bits >= to bits -= to ret << ((acc >> bits) & maxv) end end ret << ((acc << (to - bits)) & maxv) if padding && bits != 0 ret end |
.from(seed:, id:, share_index:, threshold: 0) ⇒ Codex32::Share
Create codex32 string.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
# File 'lib/codex32.rb', line 76 def from(seed:, id:, share_index:, threshold: 0) raise Errors::InvalidThreshold unless threshold.is_a?(Integer) raise Errors::InvalidIdentifier unless id.length == 4 unless id.downcase.each_char.all? { |c| CHARSET.include?(c) } raise Errors::InvalidBech32Character end if CHARSET.index(share_index.downcase).nil? raise Errors::InvalidBech32Character end validate_seed!(seed) payload = array_to_bech32( convert_bits([seed].pack("H*").unpack("C*"), 8, 5, padding: true) ) Share.new(id, threshold, share_index, payload) end |
.generate_share(shares, share_index) ⇒ Codex32::Share
Recover secret using shares.
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/codex32.rb', line 189 def generate_share(shares, share_index) raise ArgumentError, "shares must be array." unless shares.is_a?(Array) raise ArgumentError, "shares must not be empty." if shares.empty? raise Errors::IdentifierMismatch unless shares.map(&:id).uniq.length == 1 threshold = shares.map(&:threshold).uniq threshold.delete(0) raise Errors::ThresholdMismatch unless threshold.length == 1 index = CHARSET.index(share_index.downcase) raise Errors::InvalidBech32Character if index.nil? indices = shares.map(&:index).uniq unless indices.length == shares.length raise ArgumentError, "Share index duplicate." end # The interpolation collapses to all zeros if +index+ collides with any # existing share index, so every index must be checked, not just the first. if indices.any? { |i| CHARSET.index(i) == index } raise Errors::DuplicateShareIndex end raise Errors::InsufficientShares if shares.length < threshold.first unless shares.map { |s| s.payload.length }.uniq.length == 1 raise Errors::PayloadLengthMismatch end data = shares.map do |share| bech32_to_array( share.threshold.to_s + share.id + share.index + share.payload ) end result = interpolate_at(data, index) Share.new( shares.first.id, threshold.first, CHARSET[result[5]], array_to_bech32(result[6..]) ) end |
.interpolate_at(data, x) ⇒ Object
Interpolate a set of shares to derive a share at a specific index. Each share is an array of the following data transformed in a bech32 table: threshold + id + index + payload.
304 305 306 307 308 309 310 311 312 |
# File 'lib/codex32.rb', line 304 def interpolate_at(data, x) indices = data.map { |d| d[5] } w = bech32_lagrange(indices, x) data.first.length.times.map do |i| n = 0 data.length.times { |j| n ^= bech32_mul(w[j], data[j][i]) } n end end |
.long_polymod(data) ⇒ Array(Integer)
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/codex32.rb', line 255 def long_polymod(data) generators = [ 0x3d59d273535ea62d897, 0x7a9becb6361c6c51507, 0x543f9b7e6c38d8a2a0e, 0x0c577eaeccf1990d13c, 0x1887f74f8dc71b10651 ] residue = 0x23181b3 data.each do |d| b = residue >> 70 residue = (residue & 0x3fffffffffffffffff) << 5 ^ d 5.times { |i| residue ^= ((b >> i) & 1).zero? ? 0 : generators[i] } end residue end |
.parse(codex32) ⇒ Codex32::Share
Parse codex32 string.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/codex32.rb', line 42 def parse(codex32) if codex32.downcase != codex32 && codex32.upcase != codex32 raise Errors::InvalidCase end lower = codex32.downcase # The separator is the *last* occurrence of SEPARATOR, everything before it is the HRP. pos = lower.rindex(SEPARATOR) raise Errors::InvalidHRP unless pos && lower[0...pos] == HRP remain = lower[(pos + 1)..] if remain.length < MIN_DATA_LENGTH || remain.length > MAX_DATA_LENGTH raise Errors::InvalidLength end unless valid_checksum?(bech32_to_array(remain)) raise Errors::InvalidChecksum end checksum_len = remain.chars.length <= 93 ? 13 : 15 remain = remain.chars threshold = remain[0].to_i raise Errors::InvalidThreshold unless threshold.to_s == remain[0] id = remain[1..4].join share_index = remain[5] payload_end = remain.length - checksum_len payload = remain[6...payload_end].join Share.new(id, threshold, share_index, payload) end |
.polymod(data) ⇒ Array(Integer)
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 |
# File 'lib/codex32.rb', line 236 def polymod(data) generators = [ 0x19dc500ce73fde210, 0x1bfae00def77fe529, 0x1fbd920fffe7bee52, 0x1739640bdeee3fdad, 0x07729a039cfc75f5a ] residue = 0x23181b3 data.each do |d| b = residue >> 60 residue = (residue & 0x0fffffffffffffff) << 5 ^ d 5.times { |i| residue ^= ((b >> i) & 1).zero? ? 0 : generators[i] } end residue end |
.random_payload(byte_length) ⇒ String
Generate a random payload which encodes byte_length bytes.
140 141 142 143 144 145 146 147 148 149 |
# File 'lib/codex32.rb', line 140 def random_payload(byte_length) array_to_bech32( convert_bits( SecureRandom.random_bytes(byte_length).unpack("C*"), 8, 5, padding: true ) ) end |
.split(seed:, id:, threshold:, share_indexes:) ⇒ Array(Codex32::Share)
Split seed into share_indexes.length shares, any threshold of which
recover the seed. The shares are derived from random values obtained from
SecureRandom.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/codex32.rb', line 101 def split(seed:, id:, threshold:, share_indexes:) unless threshold.is_a?(Integer) && threshold > 1 && threshold < 10 raise Errors::InvalidThreshold end unless share_indexes.is_a?(Array) raise ArgumentError, "share_indexes must be array." end indexes = share_indexes.map(&:downcase) if indexes.any? { |i| CHARSET.index(i).nil? } raise Errors::InvalidBech32Character end unless indexes.uniq.length == indexes.length raise ArgumentError, "Share index duplicate." end raise Errors::InvalidShareIndex if indexes.include?(SECRET_INDEX) raise Errors::InsufficientShares if indexes.length < threshold secret = from(seed: seed, id: id, share_index: SECRET_INDEX, threshold: threshold) # Any +threshold+ shares define the polynomial, so the secret share and # threshold - 1 random shares are enough to derive the remaining ones. random_shares = indexes .take(threshold - 1) .map do |i| Share.new(id, threshold, i, random_payload(seed.length / 2)) end known = [secret] + random_shares shares = indexes.map do |i| random_shares.find { |s| s.index == i } || generate_share(known, i) end verify_shares!(shares.take(threshold), secret) shares end |
.valid_checksum?(data) ⇒ Boolean
Check whether checksum is valid or not.
317 318 319 320 321 322 323 324 325 |
# File 'lib/codex32.rb', line 317 def valid_checksum?(data) if data.length <= 93 polymod(data) == MS32_CONST elsif data.length >= 96 long_polymod(data) == MS32_LONG_CONST else false end end |
.validate_seed!(seed) ⇒ Object
Validate that seed is a hex string which encodes a master seed of
128 to 512 bits.
164 165 166 167 168 169 170 171 172 |
# File 'lib/codex32.rb', line 164 def validate_seed!(seed) unless seed.is_a?(String) && seed.match?(/\A\h*\z/) && seed.length.even? raise Errors::InvalidSeed, "seed must be an even-length hex string." end byte_length = seed.length / 2 return if byte_length.between?(MIN_SEED_LENGTH, MAX_SEED_LENGTH) raise Errors::InvalidSeed, "seed must be #{MIN_SEED_LENGTH} to #{MAX_SEED_LENGTH} bytes." end |
.verify_shares!(shares, secret) ⇒ Object
Check that shares actually recover secret.
155 156 157 158 |
# File 'lib/codex32.rb', line 155 def verify_shares!(shares, secret) return if generate_share(shares, SECRET_INDEX).to_s == secret.to_s raise Errors::Error, "Failed to verify the generated shares." end |