xcrypt 0.1.2-arm-linux → 0.2.0-arm-linux
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/lib/xcrypt/ffi.rb +1 -0
- data/lib/xcrypt/version.rb +1 -1
- data/lib/xcrypt/yescrypt.rb +219 -0
- data/lib/xcrypt.rb +69 -12
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c50664d757de26c075f277815c6436ead82be015ef4a48570985be3e7e37f401
|
|
4
|
+
data.tar.gz: 874a7514e5936865821f727029ffa910d4c432d5684097e2a941bb8700a0e7f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1faa01f3db6b917674c5d7bc8f2347e8e72a3aed6cc1e3d77860af35f3042233031a1cb4187a245caed5c9565c63142491aae3e5e0aff2d39d5a28ec33ba490f
|
|
7
|
+
data.tar.gz: 6325efa1805094021dcf58e2972bafa17cb90711c857b0ad765e378f21e48bdf0e498f5e909cb694d04c17cbaa22d9145dc1efbdcd212e092afc9e2ebc7ba933
|
data/lib/xcrypt/ffi.rb
CHANGED
data/lib/xcrypt/version.rb
CHANGED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module XCrypt
|
|
6
|
+
# Setting-string generation for the yescrypt and scrypt algorithms.
|
|
7
|
+
#
|
|
8
|
+
# Both algorithms share a base-64 alphabet and encoding scheme taken from
|
|
9
|
+
# libxcrypt's +alg-yescrypt-common.c+. The public methods produce setting
|
|
10
|
+
# strings that can be passed directly to {XCrypt.crypt}.
|
|
11
|
+
#
|
|
12
|
+
# @example Generate a $y$ yescrypt setting
|
|
13
|
+
# setting = XCrypt::Yescrypt.generate_setting(n: 16384, r: 8, p: 1)
|
|
14
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
15
|
+
#
|
|
16
|
+
# @example Generate a $7$ scrypt setting
|
|
17
|
+
# setting = XCrypt::Yescrypt.generate_scrypt_setting(n: 16384, r: 32, p: 1)
|
|
18
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
19
|
+
module Yescrypt
|
|
20
|
+
extend self
|
|
21
|
+
|
|
22
|
+
# -------------------------------------------------------------------------
|
|
23
|
+
# Flag constants — mirrors alg-yescrypt.h
|
|
24
|
+
#
|
|
25
|
+
# These may be OR'd together to form the +flags:+ argument of
|
|
26
|
+
# {generate_setting}, except that {WORM} stands alone (do not combine
|
|
27
|
+
# with {RW}).
|
|
28
|
+
# -------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
# Classic scrypt with minimal extensions (t parameter support only).
|
|
31
|
+
WORM = 0x001
|
|
32
|
+
|
|
33
|
+
# Full yescrypt mode — time-memory tradeoff resistant.
|
|
34
|
+
RW = 0x002
|
|
35
|
+
|
|
36
|
+
# Number of inner rounds.
|
|
37
|
+
ROUNDS_3 = 0x000
|
|
38
|
+
ROUNDS_6 = 0x004
|
|
39
|
+
|
|
40
|
+
# Memory-access gather width.
|
|
41
|
+
GATHER_1 = 0x000
|
|
42
|
+
GATHER_2 = 0x008
|
|
43
|
+
GATHER_4 = 0x010
|
|
44
|
+
GATHER_8 = 0x018
|
|
45
|
+
|
|
46
|
+
# Simple mix factor.
|
|
47
|
+
SIMPLE_1 = 0x000
|
|
48
|
+
SIMPLE_2 = 0x020
|
|
49
|
+
SIMPLE_4 = 0x040
|
|
50
|
+
SIMPLE_8 = 0x060
|
|
51
|
+
|
|
52
|
+
# S-box size.
|
|
53
|
+
SBOX_6K = 0x000
|
|
54
|
+
SBOX_12K = 0x080
|
|
55
|
+
SBOX_24K = 0x100
|
|
56
|
+
SBOX_48K = 0x180
|
|
57
|
+
SBOX_96K = 0x200
|
|
58
|
+
SBOX_192K = 0x280
|
|
59
|
+
SBOX_384K = 0x300
|
|
60
|
+
SBOX_768K = 0x380
|
|
61
|
+
|
|
62
|
+
# Recommended defaults: RW mode with 6 rounds, 4-wide gather, 2x simple
|
|
63
|
+
# mix, and a 12 KiB S-box.
|
|
64
|
+
DEFAULTS = RW | ROUNDS_6 | GATHER_4 | SIMPLE_2 | SBOX_12K
|
|
65
|
+
|
|
66
|
+
# -------------------------------------------------------------------------
|
|
67
|
+
# Public interface
|
|
68
|
+
# -------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
# Generates a +$y$+ yescrypt setting string from explicit parameters.
|
|
71
|
+
#
|
|
72
|
+
# Implements the encoding of libxcrypt's +yescrypt_encode_params_r+
|
|
73
|
+
# (alg-yescrypt-common.c) in pure Ruby, because that function is not
|
|
74
|
+
# exported on Linux.
|
|
75
|
+
#
|
|
76
|
+
# @param n [Integer] memory/CPU cost; must be a power of 2 greater than 1
|
|
77
|
+
# @param r [Integer] block size (default: 8)
|
|
78
|
+
# @param p [Integer] parallelism (default: 1)
|
|
79
|
+
# @param t [Integer] additional time cost (default: 0)
|
|
80
|
+
# @param flags [Integer] yescrypt mode flags; see +WORM+/+RW+/+ROUNDS_*+
|
|
81
|
+
# etc.; defaults to {DEFAULTS}
|
|
82
|
+
# @return [String] a +$y$+ setting string
|
|
83
|
+
# @raise [ArgumentError] if +n+ is not a valid power of 2, or if +flags+
|
|
84
|
+
# is an unsupported combination
|
|
85
|
+
def generate_setting(n:, r: 8, p: 1, t: 0, flags: DEFAULTS)
|
|
86
|
+
n_log2 = log2_of_power_of_2(n)
|
|
87
|
+
|
|
88
|
+
# Compute the "flavor" field exactly as yescrypt_encode_params_r does:
|
|
89
|
+
# flags < RW → flavor = flags (WORM / pure-scrypt modes)
|
|
90
|
+
# flags is valid RW → flavor = RW + (flags >> 2)
|
|
91
|
+
flavor =
|
|
92
|
+
if flags < RW
|
|
93
|
+
flags
|
|
94
|
+
elsif (flags & 0x3) == RW && flags <= (RW | 0x3fc)
|
|
95
|
+
RW + (flags >> 2)
|
|
96
|
+
else
|
|
97
|
+
raise ArgumentError, "invalid yescrypt flags: 0x#{flags.to_s(16)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# "have" bitmask indicates which optional fields follow r.
|
|
101
|
+
have = 0
|
|
102
|
+
have |= 1 if p != 1
|
|
103
|
+
have |= 2 if t != 0
|
|
104
|
+
|
|
105
|
+
setting = +"$y$"
|
|
106
|
+
setting << encode_varint(flavor, 0)
|
|
107
|
+
setting << encode_varint(n_log2, 1)
|
|
108
|
+
setting << encode_varint(r, 1)
|
|
109
|
+
if have != 0
|
|
110
|
+
setting << encode_varint(have, 1)
|
|
111
|
+
setting << encode_varint(p, 2) if p != 1
|
|
112
|
+
setting << encode_varint(t, 1) if t != 0
|
|
113
|
+
end
|
|
114
|
+
setting << "$"
|
|
115
|
+
setting << encode_bytes(SecureRandom.random_bytes(32))
|
|
116
|
+
setting
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generates a +$7$+ scrypt setting string from explicit parameters.
|
|
120
|
+
#
|
|
121
|
+
# Encodes the setting using the same base-64 alphabet and field layout as
|
|
122
|
+
# libxcrypt's +gensalt_scrypt_rn+ (crypt-scrypt.c).
|
|
123
|
+
#
|
|
124
|
+
# @param n [Integer] memory/CPU cost; must be a power of 2 greater than 1
|
|
125
|
+
# @param r [Integer] block size (default: 32)
|
|
126
|
+
# @param p [Integer] parallelism (default: 1)
|
|
127
|
+
# @return [String] a +$7$+ setting string
|
|
128
|
+
# @raise [ArgumentError] if +n+ is not a valid power of 2
|
|
129
|
+
def generate_scrypt_setting(n:, r: 32, p: 1)
|
|
130
|
+
n_log2 = log2_of_power_of_2(n)
|
|
131
|
+
|
|
132
|
+
setting = +"$7$"
|
|
133
|
+
setting << B64[n_log2]
|
|
134
|
+
setting << encode_uint32(r, 30)
|
|
135
|
+
setting << encode_uint32(p, 30)
|
|
136
|
+
setting << encode_bytes(SecureRandom.random_bytes(32))
|
|
137
|
+
setting
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# Base-64 alphabet shared by yescrypt and scrypt (crypt-style, not RFC 4648).
|
|
143
|
+
B64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
144
|
+
|
|
145
|
+
# Returns log2(n) after validating that n is a power of 2 greater than 1.
|
|
146
|
+
def log2_of_power_of_2(n)
|
|
147
|
+
n_log2 = Integer(Math.log2(n).round)
|
|
148
|
+
raise ArgumentError, "n must be a power of 2 greater than 1 (got #{n})" unless (1 << n_log2) == n && n > 1
|
|
149
|
+
n_log2
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Variable-length base-64 encoding for yescrypt parameter fields
|
|
153
|
+
# (encode64_uint32 in alg-yescrypt-common.c, last argument = min).
|
|
154
|
+
#
|
|
155
|
+
# The first character of the output encodes both the number of subsequent
|
|
156
|
+
# characters and the most-significant bits. The character ranges used are:
|
|
157
|
+
# 1 char : indices 0..47 (48 distinct values)
|
|
158
|
+
# 2 chars: indices 48..56 (9 × 64 = 576 additional values)
|
|
159
|
+
# 3 chars: indices 57..60 (4 × 64² = 16 384 additional values), …
|
|
160
|
+
def encode_varint(src, min)
|
|
161
|
+
raise ArgumentError, "value #{src} is below minimum #{min}" if src < min
|
|
162
|
+
|
|
163
|
+
src -= min
|
|
164
|
+
start = 0
|
|
165
|
+
endv = 47
|
|
166
|
+
chars = 1
|
|
167
|
+
bits = 0
|
|
168
|
+
|
|
169
|
+
loop do
|
|
170
|
+
count = (endv + 1 - start) << bits
|
|
171
|
+
break if src < count
|
|
172
|
+
raise ArgumentError, "value too large for yescrypt varint encoding" if start >= 63
|
|
173
|
+
|
|
174
|
+
start = endv + 1
|
|
175
|
+
endv = start + (62 - endv) / 2
|
|
176
|
+
src -= count
|
|
177
|
+
chars += 1
|
|
178
|
+
bits += 6
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
result = +B64[start + (src >> bits)]
|
|
182
|
+
(chars - 1).times { bits -= 6; result << B64[(src >> bits) & 0x3f] }
|
|
183
|
+
result
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Fixed-width base-64 encoding of a 32-bit value using +srcbits+ bits,
|
|
187
|
+
# LSB first (ceil(srcbits/6) output characters).
|
|
188
|
+
# Used for scrypt's r and p fields (encode64_uint32 in crypt-scrypt.c).
|
|
189
|
+
def encode_uint32(value, srcbits)
|
|
190
|
+
out = +""
|
|
191
|
+
bits = 0
|
|
192
|
+
while bits < srcbits
|
|
193
|
+
out << B64[value & 0x3f]
|
|
194
|
+
value >>= 6
|
|
195
|
+
bits += 6
|
|
196
|
+
end
|
|
197
|
+
out
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Encodes a binary string using the fixed-width base-64 scheme, processing
|
|
201
|
+
# 3 bytes (24 bits) into 4 characters at a time (encode64 in both
|
|
202
|
+
# alg-yescrypt-common.c and crypt-scrypt.c).
|
|
203
|
+
def encode_bytes(bytes)
|
|
204
|
+
out = +""
|
|
205
|
+
i = 0
|
|
206
|
+
while i < bytes.bytesize
|
|
207
|
+
value = 0
|
|
208
|
+
bits = 0
|
|
209
|
+
while bits < 24 && i < bytes.bytesize
|
|
210
|
+
value |= bytes.getbyte(i) << bits
|
|
211
|
+
bits += 8
|
|
212
|
+
i += 1
|
|
213
|
+
end
|
|
214
|
+
out << encode_uint32(value, bits)
|
|
215
|
+
end
|
|
216
|
+
out
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
data/lib/xcrypt.rb
CHANGED
|
@@ -16,9 +16,19 @@
|
|
|
16
16
|
#
|
|
17
17
|
# @example Use the generic interface
|
|
18
18
|
# hash = XCrypt.crypt("hunter2", algorithm: :sha512)
|
|
19
|
+
#
|
|
20
|
+
# @example Generate a yescrypt setting with explicit N, r, p, t, and flags
|
|
21
|
+
# setting = XCrypt.generate_setting(:yescrypt, n: 16384, r: 8, p: 1, t: 0,
|
|
22
|
+
# flags: XCrypt::YESCRYPT_DEFAULTS)
|
|
23
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
24
|
+
#
|
|
25
|
+
# @example Generate a scrypt ($7$) setting with explicit N, r, p
|
|
26
|
+
# setting = XCrypt.generate_setting(:scrypt, n: 16384, r: 32, p: 1)
|
|
27
|
+
# hash = XCrypt.crypt("hunter2", setting)
|
|
19
28
|
module XCrypt
|
|
20
29
|
require "xcrypt/version"
|
|
21
30
|
require "xcrypt/ffi"
|
|
31
|
+
require "xcrypt/yescrypt"
|
|
22
32
|
|
|
23
33
|
# Raised when hashing or salt generation fails. Common causes include an
|
|
24
34
|
# unsupported algorithm, a malformed setting string, or a passphrase that
|
|
@@ -122,14 +132,14 @@ module XCrypt
|
|
|
122
132
|
# @raise (see #yescrypt)
|
|
123
133
|
|
|
124
134
|
ALGORITHMS.each_key do |algorithm|
|
|
125
|
-
define_method(algorithm) do |phrase, setting = nil, cost: nil|
|
|
135
|
+
define_method(algorithm) do |phrase, setting = nil, cost: nil, n: nil, r: nil, p: nil, t: nil, flags: nil|
|
|
126
136
|
if setting
|
|
127
137
|
setting_algorithm = detect_algorithm(setting)
|
|
128
138
|
if setting_algorithm != algorithm
|
|
129
139
|
raise ArgumentError, "setting algorithm #{setting_algorithm.inspect} does not match expected #{algorithm.inspect}"
|
|
130
140
|
end
|
|
131
141
|
end
|
|
132
|
-
crypt(phrase, setting, algorithm:, cost:)
|
|
142
|
+
crypt(phrase, setting, algorithm:, cost:, n:, r:, p:, t:, flags:)
|
|
133
143
|
end
|
|
134
144
|
end
|
|
135
145
|
|
|
@@ -161,12 +171,20 @@ module XCrypt
|
|
|
161
171
|
# setting; ignored when +setting+ is already a String
|
|
162
172
|
# @param cost [Integer, nil] work-factor override passed to
|
|
163
173
|
# {generate_setting}; uses the library default when +nil+
|
|
174
|
+
# @param n [Integer, nil] explicit N parameter for yescrypt/scrypt; passed
|
|
175
|
+
# to {generate_setting} when no +setting+ is provided
|
|
176
|
+
# @param r [Integer, nil] explicit r parameter; passed to {generate_setting}
|
|
177
|
+
# @param p [Integer, nil] explicit p parameter; passed to {generate_setting}
|
|
178
|
+
# @param t [Integer, nil] explicit t parameter (yescrypt only); passed to
|
|
179
|
+
# {generate_setting}
|
|
180
|
+
# @param flags [Integer, nil] explicit yescrypt flags; passed to
|
|
181
|
+
# {generate_setting}
|
|
164
182
|
# @return [String] the hashed password
|
|
165
183
|
# @raise [Error] if +crypt_rn+ returns +NULL+, indicating an invalid
|
|
166
184
|
# setting or an unsupported algorithm
|
|
167
|
-
def crypt(phrase, setting = nil, algorithm: nil, cost: nil)
|
|
185
|
+
def crypt(phrase, setting = nil, algorithm: nil, cost: nil, n: nil, r: nil, p: nil, t: nil, flags: nil)
|
|
168
186
|
setting, algorithm = nil, setting if setting.is_a? Symbol
|
|
169
|
-
setting ||= generate_setting(algorithm, cost:)
|
|
187
|
+
setting ||= generate_setting(algorithm, cost:, n:, r:, p:, t:, flags:)
|
|
170
188
|
data = ::FFI::MemoryPointer.new(:uint8, FFI::CRYPT_DATA_SIZE)
|
|
171
189
|
result_ptr = FFI.crypt_rn(phrase, setting, data, FFI::CRYPT_DATA_SIZE)
|
|
172
190
|
raise Error, "crypt failed: invalid setting or unsupported algorithm" if result_ptr.null?
|
|
@@ -196,19 +214,58 @@ module XCrypt
|
|
|
196
214
|
|
|
197
215
|
# Generates a fresh setting string suitable for passing to {crypt}.
|
|
198
216
|
#
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
217
|
+
# When only +algorithm+ and optionally +cost+ are given, delegates to
|
|
218
|
+
# libxcrypt's +crypt_gensalt_rn+, which draws entropy from the OS.
|
|
219
|
+
#
|
|
220
|
+
# When +n+, +r+, +p+, +t+, or +flags+ are supplied the method generates the
|
|
221
|
+
# setting directly from those parameters instead, delegating to
|
|
222
|
+
# {XCrypt::Yescrypt}:
|
|
223
|
+
#
|
|
224
|
+
# * For +:yescrypt+ (and +:gost_yescrypt+): delegates to
|
|
225
|
+
# {XCrypt::Yescrypt.generate_setting}, producing a +$y$+ setting.
|
|
226
|
+
# +n+ must be a power of 2 greater than 1; +r+, +p+, +t+, and +flags+
|
|
227
|
+
# default to 8, 1, 0, and {XCrypt::Yescrypt::DEFAULTS} respectively.
|
|
228
|
+
#
|
|
229
|
+
# * For +:scrypt+: delegates to {XCrypt::Yescrypt.generate_scrypt_setting},
|
|
230
|
+
# producing a +$7$+ setting. +n+ must be a power of 2 (2..2^63); +r+
|
|
231
|
+
# and +p+ default to 32 and 1. +t+ and +flags+ are not used for scrypt.
|
|
232
|
+
#
|
|
233
|
+
# When +algorithm+ is +nil+, the library selects its preferred algorithm.
|
|
202
234
|
#
|
|
203
235
|
# @param algorithm [Symbol, nil] the desired algorithm; uses the library
|
|
204
236
|
# default when +nil+
|
|
205
237
|
# @param cost [Integer, nil] work-factor for the generated setting; a value
|
|
206
|
-
# of +0+ selects the library's own default cost
|
|
238
|
+
# of +0+ selects the library's own default cost; ignored when +n:+ is set
|
|
239
|
+
# @param n [Integer, nil] explicit N (memory/CPU cost, must be a power of 2
|
|
240
|
+
# greater than 1); yescrypt and scrypt only
|
|
241
|
+
# @param r [Integer, nil] block size parameter; yescrypt and scrypt only
|
|
242
|
+
# @param p [Integer, nil] parallelism parameter; yescrypt and scrypt only
|
|
243
|
+
# @param t [Integer, nil] additional time cost; yescrypt only
|
|
244
|
+
# @param flags [Integer, nil] yescrypt mode flags; see {XCrypt::Yescrypt}
|
|
245
|
+
# constants; yescrypt only; defaults to {XCrypt::Yescrypt::DEFAULTS}
|
|
207
246
|
# @return [String] a setting string beginning with the algorithm prefix
|
|
208
|
-
# @raise [ArgumentError] if +algorithm+ is not a key in {ALGORITHMS}
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
247
|
+
# @raise [ArgumentError] if +algorithm+ is not a key in {ALGORITHMS}, or if
|
|
248
|
+
# +n+ is not a power of 2 greater than 1
|
|
249
|
+
# @raise [Error] if the underlying C call returns +NULL+
|
|
250
|
+
def generate_setting(algorithm = nil, cost: nil, n: nil, r: nil, p: nil, t: nil, flags: nil)
|
|
251
|
+
if algorithm
|
|
252
|
+
ALGORITHMS.key?(algorithm) or raise ArgumentError, "unknown algorithm: #{algorithm.inspect}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if n || r || p || t || flags
|
|
256
|
+
case algorithm
|
|
257
|
+
when :yescrypt, :gost_yescrypt, nil
|
|
258
|
+
return Yescrypt.generate_setting(n: n || 4096, r: r || 8, p: p || 1,
|
|
259
|
+
t: t || 0, flags: flags || Yescrypt::DEFAULTS)
|
|
260
|
+
when :scrypt
|
|
261
|
+
return Yescrypt.generate_scrypt_setting(n: n || 16384, r: r || 32, p: p || 1)
|
|
262
|
+
else
|
|
263
|
+
raise ArgumentError,
|
|
264
|
+
"n/r/p/t/flags parameters are only supported for :yescrypt and :scrypt, got #{algorithm.inspect}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
prefix = ALGORITHMS[algorithm]
|
|
212
269
|
cost ||= 0
|
|
213
270
|
|
|
214
271
|
output = ::FFI::MemoryPointer.new(:char, FFI::CRYPT_GENSALT_OUTPUT_SIZE)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: xcrypt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: arm-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Konstantin Haase
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- lib/xcrypt/arm-linux/libxcrypt.so
|
|
65
65
|
- lib/xcrypt/ffi.rb
|
|
66
66
|
- lib/xcrypt/version.rb
|
|
67
|
+
- lib/xcrypt/yescrypt.rb
|
|
67
68
|
homepage: https://github.com/rkh/ruby-xcrypt
|
|
68
69
|
licenses:
|
|
69
70
|
- MIT
|