simplicity_client 0.1.2 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 495342a938dfee303d9653d8ea49bd7a8f94f9f31eaf367ac32de7ef1048b775
4
- data.tar.gz: ad30760937ee2466ed5b53026f7f64c5f36f8685141700dd236e1645275c81a9
3
+ metadata.gz: 3283578b786c0a520eeaee6b46290f1459e9075925f8fdf172534ac533270e33
4
+ data.tar.gz: cee5f3f5ac077c4cea0bc246d7f1a4b2ea03a23db7ad21795485730685ba3959
5
5
  SHA512:
6
- metadata.gz: f2817cbe355a5c5c614ad16a644d27c9b12694e69644029db52e06ee2039fff3a173b3089a4d6bea406dd545adc2ad2ec0313c069c8b0892efc50a67b1723148
7
- data.tar.gz: 58bc2f845410fd0687afc51135c28b20e42a1dcdd1bacf89e8b240358d5b8a610ee0c5c3c77c7a4839c89130c6f51cef763195ed8bda0730f7a06007acf28f3e
6
+ metadata.gz: 2ddd8ee90c05b00ba4ed232846f5c2e546e087c93c7e2c3a58965c74480640fe04823bea6d94c9b3b0f58e64f41cd738b8b157420b71d5864846cb74e0600185
7
+ data.tar.gz: fbf5d020b63bae7fd804154b34513d95e9991afa39023877905072e69df66879b4afd3e7028251204e314720c4ca472860dc9e81d86d4a90e521202791dd7bbe
@@ -0,0 +1,91 @@
1
+ require "json"
2
+ require "digest"
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Fido2Client
7
+ Passkey = Data.define(:credentialId, :keyAlgorithm, :keyCurve, :keyValue, :userHandle)
8
+ Assertion = Data.define(:authenticator_data, :client_data_json, :credential_id, :user_handle, :signature)
9
+
10
+ class Client
11
+ def initialize
12
+ @origin = "https://app.simplicity.kiwi"
13
+ @rp_id = "simplicity.kiwi"
14
+ end
15
+
16
+ def get_assertion(passkey, challenge)
17
+ collected_client_data = {
18
+ type: "webauthn.get",
19
+ challenge: challenge,
20
+ origin: @origin,
21
+ crossOrigin: false,
22
+ }
23
+ client_data_json = JSON.dump(collected_client_data)
24
+ client_data_hash = Digest::SHA256.digest(client_data_json)
25
+
26
+ # Assertion
27
+ auth_data = generate_auth_data
28
+ private_key = parse_private_key(passkey.keyAlgorithm, passkey.keyCurve, passkey.keyValue)
29
+ signature = generate_signature(auth_data, client_data_hash, private_key)
30
+
31
+ Assertion.new(
32
+ authenticator_data: Base64.urlsafe_encode64(auth_data.pack("c*"), padding: false),
33
+ client_data_json: Base64.urlsafe_encode64(client_data_json, padding: false),
34
+ credential_id: Base64.urlsafe_encode64(guid_to_raw_format(passkey.credentialId), padding: false),
35
+ user_handle: passkey.userHandle,
36
+ signature: Base64.urlsafe_encode64(signature, padding: false),
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def box(tag, lines)
43
+ lines.unshift "-----BEGIN #{tag}-----"
44
+ lines.push "-----END #{tag}-----"
45
+ lines.join("\n")
46
+ end
47
+
48
+ def der_to_pem(tag, der)
49
+ box tag, Base64.strict_encode64(der).scan(/.{1,64}/)
50
+ end
51
+
52
+ def parse_private_key(key_algorithm, key_curve, key_value)
53
+ raise "Unsupported key algorithm: #{key_algorithm}" unless key_algorithm == "ECDSA"
54
+ raise "Unsupported key curve: #{key_curve}" unless key_curve == "P-256"
55
+
56
+ # Decode the Base64 key value
57
+ key_value_bin = Base64.urlsafe_decode64(key_value)
58
+
59
+ pem = der_to_pem("PRIVATE KEY", key_value_bin)
60
+ OpenSSL::PKey::EC.new(pem)
61
+ end
62
+
63
+ def generate_signature(auth_data, client_data_hash, private_key)
64
+ sig_base = [*auth_data, *client_data_hash.bytes]
65
+ digest = OpenSSL::Digest.new("SHA256")
66
+ private_key.sign(digest, sig_base.pack("c*"))
67
+ end
68
+
69
+ def generate_auth_data
70
+ auth_data = []
71
+ rp_id_hash = Digest::SHA256.digest(@rp_id)
72
+ auth_data += rp_id_hash.bytes
73
+
74
+ # Flags asserted: Backup eligibility, Backup State, User Verification, User Presence
75
+ flags = 0x1D
76
+ auth_data.push(flags)
77
+
78
+ # Counter
79
+ auth_data += [0, 0, 0, 0]
80
+
81
+ auth_data
82
+ end
83
+
84
+ def guid_to_raw_format(guid)
85
+ raise TypeError, "GUID parameter is invalid" unless guid.match?(/\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\z/)
86
+
87
+ # Remove the hyphens and pack the string into raw binary
88
+ [guid.delete("-")].pack("H*")
89
+ end
90
+ end
91
+ end
@@ -9,29 +9,45 @@ require "faraday"
9
9
  require "faraday-cookie_jar"
10
10
  require "faraday/follow_redirects"
11
11
  require "aws-sdk-cognitoidentityprovider"
12
- require "aws-cognito-srp"
13
12
  require "aws-sdk-cognitoidentity"
14
13
  require "aws-sigv4"
14
+ require_relative "fido2_client"
15
15
 
16
16
  module SimplicityClient
17
+ Params = Struct.new(:email, :credentialId, :keyAlgorithm, :keyCurve, :keyValue, :userHandle, keyword_init: true)
18
+ ParamDefinition = Struct.new(:name, :label, :primary_id)
19
+
17
20
  class Error < StandardError; end
18
21
 
22
+ # For the user interface
23
+ PARAMS_INFO = [
24
+ ParamDefinition.new(name: "email", label: "Email address", primary_id: true),
25
+ ParamDefinition.new(name: "credentialId", label: "Passkey credential ID"),
26
+ ParamDefinition.new(name: "keyAlgorithm", label: "Passkey key algorithm"),
27
+ ParamDefinition.new(name: "keyCurve", label: "Passkey key curve"),
28
+ ParamDefinition.new(name: "keyValue", label: "Passkey value"),
29
+ ParamDefinition.new(name: "userHandle", label: "Passkey user handle")
30
+ ].freeze
31
+
19
32
  class Session
20
33
  def initialize
21
34
  @logger = Logger.new $stderr
22
- @logger.level = Logger::INFO
35
+ @logger.level = Logger::DEBUG
23
36
 
24
37
  @user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36"
25
38
 
26
39
  @client = Faraday.new(
27
40
  headers: { "User-Agent" => @user_agent },
28
- # proxy: "http://Thinkbook.local:8080",
29
- # :ssl => {:verify => false}
30
- ) do |builder|
41
+ # proxy: "http://Thinkbook.local:8080",
42
+ # :ssl => {:verify => false}
43
+ ) do |builder|
31
44
  builder.response :follow_redirects
