xcrypt 0.1.2-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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21eca2c864456dc81c260c68c09042df2d5977ab4b1e0a7fbc6ecd14a6932ef8
4
+ data.tar.gz: 3694326527500e11e5a9b67635bf2a394b6c07a7214b56e2c5569c4be464f662
5
+ SHA512:
6
+ metadata.gz: 4bd2246605e4f54a9e6fbeb803676e32fb7bf7d8b17e7abe4f2278de58ad18c7864879e8dfff41941e33e18c3a80ac82a24c0edc048b684a54fdd15e5f500d82
7
+ data.tar.gz: 5cf3b11c7d3c5bbd9352b5d564a5d3c527e4ee00f944aa45df79fc11352461ed027f3f47e678ee486ee7b1f4854a9c5d685dfddae285b215676eb45d3014d3a8
Binary file
data/lib/xcrypt/ffi.rb ADDED
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module XCrypt
6
+ # Low-level FFI bindings for libxcrypt.
7
+ #
8
+ # Consumers should use the high-level {XCrypt} module methods instead of
9
+ # calling into this module directly.
10
+ #
11
+ # The shared library loaded here is built from the libxcrypt submodule
12
+ # (ext/libxcrypt). Run <tt>bundle exec rake compile</tt> to build it
13
+ # before loading this gem.
14
+ module FFI
15
+ extend ::FFI::Library
16
+
17
+ # Size constants mirrored from <crypt.h>.
18
+ CRYPT_OUTPUT_SIZE = 384
19
+ CRYPT_MAX_PASSPHRASE_SIZE = 512
20
+ CRYPT_GENSALT_OUTPUT_SIZE = 192
21
+ CRYPT_DATA_RESERVED_SIZE = 767
22
+ CRYPT_DATA_INTERNAL_SIZE = 30_720
23
+
24
+ # sizeof(struct crypt_data) == 32768 bytes, as guaranteed by the header.
25
+ CRYPT_DATA_SIZE = 32_768
26
+
27
+ # crypt_checksalt(3) return codes.
28
+ CRYPT_SALT_OK = 0
29
+ CRYPT_SALT_INVALID = 1
30
+ CRYPT_SALT_METHOD_DISABLED = 2
31
+ CRYPT_SALT_METHOD_LEGACY = 3
32
+ CRYPT_SALT_TOO_CHEAP = 4
33
+
34
+ # Load the shared library built from the libxcrypt submodule.
35
+ # It lives in a platform-specific subdirectory next to this file.
36
+ begin
37
+ _ext = ::FFI::Platform.mac? ? "bundle" : "so"
38
+ _lib = File.expand_path(
39
+ "../#{::FFI::Platform::ARCH}-#{::FFI::Platform::OS}/libxcrypt.#{_ext}",
40
+ __FILE__
41
+ )
42
+ ffi_lib _lib
43
+ rescue LoadError
44
+ raise LoadError,
45
+ "XCrypt native extension not found. " \
46
+ "Build it with: rake compile"
47
+ end
48
+
49
+ # char *crypt_rn(const char *phrase, const char *setting,
50
+ # void *data, int size)
51
+ #
52
+ # Thread-safe variant that writes to a caller-supplied buffer. Returns
53
+ # NULL on error instead of a magic error string.
54
+ attach_function :crypt_rn, [:string, :string, :pointer, :int], :pointer
55
+
56
+ # char *crypt_gensalt_rn(const char *prefix, unsigned long count,
57
+ # const char *rbytes, int nrbytes,
58
+ # char *output, int output_size)
59
+ #
60
+ # Thread-safe salt generator that writes to a caller-supplied buffer.
61
+ # Pass NULL for rbytes to let the library obtain entropy from the OS.
62
+ attach_function :crypt_gensalt_rn,
63
+ [:string, :ulong, :pointer, :int, :pointer, :int],
64
+ :pointer
65
+
66
+ # int crypt_checksalt(const char *setting)
67
+ #
68
+ # Inspects a setting string and reports whether it is valid.
69
+ attach_function :crypt_checksalt, [:string], :int
70
+
71
+ # const char *crypt_preferred_method(void)
72
+ #
73
+ # Returns the prefix string of the library's preferred (strongest)
74
+ # hashing method.
75
+ attach_function :crypt_preferred_method, [], :string
76
+ end
77
+
78
+ private_constant :FFI
79
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XCrypt
4
+ VERSION = "0.1.2"
5
+ end
data/lib/xcrypt.rb ADDED
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Top-level module providing a high-level Ruby interface to libxcrypt, a
4
+ # modern library for one-way hashing of passwords.
5
+ #
6
+ # All public methods are available directly on the module.
7
+ # The most common entry points are the algorithm-specific convenience methods
8
+ # ({yescrypt}, {bcrypt}, {sha512}, etc.) and {verify}.
9
+ #
10
+ # @example Hash a password with yescrypt (the strongest supported algorithm)
11
+ # hash = XCrypt.yescrypt("correct horse battery staple")
12
+ # XCrypt.verify("correct horse battery staple", hash) #=> true
13
+ #
14
+ # @example Hash with an explicit cost factor
15
+ # hash = XCrypt.bcrypt("hunter2", cost: 12)
16
+ #
17
+ # @example Use the generic interface
18
+ # hash = XCrypt.crypt("hunter2", algorithm: :sha512)
19
+ module XCrypt
20
+ require "xcrypt/version"
21
+ require "xcrypt/ffi"
22
+
23
+ # Raised when hashing or salt generation fails. Common causes include an
24
+ # unsupported algorithm, a malformed setting string, or a passphrase that
25
+ # exceeds {FFI::CRYPT_MAX_PASSPHRASE_SIZE} bytes.
26
+ Error ||= Class.new(StandardError)
27
+
28
+ # Maps each supported algorithm name to its setting-string prefix.
29
+ #
30
+ # The prefix is the leading characters of any hash produced by that
31
+ # algorithm and is used to identify the algorithm from an existing hash.
32
+ #
33
+ # @return [Hash{Symbol => String}]
34
+ ALGORITHMS = {
35
+ yescrypt: "$y$",
36
+ gost_yescrypt: "$gy$",
37
+ scrypt: "$7$",
38
+ bcrypt: "$2b$",
39
+ sha512: "$6$",
40
+ sha256: "$5$",
41
+ sha1: "$sha1$",
42
+ sun_md5: "$md5",
43
+ md5: "$1$",
44
+ bsdi_des: "_",
45
+ des: "",
46
+ }.freeze
47
+
48
+ PREFIXES = ALGORITHMS.invert.freeze
49
+ private_constant :ALGORITHMS, :PREFIXES
50
+
51
+ extend self
52
+
53
+ # @!method yescrypt(phrase, setting = nil, cost: nil)
54
+ # Hash +phrase+ using yescrypt, the strongest supported algorithm.
55
+ # @param phrase [String] the password to hash
56
+ # @param setting [String, nil] an existing hash or salt string to use as
57
+ # the setting; a new setting is generated automatically when +nil+
58
+ # @param cost [Integer, nil] work-factor override; uses the library
59
+ # default when +nil+
60
+ # @return [String] the hashed password
61
+ # @raise [ArgumentError] if +setting+ belongs to a different algorithm
62
+ # @raise [Error] if hashing fails
63
+
64
+ # @!method gost_yescrypt(phrase, setting = nil, cost: nil)
65
+ # Hash +phrase+ using GOST R 34.11-2012 combined with yescrypt.
66
+ # @param (see #yescrypt)
67
+ # @return (see #yescrypt)
68
+ # @raise (see #yescrypt)
69
+
70
+ # @!method scrypt(phrase, setting = nil, cost: nil)
71
+ # Hash +phrase+ using scrypt.
72
+ # @param (see #yescrypt)
73
+ # @return (see #yescrypt)
74
+ # @raise (see #yescrypt)
75
+
76
+ # @!method bcrypt(phrase, setting = nil, cost: nil)
77
+ # Hash +phrase+ using bcrypt (Blowfish-based password hashing).
78
+ # @param (see #yescrypt)
79
+ # @return (see #yescrypt)
80
+ # @raise (see #yescrypt)
81
+
82
+ # @!method sha512(phrase, setting = nil, cost: nil)
83
+ # Hash +phrase+ using SHA-512 crypt.
84
+ # @param (see #yescrypt)
85
+ # @return (see #yescrypt)
86
+ # @raise (see #yescrypt)
87
+
88
+ # @!method sha256(phrase, setting = nil, cost: nil)
89
+ # Hash +phrase+ using SHA-256 crypt.
90
+ # @param (see #yescrypt)
91
+ # @return (see #yescrypt)
92
+ # @raise (see #yescrypt)
93
+
94
+ # @!method sha1(phrase, setting = nil, cost: nil)
95
+ # Hash +phrase+ using HMAC-SHA1 NetBSD crypt.
96
+ # @param (see #yescrypt)
97
+ # @return (see #yescrypt)
98
+ # @raise (see #yescrypt)
99
+
100
+ # @!method sun_md5(phrase, setting = nil, cost: nil)
101
+ # Hash +phrase+ using SunMD5 (Solaris MD5 crypt).
102
+ # @param (see #yescrypt)
103
+ # @return (see #yescrypt)
104
+ # @raise (see #yescrypt)
105
+
106
+ # @!method md5(phrase, setting = nil, cost: nil)
107
+ # Hash +phrase+ using MD5 crypt.
108
+ # @param (see #yescrypt)
109
+ # @return (see #yescrypt)
110
+ # @raise (see #yescrypt)
111
+
112
+ # @!method bsdi_des(phrase, setting = nil, cost: nil)
113
+ # Hash +phrase+ using BSDi extended DES crypt.
114
+ # @param (see #yescrypt)
115
+ # @return (see #yescrypt)
116
+ # @raise (see #yescrypt)
117
+
118
+ # @!method des(phrase, setting = nil, cost: nil)
119
+ # Hash +phrase+ using traditional DES crypt.
120
+ # @param (see #yescrypt)
121
+ # @return (see #yescrypt)
122
+ # @raise (see #yescrypt)
123
+
124
+ ALGORITHMS.each_key do |algorithm|
125
+ define_method(algorithm) do |phrase, setting = nil, cost: nil|
126
+ if setting
127
+ setting_algorithm = detect_algorithm(setting)
128
+ if setting_algorithm != algorithm
129
+ raise ArgumentError, "setting algorithm #{setting_algorithm.inspect} does not match expected #{algorithm.inspect}"
130
+ end
131
+ end
132
+ crypt(phrase, setting, algorithm:, cost:)
133
+ end
134
+ end
135
+
136
+ # Returns the names of all supported algorithms.
137
+ #
138
+ # @return [Array<Symbol>] algorithm names in order from strongest to weakest
139
+ def algorithms = ALGORITHMS.keys
140
+
141
+ # Detects which algorithm produced a given setting or hash string by
142
+ # matching its leading prefix against {ALGORITHMS}.
143
+ #
144
+ # @param setting [String] a setting string or an existing password hash
145
+ # @return [Symbol, nil] the algorithm name, or +nil+ if the prefix is
146
+ # unrecognized
147
+ def detect_algorithm(setting) = PREFIXES[setting[/\A\$\w+\$?|_/].to_s]
148
+
149
+ # Hashes +phrase+ using libxcrypt's +crypt_rn+ function.
150
+ #
151
+ # When both +setting+ and +algorithm+ are omitted, a fresh setting is
152
+ # generated with the library's default algorithm. The result is always a
153
+ # self-describing string whose leading prefix identifies the algorithm and
154
+ # encodes the salt, making it safe to store directly.
155
+ #
156
+ # @param phrase [String] the password to hash
157
+ # @param setting [String, Symbol, nil] an existing hash or salt string, or
158
+ # an algorithm +Symbol+ as shorthand for passing only +algorithm:+;
159
+ # generates a fresh setting when +nil+
160
+ # @param algorithm [Symbol, nil] algorithm to use when generating a new
161
+ # setting; ignored when +setting+ is already a String
162
+ # @param cost [Integer, nil] work-factor override passed to
163
+ # {generate_setting}; uses the library default when +nil+
164
+ # @return [String] the hashed password
165
+ # @raise [Error] if +crypt_rn+ returns +NULL+, indicating an invalid
166
+ # setting or an unsupported algorithm
167
+ def crypt(phrase, setting = nil, algorithm: nil, cost: nil)
168
+ setting, algorithm = nil, setting if setting.is_a? Symbol
169
+ setting ||= generate_setting(algorithm, cost:)
170
+ data = ::FFI::MemoryPointer.new(:uint8, FFI::CRYPT_DATA_SIZE)
171
+ result_ptr = FFI.crypt_rn(phrase, setting, data, FFI::CRYPT_DATA_SIZE)
172
+ raise Error, "crypt failed: invalid setting or unsupported algorithm" if result_ptr.null?
173
+ result_ptr.read_string
174
+ ensure
175
+ data&.clear
176
+ end
177
+
178
+ # Verifies that +phrase+ matches a previously computed +hash+.
179
+ #
180
+ # Returns +false+ immediately for any hash value that would cause
181
+ # libxcrypt to return a magic failure token (strings beginning with +"*"+),
182
+ # or for empty or +nil+ input, guarding against invalid-hash oracle attacks.
183
+ # The final comparison is performed in constant time to prevent timing
184
+ # attacks.
185
+ #
186
+ # @param phrase [String] the candidate password
187
+ # @param hash [String, nil] the stored password hash to verify against
188
+ # @return [Boolean] +true+ if +phrase+ matches +hash+, +false+ otherwise
189
+ def verify(phrase, hash)
190
+ return false if hash.nil? || hash.empty? || hash.start_with?("*")
191
+ result = crypt(phrase, hash)
192
+ secure_compare(result, hash)
193
+ rescue Error
194
+ false
195
+ end
196
+
197
+ # Generates a fresh setting string suitable for passing to {crypt}.
198
+ #
199
+ # Delegates to libxcrypt's +crypt_gensalt_rn+, which draws entropy from
200
+ # the OS to produce the random salt component. When +algorithm+ is +nil+,
201
+ # the library selects its preferred (strongest) algorithm.
202
+ #
203
+ # @param algorithm [Symbol, nil] the desired algorithm; uses the library
204
+ # default when +nil+
205
+ # @param cost [Integer, nil] work-factor for the generated setting; a value
206
+ # of +0+ selects the library's own default cost
207
+ # @return [String] a setting string beginning with the algorithm prefix
208
+ # @raise [ArgumentError] if +algorithm+ is not a key in {ALGORITHMS}
209
+ # @raise [Error] if +crypt_gensalt_rn+ returns +NULL+
210
+ def generate_setting(algorithm = nil, cost: nil)
211
+ prefix = ALGORITHMS.fetch(algorithm) { raise ArgumentError, "unknown algorithm: #{algorithm.inspect}" } if algorithm
212
+ cost ||= 0
213
+
214
+ output = ::FFI::MemoryPointer.new(:char, FFI::CRYPT_GENSALT_OUTPUT_SIZE)
215
+ result_ptr = FFI.crypt_gensalt_rn(prefix, cost, nil, 0, output, FFI::CRYPT_GENSALT_OUTPUT_SIZE)
216
+ raise Error, "crypt_gensalt failed: unknown prefix or unsupported algorithm" if result_ptr.null?
217
+
218
+ result_ptr.read_string
219
+ end
220
+
221
+ private
222
+
223
+ # Compares two strings in constant time to prevent timing attacks.
224
+ #
225
+ # Pads or truncates +trusted+ to match +untrusted+'s byte length before
226
+ # comparing so that the number of loop iterations is always the same
227
+ # regardless of content. A separate length check at the end ensures that a
228
+ # length-padded match is still rejected.
229
+ #
230
+ # Uses {OpenSSL.fixed_length_secure_compare} when available (Ruby >= 2.7
231
+ # with openssl >= 2.2); otherwise falls back to a pure-Ruby XOR loop.
232
+ #
233
+ # @param trusted [String] the known-good value (e.g., the output of {crypt})
234
+ # @param untrusted [String] the value supplied by the caller
235
+ # @return [Boolean] +true+ only when both strings are identical
236
+ def secure_compare(trusted, untrusted)
237
+ return false unless trusted.respond_to? :to_str and trusted = trusted.to_str.b
238
+ return false unless untrusted.respond_to? :to_str and untrusted = untrusted.to_str.b
239
+
240
+ # avoid ability for attacker to guess length of string by timing attack
241
+ comparable = trusted[0, untrusted.bytesize].ljust(untrusted.bytesize, "\0".b)
242
+
243
+ result = defined?(OpenSSL.fixed_length_secure_compare) ?
244
+ OpenSSL.fixed_length_secure_compare(comparable, untrusted) :
245
+ comparable.each_byte.zip(untrusted.each_byte).reduce(0) { |acc, (a, b)| acc | (a ^ b) }.zero?
246
+
247
+ trusted.bytesize == untrusted.bytesize and result
248
+ end
249
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xcrypt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: arm-linux
6
+ authors:
7
+ - Konstantin Haase
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ffi
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '13.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '13.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake-compiler-dock
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: |
55
+ Ruby FFI bindings for libxcrypt, a modern library for one-way hashing of
56
+ passwords. Supports yescrypt, bcrypt, SHA-512, SHA-256, and other
57
+ algorithms provided by the bundled libxcrypt source (ext/libxcrypt).
58
+ email: ruby-xcrypt@rkh.im
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - lib/xcrypt.rb
64
+ - lib/xcrypt/arm-linux/libxcrypt.so
65
+ - lib/xcrypt/ffi.rb
66
+ - lib/xcrypt/version.rb
67
+ homepage: https://github.com/rkh/ruby-xcrypt
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ source_code_uri: https://github.com/rkh/ruby-xcrypt
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '3.0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 4.0.3
87
+ specification_version: 4
88
+ summary: Ruby FFI bindings for libxcrypt
89
+ test_files: []