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 =

Returns:

  • (String)
"0.2.0"

Class Method Summary collapse

Class Method Details

.array_to_bech32(data) ⇒ String

Convert array to bech32 string.

Parameters:

  • data (Array(Integer))

    An array.

Returns:

  • (String)

    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.

Parameters:

  • bech32_str (String)

    bech32 string.

Returns:

  • (Array(Integer))

    array of bech32 data.



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.

Parameters:

  • data (Array)
  • from (Integer)
  • to (Integer)
  • padding (Boolean) (defaults to: true)

Returns:

  • (Array)


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.

Parameters:

  • seed (String)

    Secret with hex format.

  • threshold (Integer) (defaults to: 0)

    Threshold value.

  • id (String)

    Identifier.

  • share_index (String)

    Index of share.

Returns:

Raises:



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.

Parameters:

  • shares (Array(Codex32::Share))

    Array of share.

  • share_index (String)

    A share index.

Returns:

Raises:

  • (ArgumentError)


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.

Parameters:

  • data (Array(Integer))

    A set of shares.

  • x (Integer)

    index value.



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)

Parameters:

  • data (Array(Integer))

Returns:

  • (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.

Parameters:

  • codex32 (String)

    Codex32 string

Returns:

Raises:



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)

Parameters:

  • data (Array(Integer))

Returns:

  • (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.

Parameters:

  • byte_length (Integer)

    Byte length of the payload.

Returns:

  • (String)

    bech32 string.



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.

Parameters:

  • seed (String)

    Secret with hex format.

  • id (String)

    Identifier.

  • threshold (Integer)

    Threshold value. Must be 2 to 9.

  • share_indexes (Array(String))

    Indexes of the shares to be created.

Returns:

Raises:



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.

Parameters:

  • data (Array(Integer))

    A part as a list of integers representing the characters converted.

Returns:

  • (Boolean)


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.

Parameters:

  • seed (String)

    Secret with hex format.

Raises:



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.

Parameters:

Raises:



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