altcha 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ec9e3706caf746807ebb7349d3ab202eb6601b9fed628fe6b5176c5a81ef6118
4
+ data.tar.gz: 6d6ddbb904cde1fed358bf6b36e11207c49604d0ecfed0646558c05f9d3e6ad0
5
+ SHA512:
6
+ metadata.gz: feed3cad4fe038420c0e1701751d362408b64ef61e3fcb2d09640dcfef9b4566e2b486a3b3ddb8710ec5c4303948cc63eda072032797d5d1def90bd011b22a14
7
+ data.tar.gz: cd3a752cdb06111293a949d31fcb5988080d69d779d9db735e2420d9fdf57e0bde82734c0fa38e253f7c46ef30ed16b2707cbe0f39cd664fd9a7fe2a41d2fa83
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in altcha.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,65 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ altcha (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.2)
10
+ diff-lcs (1.5.1)
11
+ json (2.7.2)
12
+ language_server-protocol (3.17.0.3)
13
+ parallel (1.25.1)
14
+ parser (3.3.4.0)
15
+ ast (~> 2.4.1)
16
+ racc
17
+ racc (1.8.1)
18
+ rainbow (3.1.1)
19
+ rake (10.5.0)
20
+ regexp_parser (2.9.2)
21
+ rexml (3.3.4)
22
+ strscan
23
+ rspec (3.13.0)
24
+ rspec-core (~> 3.13.0)
25
+ rspec-expectations (~> 3.13.0)
26
+ rspec-mocks (~> 3.13.0)
27
+ rspec-core (3.13.0)
28
+ rspec-support (~> 3.13.0)
29
+ rspec-expectations (3.13.1)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.13.0)
32
+ rspec-mocks (3.13.1)
33
+ diff-lcs (>= 1.2.0, < 2.0)
34
+ rspec-support (~> 3.13.0)
35
+ rspec-support (3.13.1)
36
+ rubocop (1.65.1)
37
+ json (~> 2.3)
38
+ language_server-protocol (>= 3.17.0)
39
+ parallel (~> 1.10)
40
+ parser (>= 3.3.0.2)
41
+ rainbow (>= 2.2.2, < 4.0)
42
+ regexp_parser (>= 2.4, < 3.0)
43
+ rexml (>= 3.2.5, < 4.0)
44
+ rubocop-ast (>= 1.31.1, < 2.0)
45
+ ruby-progressbar (~> 1.7)
46
+ unicode-display_width (>= 2.4.0, < 3.0)
47
+ rubocop-ast (1.31.3)
48
+ parser (>= 3.3.1.0)
49
+ ruby-progressbar (1.13.0)
50
+ strscan (3.1.0)
51
+ unicode-display_width (2.5.0)
52
+
53
+ PLATFORMS
54
+ arm64-darwin-23
55
+ ruby
56
+
57
+ DEPENDENCIES
58
+ altcha!
59
+ bundler (~> 2.5)
60
+ rake (~> 10.0)
61
+ rspec (~> 3.0)
62
+ rubocop (~> 1.65)
63
+
64
+ BUNDLED WITH
65
+ 2.5.11
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Daniel Regeci
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # ALTCHA Ruby Library
2
+
3
+ The ALTCHA Ruby Library is a lightweight, zero-dependency library designed for creating and verifying [ALTCHA](https://altcha.org) challenges.
4
+
5
+ ## Compatibility
6
+
7
+ This library is compatible with:
8
+
9
+ - Ruby 2.7+
10
+
11
+ ## Example
12
+
13
+ - [Demo server](https://github.com/altcha-org/altcha-starter-rb)
14
+
15
+ ## Installation
16
+
17
+ To install the ALTCHA Ruby Library, add it to your Gemfile:
18
+
19
+ ```ruby
20
+ gem 'altcha', git: 'https://github.com/altcha-org/altcha-lib-rb'
21
+ ```
22
+
23
+ Then run:
24
+
25
+ ```sh
26
+ bundle install
27
+ ```
28
+
29
+ Alternatively, install it directly using:
30
+
31
+ ```sh
32
+ gem install altcha
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Here’s a basic example of how to use the ALTCHA Ruby Library:
38
+
39
+ ```ruby
40
+ require 'altcha'
41
+
42
+ hmac_key = 'secret hmac key'
43
+
44
+ # Create a new challenge
45
+ options = Altcha::ChallengeOptions.new.tap do |opts|
46
+ opts.hmac_key = hmac_key
47
+ opts.max_number = 100000 # the maximum random number
48
+ end
49
+
50
+ challenge = Altcha.create_challenge(options)
51
+
52
+ # Example payload to verify
53
+ payload = {
54
+ algorithm: challenge.algorithm,
55
+ challenge: challenge.challenge,
56
+ number: 12345, # Example number
57
+ salt: challenge.salt,
58
+ signature: challenge.signature
59
+ }
60
+
61
+ # Verify the solution
62
+ valid = Altcha.verify_solution(payload, hmac_key, true)
63
+ puts valid ? "Solution verified!" : "Invalid solution."
64
+ ```
65
+
66
+ ## API
67
+
68
+ ### `Altcha.create_challenge(options)`
69
+
70
+ Creates a new challenge for ALTCHA.
71
+
72
+ **Parameters:**
73
+
74
+ - `options [ChallengeOptions]`:
75
+ - `algorithm [String]`: Hashing algorithm to use (`SHA-1`, `SHA-256`, `SHA-512`, default: `SHA-256`).
76
+ - `max_number [Integer]`: Maximum number for the random number generator (default: 1,000,000).
77
+ - `salt_length [Integer]`: Length of the random salt (default: 12 bytes).
78
+ - `hmac_key [String]`: Required HMAC key.
79
+ - `salt [String]`: Optional salt string. If not provided, a random salt will be generated.
80
+ - `number [Integer]`: Optional specific number to use. If not provided, a random number will be generated.
81
+ - `expires [Time]`: Optional expiration time for the challenge.
82
+ - `params [Hash]`: Optional URL-encoded query parameters.
83
+
84
+ **Returns:** `Challenge`
85
+
86
+ ### `Altcha.verify_solution(payload, hmac_key, check_expires = true)`
87
+
88
+ Verifies an ALTCHA solution.
89
+
90
+ **Parameters:**
91
+
92
+ - `payload [Hash]`: The solution payload to verify.
93
+ - `hmac_key [String]`: The HMAC key used for verification.
94
+ - `check_expires [Boolean]`: Whether to check if the challenge has expired.
95
+
96
+ **Returns:** `Boolean`
97
+
98
+ ### `Altcha.extract_params(payload)`
99
+
100
+ Extracts URL parameters from the payload's salt.
101
+
102
+ **Parameters:**
103
+
104
+ - `payload [Hash]`: The payload containing the salt.
105
+
106
+ **Returns:** `Hash`
107
+
108
+ ### `Altcha.verify_fields_hash(form_data, fields, fields_hash, algorithm)`
109
+
110
+ Verifies the hash of form fields.
111
+
112
+ **Parameters:**
113
+
114
+ - `form_data [Hash]`: The form data to hash.
115
+ - `fields [Array<String>]`: The fields to include in the hash.
116
+ - `fields_hash [String]`: The expected hash value.
117
+ - `algorithm [String]`: Hashing algorithm (`SHA-1`, `SHA-256`, `SHA-512`).
118
+
119
+ **Returns:** `Boolean`
120
+
121
+ ### `Altcha.verify_server_signature(payload, hmac_key)`
122
+
123
+ Verifies the server's signature.
124
+
125
+ **Parameters:**
126
+
127
+ - `payload [String, ServerSignaturePayload]`: The payload to verify (string or `ServerSignaturePayload`).
128
+ - `hmac_key [String]`: The HMAC key used for verification.
129
+
130
+ **Returns:** `[Boolean, ServerSignatureVerificationData]`
131
+
132
+ ### `Altcha.solve_challenge(challenge, salt, algorithm, max, start)`
133
+
134
+ Finds a solution to the given challenge.
135
+
136
+ **Parameters:**
137
+
138
+ - `challenge [String]`: The challenge hash.
139
+ - `salt [String]`: The challenge salt.
140
+ - `algorithm [String]`: Hashing algorithm (`SHA-1`, `SHA-256`, `SHA-512`).
141
+ - `max [Integer]`: Maximum number to iterate to.
142
+ - `start [Integer]`: Starting number.
143
+
144
+ **Returns:** `Solution, nil`
145
+
146
+ ## License
147
+
148
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/altcha.gemspec ADDED
@@ -0,0 +1,28 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "altcha/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "altcha"
8
+ spec.version = Altcha::VERSION
9
+ spec.authors = ["Daniel Regeci"]
10
+
11
+ spec.summary = "ALTCHA Library"
12
+ spec.description = "A lightweight library for creating and verifying ALTCHA challenges."
13
+ spec.homepage = "https://altcha.org"
14
+ spec.license = "MIT"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 2.5"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "altcha"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Altcha
4
+ VERSION = '0.1.0'
5
+ end
data/lib/altcha.rb ADDED
@@ -0,0 +1,357 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'altcha/version'
4
+ require 'openssl'
5
+ require 'base64'
6
+ require 'uri'
7
+ require 'time'
8
+
9
+ # Altcha module provides functions for creating and verifying ALTCHA challenges.
10
+ module Altcha
11
+ # Contains algorithm type definitions for hashing.
12
+ module Algorithm
13
+ SHA1 = 'SHA-1'
14
+ SHA256 = 'SHA-256'
15
+ SHA512 = 'SHA-512'
16
+ end
17
+
18
+ # Default values for challenge generation.
19
+ DEFAULT_MAX_NUMBER = 1_000_000
20
+ DEFAULT_SALT_LENGTH = 12
21
+ DEFAULT_ALGORITHM = Algorithm::SHA256
22
+
23
+ # Class representing options for generating a challenge.
24
+ class ChallengeOptions
25
+ attr_accessor :algorithm, :max_number, :salt_length, :hmac_key, :salt, :number, :expires, :params
26
+ end
27
+
28
+ # Class representing a challenge with its attributes.
29
+ class Challenge
30
+ attr_accessor :algorithm, :challenge, :maxnumber, :salt, :signature
31
+
32
+ # Converts the Challenge object to a JSON string.
33
+ # @param options [Hash] options to customize JSON encoding.
34
+ # @return [String] JSON representation of the Challenge object.
35
+ def to_json(options = {})
36
+ {
37
+ algorithm: @algorithm,
38
+ challenge: @challenge,
39
+ maxnumber: @maxnumber,
40
+ salt: @salt,
41
+ signature: @signature
42
+ }.to_json(options)
43
+ end
44
+
45
+ # Creates a Challenge object from a JSON string.
46
+ # @param string [String] JSON string to parse.
47
+ # @return [Challenge] Parsed Challenge object.
48
+ def from_json(string)
49
+ data = JSON.parse(string)
50
+ new data['algorithm'], data['challenge'], data['maxnumber'], data['salt'], data['signature']
51
+ end
52
+ end
53
+
54
+ # Class representing the payload of a challenge.
55
+ class Payload
56
+ attr_accessor :algorithm, :challenge, :number, :salt, :signature
57
+
58
+ # Converts the Payload object to a JSON string.
59
+ # @param options [Hash] options to customize JSON encoding.
60
+ # @return [String] JSON representation of the Payload object.
61
+ def to_json(options = {})
62
+ {
63
+ algorithm: @algorithm,
64
+ challenge: @challenge,
65
+ number: @number,
66
+ salt: @salt,
67
+ signature: @signature
68
+ }.to_json(options)
69
+ end
70
+
71
+ # Creates a Payload object from a JSON string.
72
+ # @param string [String] JSON string to parse.
73
+ # @return [Payload] Parsed Payload object.
74
+ def from_json(string)
75
+ data = JSON.parse(string)
76
+ new data['algorithm'], data['verificationData'], data['signature'], data['verified']
77
+ end
78
+ end
79
+
80
+ # Class representing the payload for server signatures.
81
+ class ServerSignaturePayload
82
+ attr_accessor :algorithm, :verification_data, :signature, :verified
83
+
84
+ # Converts the ServerSignaturePayload object to a JSON string.
85
+ # @param options [Hash] options to customize JSON encoding.
86
+ # @return [String] JSON representation of the ServerSignaturePayload object.
87
+ def to_json(options = {})
88
+ {
89
+ algorithm: @algorithm,
90
+ verificationData: @verification_data,
91
+ signature: @signature,
92
+ verified: @verified
93
+ }.to_json(options)
94
+ end
95
+
96
+ # Creates a ServerSignaturePayload object from a JSON string.
97
+ # @param string [String] JSON string to parse.
98
+ # @return [ServerSignaturePayload] Parsed ServerSignaturePayload object.
99
+ def from_json(string)
100
+ data = JSON.parse(string)
101
+ new data['algorithm'], data['verificationData'], data['signature'], data['verified']
102
+ end
103
+ end
104
+
105
+ # Class for verifying server signatures, containing various data points.
106
+ class ServerSignatureVerificationData
107
+ attr_accessor :classification, :country, :detected_language, :email, :expire, :fields, :fields_hash,
108
+ :ip_address, :reasons, :score, :time, :verified
109
+ end
110
+
111
+ # Class representing the solution to a challenge.
112
+ class Solution
113
+ attr_accessor :number, :took
114
+ end
115
+
116
+ # Generates a random byte array of the specified length.
117
+ # @param length [Integer] The length of the byte array to generate.
118
+ # @return [String] The generated random byte array.
119
+ def self.random_bytes(length)
120
+ OpenSSL::Random.random_bytes(length)
121
+ end
122
+
123
+ # Generates a random integer between 0 and the specified maximum (inclusive).
124
+ # @param max [Integer] The upper bound for the random integer.
125
+ # @return [Integer] The generated random integer.
126
+ def self.random_int(max)
127
+ rand(max + 1)
128
+ end
129
+
130
+ # Hashes the input data using the specified algorithm and returns the hexadecimal representation of the hash.
131
+ # @param algorithm [String] The hashing algorithm to use (e.g., SHA-1, SHA-256, SHA-512).
132
+ # @param data [String] The data to hash.
133
+ # @return [String] The hexadecimal representation of the hashed data.
134
+ def self.hash_hex(algorithm, data)
135
+ hash = hash(algorithm, data)
136
+ hash.unpack1('H*')
137
+ end
138
+
139
+ # Hashes the input data using the specified algorithm.
140
+ # @param algorithm [String] The hashing algorithm to use (e.g., SHA-1, SHA-256, SHA-512).
141
+ # @param data [String] The data to hash.
142
+ # @return [String] The binary hash of the data.
143
+ # @raise [ArgumentError] If an unsupported algorithm is specified.
144
+ def self.hash(algorithm, data)
145
+ case algorithm
146
+ when Algorithm::SHA1
147
+ OpenSSL::Digest::SHA1.digest(data)
148
+ when Algorithm::SHA256
149
+ OpenSSL::Digest::SHA256.digest(data)
150
+ when Algorithm::SHA512
151
+ OpenSSL::Digest::SHA512.digest(data)
152
+ else
153
+ raise ArgumentError, "Unsupported algorithm: #{algorithm}"
154
+ end
155
+ end
156
+
157
+ # Computes the HMAC of the input data using the specified algorithm and key, and returns the hexadecimal representation.
158
+ # @param algorithm [String] The hashing algorithm to use (e.g., SHA-1, SHA-256, SHA-512).
159
+ # @param data [String] The data to hash.
160
+ # @param key [String] The key for the HMAC.
161
+ # @return [String] The hexadecimal representation of the HMAC.
162
+ def self.hmac_hex(algorithm, data, key)
163
+ hmac = hmac_hash(algorithm, data, key)
164
+ hmac.unpack1('H*')
165
+ end
166
+
167
+ # Computes the HMAC of the input data using the specified algorithm and key.
168
+ # @param algorithm [String] The hashing algorithm to use (e.g., SHA-1, SHA-256, SHA-512).
169
+ # @param data [String] The data to hash.
170
+ # @param key [String] The key for the HMAC.
171
+ # @return [String] The binary HMAC of the data.
172
+ # @raise [ArgumentError] If an unsupported algorithm is specified.
173
+ def self.hmac_hash(algorithm, data, key)
174
+ digest_class = case algorithm
175
+ when Algorithm::SHA1
176
+ OpenSSL::Digest::SHA1
177
+ when Algorithm::SHA256
178
+ OpenSSL::Digest::SHA256
179
+ when Algorithm::SHA512
180
+ OpenSSL::Digest::SHA512
181
+ else
182
+ raise ArgumentError, "Unsupported algorithm: #{algorithm}"
183
+ end
184
+ OpenSSL::HMAC.digest(digest_class.new, key, data)
185
+ end
186
+
187
+ # Creates a challenge for the client to solve based on the provided options.
188
+ # @param options [ChallengeOptions] Options for generating the challenge.
189
+ # @return [Challenge] The generated Challenge object.
190
+ def self.create_challenge(options)
191
+ algorithm = options.algorithm || DEFAULT_ALGORITHM
192
+ max_number = options.max_number || DEFAULT_MAX_NUMBER
193
+ salt_length = options.salt_length || DEFAULT_SALT_LENGTH
194
+
195
+ params = options.params || {}
196
+ params['expires'] = options.expires.to_i if options.expires
197
+
198
+ salt = options.salt || random_bytes(salt_length).unpack1('H*')
199
+ salt += "?#{URI.encode_www_form(params)}" unless params.empty?
200
+
201
+ number = options.number || random_int(max_number)
202
+
203
+ challenge_str = "#{salt}#{number}"
204
+ challenge = hash_hex(algorithm, challenge_str)
205
+ signature = hmac_hex(algorithm, challenge, options.hmac_key)
206
+
207
+ Challenge.new.tap do |c|
208
+ c.algorithm = algorithm
209
+ c.challenge = challenge
210
+ c.maxnumber = max_number
211
+ c.salt = salt
212
+ c.signature = signature
213
+ end
214
+ end
215
+
216
+ # Verifies the solution provided by the client.
217
+ # @param payload [String, Payload] The payload to verify, either as a base64 encoded JSON string or a Payload instance.
218
+ # @param hmac_key [String] The key used for HMAC verification.
219
+ # @param check_expires [Boolean] Whether to check if the challenge has expired.
220
+ # @return [Boolean] True if the solution is valid, false otherwise.
221
+ def self.verify_solution(payload, hmac_key, check_expires = true)
222
+ # Attempt to handle payload as a base64 encoded JSON string or as a Payload instance
223
+
224
+ # Decode and parse base64 JSON string if it's a String
225
+ if payload.is_a?(String)
226
+ decoded_payload = Base64.decode64(payload)
227
+ payload = JSON.parse(decoded_payload, object_class: Payload)
228
+ end
229
+
230
+ # Ensure payload is an instance of Payload
231
+ return false unless payload.is_a?(Payload)
232
+
233
+ required_attributes = %i[algorithm challenge number salt signature]
234
+ required_attributes.each do |attr|
235
+ value = payload.send(attr)
236
+ return false if value.nil? || value.to_s.strip.empty?
237
+ end
238
+
239
+ # Extract expiration time if checking expiration
240
+ if check_expires && payload.salt.include?('?')
241
+ expires = URI.decode_www_form(payload.salt.split('?').last).to_h['expires'].to_i
242
+ return false if expires && Time.now.to_i > expires
243
+ end
244
+
245
+ # Convert payload to ChallengeOptions
246
+ challenge_options = ChallengeOptions.new.tap do |co|
247
+ co.algorithm = payload.algorithm
248
+ co.hmac_key = hmac_key
249
+ co.number = payload.number
250
+ co.salt = payload.salt
251
+ end
252
+
253
+ # Create expected challenge and compare with the provided payload
254
+ expected_challenge = create_challenge(challenge_options)
255
+ expected_challenge.challenge == payload.challenge && expected_challenge.signature == payload.signature
256
+ rescue ArgumentError, JSON::ParserError
257
+ # Handle specific exceptions for invalid Base64 or JSON
258
+ false
259
+ end
260
+
261
+ # Extracts parameters from the payload's salt.
262
+ # @param payload [Payload] The payload containing the salt.
263
+ # @return [Hash] Parameters extracted from the payload's salt.
264
+ def self.extract_params(payload)
265
+ URI.decode_www_form(payload.salt.split('?').last).to_h
266
+ end
267
+
268
+ # Verifies the hash of form fields.
269
+ # @param form_data [Hash] The form data to verify.
270
+ # @param fields [Array<String>] The fields to include in the hash.
271
+ # @param fields_hash [String] The expected hash of the fields.
272
+ # @param algorithm [String] The hashing algorithm to use.
273
+ # @return [Boolean] True if the fields hash matches, false otherwise.
274
+ def self.verify_fields_hash(form_data, fields, fields_hash, algorithm)
275
+ lines = fields.map { |field| form_data[field].to_a.first.to_s }
276
+ joined_data = lines.join("\n")
277
+ computed_hash = hash_hex(algorithm, joined_data)
278
+ computed_hash == fields_hash
279
+ end
280
+
281
+ # Verifies the server's signature.
282
+ # @param payload [String, ServerSignaturePayload] The payload to verify, either as a base64 encoded JSON string or a ServerSignaturePayload instance.
283
+ # @param hmac_key [String] The key used for HMAC verification.
284
+ # @return [Array<Boolean, ServerSignatureVerificationData>] A tuple where the first element is true if the signature is valid, and the second element is the verification data.
285
+ def self.verify_server_signature(payload, hmac_key)
286
+ # Decode and parse base64 JSON string if it's a String
287
+ if payload.is_a?(String)
288
+ decoded_payload = Base64.decode64(payload)
289
+ payload = JSON.parse(decoded_payload, object_class: ServerSignaturePayload)
290
+ end
291
+
292
+ # Ensure payload is an instance of ServerSignaturePayload
293
+ return [false, nil] unless payload.is_a?(ServerSignaturePayload)
294
+
295
+ required_attributes = %i[algorithm verification_data signature verified]
296
+ required_attributes.each do |attr|
297
+ value = payload.send(attr)
298
+ return false if value.nil? || value.to_s.strip.empty?
299
+ end
300
+
301
+ hash_data = hash(payload.algorithm, payload.verification_data)
302
+ expected_signature = hmac_hex(payload.algorithm, hash_data, hmac_key)
303
+
304
+ params = URI.decode_www_form(payload.verification_data).to_h
305
+ verification_data = ServerSignatureVerificationData.new.tap do |v|
306
+ v.classification = params['classification'] || nil
307
+ v.country = params['country'] || nil
308
+ v.detected_language = params['detectedLanguage'] || nil
309
+ v.email = params['email'] || nil
310
+ v.expire = params['expire'] ? params['expire'].to_i : nil
311
+ v.fields = params['fields'] ? params['fields'].split(',') : nil
312
+ v.reasons = params['reasons'] ? params['reasons'].split(',') : nil
313
+ v.score = params['score'] ? params['score'].to_f : nil
314
+ v.time = params['time'] ? params['time'].to_i : nil
315
+ v.verified = params['verified'] == 'true'
316
+ end
317
+
318
+ now = Time.now.to_i
319
+ is_verified = payload.verified &&
320
+ verification_data.verified &&
321
+ (verification_data.expire.nil? || verification_data.expire > now) &&
322
+ payload.signature == expected_signature
323
+
324
+ [is_verified, verification_data]
325
+ rescue ArgumentError, JSON::ParserError => e
326
+ # Handle specific exceptions for invalid Base64 or JSON
327
+ puts "Error decoding or parsing payload: #{e.message}"
328
+ false
329
+ end
330
+
331
+ # Solves a challenge by iterating over possible solutions.
332
+ # @param challenge [String] The challenge to solve.
333
+ # @param salt [String] The salt used in the challenge.
334
+ # @param algorithm [String] The hashing algorithm used.
335
+ # @param max [Integer] The maximum number to try.
336
+ # @param start [Integer] The starting number to try.
337
+ # @return [Solution, nil] The solution if found, or nil if not.
338
+ def self.solve_challenge(challenge, salt, algorithm, max, start)
339
+ algorithm ||= Algorithm::SHA256
340
+ max ||= DEFAULT_MAX_NUMBER
341
+ start ||= 0
342
+
343
+ start_time = Time.now
344
+
345
+ (start..max).each do |n|
346
+ hash = hash_hex(algorithm, "#{salt}#{n}")
347
+ if hash == challenge
348
+ return Solution.new.tap do |s|
349
+ s.number = n
350
+ s.took = Time.now - start_time
351
+ end
352
+ end
353
+ end
354
+
355
+ nil
356
+ end
357
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: altcha
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel Regeci
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-08-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: A lightweight library for creating and verifying ALTCHA challenges.
56
+ email:
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".gitignore"
62
+ - ".rspec"
63
+ - Gemfile
64
+ - Gemfile.lock
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - altcha.gemspec
69
+ - bin/console
70
+ - bin/setup
71
+ - lib/altcha.rb
72
+ - lib/altcha/version.rb
73
+ homepage: https://altcha.org
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.5.11
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: ALTCHA Library
96
+ test_files: []