codex32 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -5
- data/.ruby-version +1 -1
- data/CHANGELOG.md +43 -0
- data/Gemfile +1 -1
- data/README.md +30 -1
- data/codex32.gemspec +4 -4
- data/lib/codex32/errors.rb +6 -3
- data/lib/codex32/share.rb +1 -1
- data/lib/codex32/version.rb +1 -1
- data/lib/codex32.rb +122 -9
- metadata +6 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 93df6005aec8470948875ab612155a1c3380bf79774b77f0d6d1a8059e9afa82
|
|
4
|
+
data.tar.gz: c4229ff6918b00717dcd3db5bf1a93f7464b9dabdd7ec7c6d9ebff71a6321d30
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4dc15829c70fcc68c250063312c2d86d0deb30628286c2d72157dc07c44fab1beb2c124f116423bc3b99d79629f34171023e2e30eff7f80a29b7a66e310476df
|
|
7
|
+
data.tar.gz: 1661ba5642dc6c28394b7be554d59bcde96d6bc72103273adf5c04acce56d13234226a647912dbe5cb1d7826541c49f5d9c8ca12c3aa91e1b42b8dcd1b36a796
|
data/.rubocop.yml
CHANGED
|
@@ -4,20 +4,16 @@ require:
|
|
|
4
4
|
inherit_gem:
|
|
5
5
|
prettier: rubocop.yml
|
|
6
6
|
AllCops:
|
|
7
|
-
TargetRubyVersion:
|
|
7
|
+
TargetRubyVersion: 3.0
|
|
8
8
|
RSpec/ExampleLength:
|
|
9
9
|
Enabled: false
|
|
10
10
|
Metrics:
|
|
11
11
|
Enabled: false
|
|
12
|
-
RSpec/MultipleExpectations:
|
|
13
|
-
Max: 5
|
|
14
12
|
Style/IfUnlessModifier:
|
|
15
13
|
Enabled: false
|
|
16
14
|
Style/WhileUntilModifier:
|
|
17
15
|
Enabled: false
|
|
18
16
|
Naming/MethodParameterName:
|
|
19
17
|
Enabled: false
|
|
20
|
-
RSpec/FilePath:
|
|
21
|
-
Enabled: false
|
|
22
18
|
RSpec/MultipleExpectations:
|
|
23
19
|
Enabled: false
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby-
|
|
1
|
+
ruby-4.0.0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- `Codex32.split` splits a master seed into shares using `SecureRandom` and
|
|
6
|
+
verifies that the generated shares recover the seed. Creating shares from
|
|
7
|
+
anything but cryptographically secure randomness leaks the master seed, and
|
|
8
|
+
the library previously offered no way to do it.
|
|
9
|
+
- A "Security considerations" section in the README, covering the lack of
|
|
10
|
+
constant time operations and of memory wiping.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- `Codex32::Errors::SeparatorNotFound`. It was never raised: a string which
|
|
15
|
+
starts with `ms` but has no separator is reported as
|
|
16
|
+
`Codex32::Errors::InvalidHRP`, the same as the invalid test vectors of BIP-93
|
|
17
|
+
which mix a missing prefix and a missing separator.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- `Codex32.generate_share` no longer returns an all-zero share when the requested
|
|
22
|
+
share index collides with the index of one of the given shares. The check was
|
|
23
|
+
comparing a bech32 character with its integer value, so it never matched and
|
|
24
|
+
the Lagrange interpolation silently collapsed to zero. It now raises
|
|
25
|
+
`Codex32::Errors::DuplicateShareIndex`.
|
|
26
|
+
- `Codex32.from` validates the master seed. A non hexadecimal, odd length or out
|
|
27
|
+
of range (128 to 512 bits) seed used to be silently converted into a different
|
|
28
|
+
value by `Array#pack`. It now raises `Codex32::Errors::InvalidSeed`.
|
|
29
|
+
- `Codex32.parse` treats the last `1` as the separator, as BIP-93 requires.
|
|
30
|
+
Appending `1` and arbitrary data to a valid codex32 string is no longer
|
|
31
|
+
accepted, and the length is validated against the data part rather than the
|
|
32
|
+
whole string.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- `Codex32.generate_share` compares the number of shares with the threshold of
|
|
37
|
+
the shares themselves instead of the threshold of the first share, raises
|
|
38
|
+
`Codex32::Errors::PayloadLengthMismatch` for shares with different payload
|
|
39
|
+
lengths, and raises `Codex32::Errors::IdentifierMismatch` (previously a
|
|
40
|
+
`NameError` was raised because of a missing namespace).
|
|
41
|
+
- `Codex32::Share#initialize` accepts an upper case `S` as the secret index.
|
|
42
|
+
- `Codex32.convert_bits` raises `ArgumentError` for a value which does not fit
|
|
43
|
+
in the source bit width instead of returning `nil`, which the callers did not
|
|
44
|
+
check for.
|
|
45
|
+
|
|
3
46
|
## [0.1.0] - 2023-03-12
|
|
4
47
|
|
|
5
48
|
- Initial release
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -29,6 +29,16 @@ share = Codex32.parse("ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw")
|
|
|
29
29
|
# Get share data.
|
|
30
30
|
share.data
|
|
31
31
|
|
|
32
|
+
# Split a master seed into 5 shares, any 3 of which recover the seed.
|
|
33
|
+
# The shares are derived from random values obtained from SecureRandom.
|
|
34
|
+
shares = Codex32.split(
|
|
35
|
+
seed: "ffeeddccbbaa99887766554433221100",
|
|
36
|
+
id: "test",
|
|
37
|
+
threshold: 3,
|
|
38
|
+
share_indexes: %w[a c d e f]
|
|
39
|
+
)
|
|
40
|
+
shares.map(&:to_s)
|
|
41
|
+
|
|
32
42
|
# Recovery master seed using shares.
|
|
33
43
|
share1 = Codex32.parse("MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM")
|
|
34
44
|
share2 = Codex32.parse("MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN")
|
|
@@ -41,4 +51,23 @@ secret.data
|
|
|
41
51
|
share3 = Codex32.generate_share([share1, share2], "d")
|
|
42
52
|
# Obtain bech32 string.
|
|
43
53
|
share3.to_s
|
|
44
|
-
```
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Security considerations
|
|
57
|
+
|
|
58
|
+
Codex32 strings and master seeds are secret material. Keep the following
|
|
59
|
+
limitations in mind.
|
|
60
|
+
|
|
61
|
+
- **Use `Codex32.split` to create shares.** Shares built from anything but
|
|
62
|
+
cryptographically secure randomness leak the master seed. `Codex32.split`
|
|
63
|
+
uses `SecureRandom`.
|
|
64
|
+
- **Verify a backup before relying on it.** Recover the master seed from a
|
|
65
|
+
threshold of the shares and compare it with the original. `Codex32.split`
|
|
66
|
+
performs this check once for the shares it returns.
|
|
67
|
+
- **This library is not constant time.** Character lookups, the field
|
|
68
|
+
arithmetic and the validation errors all depend on the data being processed.
|
|
69
|
+
Run it on a machine you trust, preferably offline, and do not feed it input
|
|
70
|
+
from untrusted parties on a shared host.
|
|
71
|
+
- **Secrets are not erased from memory.** Ruby gives a library no reliable way
|
|
72
|
+
to wipe a string, so master seeds and shares stay in the heap until they are
|
|
73
|
+
garbage collected, and may be copied by the GC or swapped to disk.
|
data/codex32.gemspec
CHANGED
|
@@ -9,14 +9,14 @@ Gem::Specification.new do |spec|
|
|
|
9
9
|
spec.email = ["techmedia.think@gmail.com"]
|
|
10
10
|
|
|
11
11
|
spec.summary = "Ruby implementation of codex32"
|
|
12
|
-
spec.description = spec.
|
|
12
|
+
spec.description = spec.summary
|
|
13
13
|
spec.homepage = "https://github.com/azuchi/codex32rb"
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = ">=
|
|
15
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
16
16
|
|
|
17
17
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
-
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
-
spec.metadata["changelog_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
|
|
20
20
|
|
|
21
21
|
# Specify which files should be added to the gem when it is released.
|
|
22
22
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
data/lib/codex32/errors.rb
CHANGED
|
@@ -22,9 +22,6 @@ module Codex32
|
|
|
22
22
|
class InvalidHRP < Error
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
class SeparatorNotFound < Error
|
|
26
|
-
end
|
|
27
|
-
|
|
28
25
|
class InvalidIdentifier < Error
|
|
29
26
|
end
|
|
30
27
|
|
|
@@ -45,5 +42,11 @@ module Codex32
|
|
|
45
42
|
|
|
46
43
|
class InvalidCase < Error
|
|
47
44
|
end
|
|
45
|
+
|
|
46
|
+
class InvalidSeed < Error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class PayloadLengthMismatch < Error
|
|
50
|
+
end
|
|
48
51
|
end
|
|
49
52
|
end
|
data/lib/codex32/share.rb
CHANGED
data/lib/codex32/version.rb
CHANGED
data/lib/codex32.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
require_relative "codex32/version"
|
|
4
6
|
require_relative "codex32/errors"
|
|
5
7
|
require_relative "codex32/share"
|
|
@@ -22,6 +24,16 @@ module Codex32
|
|
|
22
24
|
|
|
23
25
|
SECRET_INDEX = "s"
|
|
24
26
|
|
|
27
|
+
# Minimum/maximum length of the data part (the part after the separator).
|
|
28
|
+
# 45 = threshold(1) + id(4) + index(1) + payload(26) + checksum(13)
|
|
29
|
+
# 124 = 127 (maximum length of a codex32 string) - "ms1"
|
|
30
|
+
MIN_DATA_LENGTH = 45
|
|
31
|
+
MAX_DATA_LENGTH = 124
|
|
32
|
+
|
|
33
|
+
# Minimum/maximum byte length of a master seed (128 bits to 512 bits).
|
|
34
|
+
MIN_SEED_LENGTH = 16
|
|
35
|
+
MAX_SEED_LENGTH = 64
|
|
36
|
+
|
|
25
37
|
module_function
|
|
26
38
|
|
|
27
39
|
# Parse codex32 string.
|
|
@@ -31,10 +43,14 @@ module Codex32
|
|
|
31
43
|
if codex32.downcase != codex32 && codex32.upcase != codex32
|
|
32
44
|
raise Errors::InvalidCase
|
|
33
45
|
end
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
raise Errors::
|
|
46
|
+
lower = codex32.downcase
|
|
47
|
+
# The separator is the *last* occurrence of SEPARATOR, everything before it is the HRP.
|
|
48
|
+
pos = lower.rindex(SEPARATOR)
|
|
49
|
+
raise Errors::InvalidHRP unless pos && lower[0...pos] == HRP
|
|
50
|
+
remain = lower[(pos + 1)..]
|
|
51
|
+
if remain.length < MIN_DATA_LENGTH || remain.length > MAX_DATA_LENGTH
|
|
52
|
+
raise Errors::InvalidLength
|
|
53
|
+
end
|
|
38
54
|
unless valid_checksum?(bech32_to_array(remain))
|
|
39
55
|
raise Errors::InvalidChecksum
|
|
40
56
|
end
|
|
@@ -60,7 +76,13 @@ module Codex32
|
|
|
60
76
|
def from(seed:, id:, share_index:, threshold: 0)
|
|
61
77
|
raise Errors::InvalidThreshold unless threshold.is_a?(Integer)
|
|
62
78
|
raise Errors::InvalidIdentifier unless id.length == 4
|
|
63
|
-
|
|
79
|
+
unless id.downcase.each_char.all? { |c| CHARSET.include?(c) }
|
|
80
|
+
raise Errors::InvalidBech32Character
|
|
81
|
+
end
|
|
82
|
+
if CHARSET.index(share_index.downcase).nil?
|
|
83
|
+
raise Errors::InvalidBech32Character
|
|
84
|
+
end
|
|
85
|
+
validate_seed!(seed)
|
|
64
86
|
payload =
|
|
65
87
|
array_to_bech32(
|
|
66
88
|
convert_bits([seed].pack("H*").unpack("C*"), 8, 5, padding: true)
|
|
@@ -68,6 +90,87 @@ module Codex32
|
|
|
68
90
|
Share.new(id, threshold, share_index, payload)
|
|
69
91
|
end
|
|
70
92
|
|
|
93
|
+
# Split +seed+ into +share_indexes.length+ shares, any +threshold+ of which
|
|
94
|
+
# recover the seed. The shares are derived from random values obtained from
|
|
95
|
+
# +SecureRandom+.
|
|
96
|
+
# @param [String] seed Secret with hex format.
|
|
97
|
+
# @param [String] id Identifier.
|
|
98
|
+
# @param [Integer] threshold Threshold value. Must be 2 to 9.
|
|
99
|
+
# @param [Array(String)] share_indexes Indexes of the shares to be created.
|
|
100
|
+
# @return [Array(Codex32::Share)] Shares in the order of +share_indexes+.
|
|
101
|
+
def split(seed:, id:, threshold:, share_indexes:)
|
|
102
|
+
unless threshold.is_a?(Integer) && threshold > 1 && threshold < 10
|
|
103
|
+
raise Errors::InvalidThreshold
|
|
104
|
+
end
|
|
105
|
+
unless share_indexes.is_a?(Array)
|
|
106
|
+
raise ArgumentError, "share_indexes must be array."
|
|
107
|
+
end
|
|
108
|
+
indexes = share_indexes.map(&:downcase)
|
|
109
|
+
if indexes.any? { |i| CHARSET.index(i).nil? }
|
|
110
|
+
raise Errors::InvalidBech32Character
|
|
111
|
+
end
|
|
112
|
+
unless indexes.uniq.length == indexes.length
|
|
113
|
+
raise ArgumentError, "Share index duplicate."
|
|
114
|
+
end
|
|
115
|
+
raise Errors::InvalidShareIndex if indexes.include?(SECRET_INDEX)
|
|
116
|
+
raise Errors::InsufficientShares if indexes.length < threshold
|
|
117
|
+
|
|
118
|
+
secret =
|
|
119
|
+
from(seed: seed, id: id, share_index: SECRET_INDEX, threshold: threshold)
|
|
120
|
+
# Any +threshold+ shares define the polynomial, so the secret share and
|
|
121
|
+
# threshold - 1 random shares are enough to derive the remaining ones.
|
|
122
|
+
random_shares =
|
|
123
|
+
indexes
|
|
124
|
+
.take(threshold - 1)
|
|
125
|
+
.map do |i|
|
|
126
|
+
Share.new(id, threshold, i, random_payload(seed.length / 2))
|
|
127
|
+
end
|
|
128
|
+
known = [secret] + random_shares
|
|
129
|
+
shares =
|
|
130
|
+
indexes.map do |i|
|
|
131
|
+
random_shares.find { |s| s.index == i } || generate_share(known, i)
|
|
132
|
+
end
|
|
133
|
+
verify_shares!(shares.take(threshold), secret)
|
|
134
|
+
shares
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Generate a random payload which encodes +byte_length+ bytes.
|
|
138
|
+
# @param [Integer] byte_length Byte length of the payload.
|
|
139
|
+
# @return [String] bech32 string.
|
|
140
|
+
def random_payload(byte_length)
|
|
141
|
+
array_to_bech32(
|
|
142
|
+
convert_bits(
|
|
143
|
+
SecureRandom.random_bytes(byte_length).unpack("C*"),
|
|
144
|
+
8,
|
|
145
|
+
5,
|
|
146
|
+
padding: true
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Check that +shares+ actually recover +secret+.
|
|
152
|
+
# @param [Array(Codex32::Share)] shares Array of share.
|
|
153
|
+
# @param [Codex32::Share] secret Expected secret.
|
|
154
|
+
# @raise [Codex32::Errors::Error]
|
|
155
|
+
def verify_shares!(shares, secret)
|
|
156
|
+
return if generate_share(shares, SECRET_INDEX).to_s == secret.to_s
|
|
157
|
+
raise Errors::Error, "Failed to verify the generated shares."
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Validate that +seed+ is a hex string which encodes a master seed of
|
|
161
|
+
# 128 to 512 bits.
|
|
162
|
+
# @param [String] seed Secret with hex format.
|
|
163
|
+
# @raise [Codex32::Errors::InvalidSeed]
|
|
164
|
+
def validate_seed!(seed)
|
|
165
|
+
unless seed.is_a?(String) && seed.match?(/\A\h*\z/) && seed.length.even?
|
|
166
|
+
raise Errors::InvalidSeed, "seed must be an even-length hex string."
|
|
167
|
+
end
|
|
168
|
+
byte_length = seed.length / 2
|
|
169
|
+
return if byte_length.between?(MIN_SEED_LENGTH, MAX_SEED_LENGTH)
|
|
170
|
+
raise Errors::InvalidSeed,
|
|
171
|
+
"seed must be #{MIN_SEED_LENGTH} to #{MAX_SEED_LENGTH} bytes."
|
|
172
|
+
end
|
|
173
|
+
|
|
71
174
|
# Convert bech32 string to array.
|
|
72
175
|
# @param [String] bech32_str bech32 string.
|
|
73
176
|
# @return [Array(Integer)] array of bech32 data.
|
|
@@ -85,7 +188,8 @@ module Codex32
|
|
|
85
188
|
# @return [Codex32::Share] Recovery secret.
|
|
86
189
|
def generate_share(shares, share_index)
|
|
87
190
|
raise ArgumentError, "shares must be array." unless shares.is_a?(Array)
|
|
88
|
-
raise
|
|
191
|
+
raise ArgumentError, "shares must not be empty." if shares.empty?
|
|
192
|
+
raise Errors::IdentifierMismatch unless shares.map(&:id).uniq.length == 1
|
|
89
193
|
threshold = shares.map(&:threshold).uniq
|
|
90
194
|
threshold.delete(0)
|
|
91
195
|
raise Errors::ThresholdMismatch unless threshold.length == 1
|
|
@@ -95,8 +199,15 @@ module Codex32
|
|
|
95
199
|
unless indices.length == shares.length
|
|
96
200
|
raise ArgumentError, "Share index duplicate."
|
|
97
201
|
end
|
|
98
|
-
|
|
99
|
-
|
|
202
|
+
# The interpolation collapses to all zeros if +index+ collides with any
|
|
203
|
+
# existing share index, so every index must be checked, not just the first.
|
|
204
|
+
if indices.any? { |i| CHARSET.index(i) == index }
|
|
205
|
+
raise Errors::DuplicateShareIndex
|
|
206
|
+
end
|
|
207
|
+
raise Errors::InsufficientShares if shares.length < threshold.first
|
|
208
|
+
unless shares.map { |s| s.payload.length }.uniq.length == 1
|
|
209
|
+
raise Errors::PayloadLengthMismatch
|
|
210
|
+
end
|
|
100
211
|
|
|
101
212
|
data =
|
|
102
213
|
shares.map do |share|
|
|
@@ -171,7 +282,9 @@ module Codex32
|
|
|
171
282
|
maxv = (1 << to) - 1
|
|
172
283
|
max_acc = (1 << (from + to - 1)) - 1
|
|
173
284
|
data.each do |v|
|
|
174
|
-
|
|
285
|
+
if v.negative? || (v >> from) != 0
|
|
286
|
+
raise ArgumentError, "#{v} does not fit in #{from} bits."
|
|
287
|
+
end
|
|
175
288
|
acc = ((acc << from) | v) & max_acc
|
|
176
289
|
bits += from
|
|
177
290
|
while bits >= to
|
metadata
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: codex32
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- azuchi
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies: []
|
|
13
|
-
description:
|
|
12
|
+
description: Ruby implementation of codex32
|
|
14
13
|
email:
|
|
15
14
|
- techmedia.think@gmail.com
|
|
16
15
|
executables: []
|
|
@@ -42,8 +41,7 @@ licenses:
|
|
|
42
41
|
metadata:
|
|
43
42
|
homepage_uri: https://github.com/azuchi/codex32rb
|
|
44
43
|
source_code_uri: https://github.com/azuchi/codex32rb
|
|
45
|
-
changelog_uri: https://github.com/azuchi/codex32rb
|
|
46
|
-
post_install_message:
|
|
44
|
+
changelog_uri: https://github.com/azuchi/codex32rb/blob/master/CHANGELOG.md
|
|
47
45
|
rdoc_options: []
|
|
48
46
|
require_paths:
|
|
49
47
|
- lib
|
|
@@ -51,15 +49,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
51
49
|
requirements:
|
|
52
50
|
- - ">="
|
|
53
51
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
52
|
+
version: 3.0.0
|
|
55
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
54
|
requirements:
|
|
57
55
|
- - ">="
|
|
58
56
|
- !ruby/object:Gem::Version
|
|
59
57
|
version: '0'
|
|
60
58
|
requirements: []
|
|
61
|
-
rubygems_version:
|
|
62
|
-
signing_key:
|
|
59
|
+
rubygems_version: 4.0.3
|
|
63
60
|
specification_version: 4
|
|
64
61
|
summary: Ruby implementation of codex32
|
|
65
62
|
test_files: []
|