32
- # builder.response :logger
45
+ builder.response :logger
33
46
  builder.adapter Faraday.default_adapter
34
47
  end
48
+
49
+ @authsignal_base = "https://au.api.authsignal.com/v1"
50
+ @authsignal_tenant_id = "e768822f-b1a1-404c-a685-0e37905ca5f5"
35
51
  end
36
52
 
37
53
  # Write the session data out to a string so that the session can be restored later
@@ -47,49 +63,76 @@ module SimplicityClient
47
63
  @auth_data = json["auth_data"]
48
64
  end
49
65
 
50
- def login(username, password)
51
- client = Aws::CognitoIdentityProvider::Client.new(region: 'ap-southeast-2')
66
+ def login(params)
67
+ client = Aws::CognitoIdentityProvider::Client.new(region: "ap-southeast-2")
52
68
 
53
69
  user_pool_id = "ap-southeast-2_abAklW6ap"
54
70
 
55
- aws_srp = Aws::CognitoSrp.new(
56
- username: username.downcase,
57
- password: password,
58
- pool_id: user_pool_id,
59
- client_id: "kvoiu7unft0c8hqqsa6hkmeu5",
60
- aws_client: client,
71
+ device_id = SecureRandom.uuid
72
+ passkey = Fido2Client::Passkey.new(params.credentialId, params.keyAlgorithm, params.keyCurve, params.keyValue, params.userHandle)
73
+ challenge_id = fetch_challenge_id
74
+ authentication_options = fetch_authentication_options(challenge_id)
75
+ challenge = authentication_options["options"]["challenge"]
76
+ assertion = Fido2Client::Client.new.get_assertion(passkey, challenge)
77
+ passkey_result = present_passkey(challenge_id, assertion, device_id)
78
+
79
+ resp = client.initiate_auth(
80
+ {
81
+ auth_flow: "CUSTOM_AUTH",
82
+ auth_parameters: {
83
+ USERNAME: params.email,
84
+ },
85
+ client_metadata: {
86
+ anonymousId: device_id,
87
+ },
88
+ client_id: "kvoiu7unft0c8hqqsa6hkmeu5",
89
+ },
61
90
  )
62
91
 
63
- resp = aws_srp.authenticate
92
+ resp = client.respond_to_auth_challenge(
93
+ {
94
+ challenge_name: "CUSTOM_CHALLENGE",
95
+ challenge_responses: {
96
+ USERNAME: params.email,
97
+ ANSWER: passkey_result["accessToken"],
98
+ },
99
+ client_id: "kvoiu7unft0c8hqqsa6hkmeu5",
100
+ session: resp.session,
101
+ },
102
+ )
64
103
 
65
- cognito_identity_client = Aws::CognitoIdentity::Client.new(region: 'ap-southeast-2')
104
+ cognito_identity_client = Aws::CognitoIdentity::Client.new(region: "ap-southeast-2")
66
105
 
67
106
  # Assuming you have the Identity Pool ID and the ID token
68
- identity_pool_id = 'ap-southeast-2:0ed33fc6-4cef-4f2e-b634-31c616e108e2'
69
- id_token = resp.id_token
107
+ identity_pool_id = "ap-southeast-2:0ed33fc6-4cef-4f2e-b634-31c616e108e2"
108
+ id_token = resp.authentication_result.id_token
70
109
 
71
110
  # Get ID from the identity pool
72
- id_response = cognito_identity_client.get_id({
73
- identity_pool_id: identity_pool_id,
74
- logins: {
75
- "cognito-idp.ap-southeast-2.amazonaws.com/#{user_pool_id}" => id_token
76
- }
77
- })
111
+ id_response = cognito_identity_client.get_id(
112
+ {
113
+ identity_pool_id: identity_pool_id,
114
+ logins: {
115
+ "cognito-idp.ap-southeast-2.amazonaws.com/#{user_pool_id}" => id_token,
116
+ },
117
+ },
118
+ )
78
119
 
79
120
  # Get credentials for the ID
80
- credentials_response = cognito_identity_client.get_credentials_for_identity({
81
- identity_id: id_response.identity_id,
82
- logins: {
83
- "cognito-idp.ap-southeast-2.amazonaws.com/#{user_pool_id}" => id_token
84
- }
85
- })
121
+ credentials_response = cognito_identity_client.get_credentials_for_identity(
122
+ {
123
+ identity_id: id_response.identity_id,
124
+ logins: {
125
+ "cognito-idp.ap-southeast-2.amazonaws.com/#{user_pool_id}" => id_token,
126
+ },
127
+ },
128
+ )
86
129
 
87
130
  access_key_id = credentials_response.credentials.access_key_id
88
131
  secret_key = credentials_response.credentials.secret_key
89
132
  session_token = credentials_response.credentials.session_token
90
133
 
91
134
  @auth_data = {
92
- email: username,
135
+ email: params.email,
93
136
  access_key_id: access_key_id,
94
137
  secret_key: secret_key,
95
138
  region: "ap-southeast-2",
@@ -126,12 +169,12 @@ module SimplicityClient
126
169
  signature = signer.sign_request(
127
170
  http_method: http_method,
128
171
  url: url,
129
- body: body
172
+ body: body,
130
173
  )
131
174
 
132
175
  response = @client.post(url) do |req|
133
176
  req.headers = signature.headers.merge({
134
- 'Content-Type' => 'application/json'
177
+ "Content-Type" => "application/json",
135
178
  })
136
179
  req.body = body
137
180
  end
@@ -154,5 +197,76 @@ module SimplicityClient
154
197
  raise Error, "Failed to list accounts: #{response.status}, #{response.body}"
155
198
  end
156
199
  end
200
+
201
+ private
202
+
203
+ def fetch_challenge_id
204
+ response = @client.post("#{@authsignal_base}/client/challenge") do |req|
205
+ req.headers = {
206
+ "Authorization" => "Basic #{Base64.encode64(@authsignal_tenant_id)}",
207
+ "Content-Type" => "application/json",
208
+ }
209
+ req.body = {
210
+ action: "cognitoAuth",
211
+ }.to_json
212
+ end
213
+
214
+ if response.success?
215
+ obj = JSON.parse(response.body)
216
+ obj["challengeId"]
217
+ else
218
+ raise Error, "Failed to get challenge ID: #{response.status}, #{response.body}"
219
+ end
220
+ end
221
+
222
+ def fetch_authentication_options(challenge_id)
223
+ response = @client.post("#{@authsignal_base}/client/user-authenticators/passkey/authentication-options") do |req|
224
+ req.headers = {
225
+ "Authorization" => "Basic #{Base64.encode64(@authsignal_tenant_id)}",
226
+ "Content-Type" => "application/json",
227
+ }
228
+ req.body = {
229
+ challengeId: challenge_id,
230
+ }.to_json
231
+ end
232
+
233
+ if response.success?
234
+ JSON.parse(response.body)
235
+ else
236
+ raise Error, "Failed to get authentication options: #{response.status}, #{response.body}"
237
+ end
238
+ end
239
+
240
+ def present_passkey(challenge_id, assertion, device_id)
241
+ response = @client.post("#{@authsignal_base}/client/verify/passkey") do |req|
242
+ req.headers = {
243
+ "Authorization" => "Basic #{Base64.encode64(@authsignal_tenant_id)}",
244
+ "Content-Type" => "application/json",
245
+ }
246
+ req.body = {
247
+ challengeId: challenge_id,
248
+ authenticationCredential: {
249
+ id: assertion.credential_id,
250
+ rawId: assertion.credential_id,
251
+ response: {
252
+ authenticatorData: assertion.authenticator_data,
253
+ clientDataJSON: assertion.client_data_json,
254
+ signature: assertion.signature,
255
+ userHandle: assertion.user_handle,
256
+ },
257
+ type: "public-key",
258
+ clientExtensionResults: {},
259
+ authenticatorAttachment: "platform",
260
+ },
261
+ deviceId: device_id,
262
+ }.to_json
263
+ end
264
+
265
+ if response.success?
266
+ JSON.parse(response.body)
267
+ else
268
+ raise Error, "Failed to get JWT: #{response.status}, #{response.body}"
269
+ end
270
+ end
157
271
  end
158
272
  end
metadata CHANGED
@@ -1,113 +1,99 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplicity_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - George Dewar
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-02 00:00:00.000000000 Z
11
+ date: 2025-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: faraday
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '2.7'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '2.7'
27
- - !ruby/object:Gem::Dependency
28
- name: faraday-cookie_jar
14
+ name: aws-sdk-cognitoidentity
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - "~>"
32
18
  - !ruby/object:Gem::Version
33
- version: 0.0.7
19
+ version: 1.52.0
34
20
  type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - "~>"
39
25
  - !ruby/object:Gem::Version
40
- version: 0.0.7
26
+ version: 1.52.0
41
27
  - !ruby/object:Gem::Dependency
42
- name: faraday-follow_redirects
28
+ name: aws-sdk-cognitoidentityprovider
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - "~>"
46
32
  - !ruby/object:Gem::Version
47
- version: 0.3.0
33
+ version: '1.87'
48
34
  type: :runtime
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
38
  - - "~>"
53
39
  - !ruby/object:Gem::Version
54
- version: 0.3.0
40
+ version: '1.87'
55
41
  - !ruby/object:Gem::Dependency
56
- name: aws-sdk-cognitoidentityprovider
42
+ name: aws-sigv4
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: '1.87'
47
+ version: 1.8.0
62
48
  type: :runtime
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: '1.87'
54
+ version: 1.8.0
69
55
  - !ruby/object:Gem::Dependency
70
- name: aws-cognito-srp
56
+ name: faraday
71
57
  requirement: !ruby/object:Gem::Requirement
72
58
  requirements:
73
59
  - - "~>"
74
60
  - !ruby/object:Gem::Version
75
- version: 0.6.0
61
+ version: '2.7'
76
62
  type: :runtime
77
63
  prerelease: false
78
64
  version_requirements: !ruby/object:Gem::Requirement
79
65
  requirements:
80
66
  - - "~>"
81
67
  - !ruby/object:Gem::Version
82
- version: 0.6.0
68
+ version: '2.7'
83
69
  - !ruby/object:Gem::Dependency
84
- name: aws-sdk-cognitoidentity
70
+ name: faraday-cookie_jar
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
73
  - - "~>"
88
74
  - !ruby/object:Gem::Version
89
- version: 1.52.0
75
+ version: 0.0.7
90
76
  type: :runtime
91
77
  prerelease: false
92
78
  version_requirements: !ruby/object:Gem::Requirement
93
79
  requirements:
94
80
  - - "~>"
95
81
  - !ruby/object:Gem::Version
96
- version: 1.52.0
82
+ version: 0.0.7
97
83
  - !ruby/object:Gem::Dependency
98
- name: aws-sigv4
84
+ name: faraday-follow_redirects
99
85
  requirement: !ruby/object:Gem::Requirement
100
86
  requirements:
101
87
  - - "~>"
102
88
  - !ruby/object:Gem::Version
103
- version: 1.8.0
89
+ version: 0.3.0
104
90
  type: :runtime
105
91
  prerelease: false
106
92
  version_requirements: !ruby/object:Gem::Requirement
107
93
  requirements:
108
94
  - - "~>"
109
95
  - !ruby/object:Gem::Version
110
- version: 1.8.0
96
+ version: 0.3.0
111
97
  description: This gem can log into Simplicity and fetch fund balances
112
98
  email:
113
99
  - george@dewar.co.nz
@@ -115,12 +101,13 @@ executables: []
115
101
  extensions: []
116
102
  extra_rdoc_files: []
117
103
  files:
104
+ - lib/fido2_client.rb
118
105
  - lib/simplicity_client.rb
119
- homepage: https://rubygems.org/gems/hola
106
+ homepage: https://github.com/GeorgeDewar/simplicity_client
120
107
  licenses:
121
108
  - MIT
122
109
  metadata: {}
123
- post_install_message:
110
+ post_install_message:
124
111
  rdoc_options: []
125
112
  require_paths:
126
113
  - lib
@@ -135,8 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
122
  - !ruby/object:Gem::Version
136
123
  version: '0'
137
124
  requirements: []
138
- rubygems_version: 3.2.3
139
- signing_key:
125
+ rubygems_version: 3.5.16
126
+ signing_key:
140
127
  specification_version: 4
141
128
  summary: Fetch KiwiSaver and investment fund balances from Simplicity
142
129
  test_files: []