civic_sip_sdk 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8f25482aa5c37b9879c21c1a76ef9967337b9385832b6a24d03584f2561552d8
4
+ data.tar.gz: 071fe7ee8420f5c1fdf9fff5f026a33d8d316e5c400bbf2a6fb80a81a60a2b65
5
+ SHA512:
6
+ metadata.gz: d06a3a0612b446b09e7a3cc725b7ec6c38d768824ff3f61caca8ccca03c0fe87424ebe3e64215a5fe7ea584372c235b82e608b2c78466163f4dd60e0f41a2337
7
+ data.tar.gz: 9dda8d41853a87394f17c178d8386a65e5cc8f813aafd83dbfb4eb268994bade0b6f30c027de2b55b7a60ce89aa90c39086c940824e6c75bc4f81116442a2a7e
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CivicSIPSdk
4
+ class AppConfig
5
+ attr_reader :id, :env, :private_key, :secret
6
+
7
+ VALID_ENVS = %i[dev prod].freeze
8
+ REQUIRED_KEYS = [
9
+ { name: :id, error: 'Civic application id is missing!' },
10
+ { name: :private_key, error: 'Civic application private signing key is missing!' },
11
+ { name: :secret, error: 'Civic application secret is missing!' }
12
+ ].freeze
13
+
14
+ # Creates a new instance of <tt>CivicSIPSdk::AppConfig</tt>.
15
+ # This is used to configure the SDK connection parameters to the Civic SIP service.
16
+ #
17
+ # It raises an ArgumentError if any argument is nil.
18
+ #
19
+ # Args:
20
+ # * <tt>id</tt> - The application id.
21
+ # * <tt>env</tt> - The application environment. Defaults to +:prod+ if the value is incorrect.
22
+ # * <tt>private_key</tt> - The application's private signing key.
23
+ # * <tt>secret</tt> - The application secret
24
+ def initialize(id:, env:, private_key:, secret:)
25
+ @id = id
26
+ @env = VALID_ENVS.include?(env.to_sym) ? env.to_sym : VALID_ENVS.last
27
+ @private_key = private_key
28
+ @secret = secret
29
+
30
+ validate
31
+ end
32
+
33
+ private
34
+
35
+ def validate
36
+ validation_errors = REQUIRED_KEYS.map { |rk| instance_variable_get("@#{rk[:name]}").nil? ? rk[:error] : nil }
37
+ .compact
38
+
39
+ raise ArgumentError.new(validation_errors.join("\n")) unless validation_errors.empty?
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'httparty'
5
+ require 'civic_sip_sdk/app_config'
6
+ require 'civic_sip_sdk/user_data'
7
+ require 'civic_sip_sdk/crypto'
8
+
9
+ module CivicSIPSdk
10
+ class Client
11
+ BASE_URL = 'https://api.civic.com/sip'
12
+ AUTH_CODE_PATH = 'scopeRequest/authCode'
13
+ PUBLIC_HEX = '049a45998638cfb3c4b211d72030d9ae8329a242db63bfb0076a54e7647370a8ac5708b57af6065805d5a6be72332620932dbb35e8d318fce18e7c980a0eb26aa1'
14
+ MIMETYPE_JSON = 'application/json'
15
+
16
+ ENV_VAR = 'ENVIRONMENT'
17
+ RAILS_ENV = 'RAILS_ENV'
18
+ PRODUCTION_ENV = 'production'
19
+
20
+ HTTP_REQUEST_METHOD = :POST
21
+
22
+ # Creates a client
23
+ #
24
+ # Args:
25
+ # * <tt>config</tt> - an instance of CivicSIPSdk::AppConfig
26
+ def initialize(config:)
27
+ @config = config
28
+ end
29
+
30
+ # Exchange authorization code in the form of a JWT Token for the user data
31
+ # requested in the scope request.
32
+ #
33
+ # Args:
34
+ # * <tt>jwt_token</tt> - a JWT token that contains the authorization code
35
+ def exchange_code(jwt_token:)
36
+ json_body_str = JSON.generate('authToken' => jwt_token)
37
+
38
+ response = HTTParty.post(
39
+ "#{BASE_URL}/#{@config.env}/#{AUTH_CODE_PATH}",
40
+ headers: {
41
+ 'Content-Type' => MIMETYPE_JSON,
42
+ 'Accept' => MIMETYPE_JSON,
43
+ 'Content-Length' => json_body_str.size.to_s,
44
+ 'Authorization' => authorization_header(target_path: AUTH_CODE_PATH, body: json_body_str)
45
+ },
46
+ body: json_body_str,
47
+ debug_output: ENV[ENV_VAR] == PRODUCTION_ENV || ENV[RAILS_ENV] == PRODUCTION_ENV ? nil : STDOUT
48
+ )
49
+
50
+ unless response.code == 200
51
+ raise StandardError.new(
52
+ "Failed to exchange JWT token. HTTP status: #{response.code}, response body: #{response.body}"
53
+ )
54
+ end
55
+
56
+ res_payload = JSON.parse(response.body)
57
+ extract_user_data(response: res_payload)
58
+ end
59
+
60
+ private
61
+
62
+ def authorization_header(target_path:, body:)
63
+ jwt_token = Crypto.jwt_token(
64
+ app_id: @config.id,
65
+ sip_base_url: BASE_URL,
66
+ data: {
67
+ method: HTTP_REQUEST_METHOD,
68
+ path: target_path
69
+ },
70
+ private_key: @config.private_key
71
+ )
72
+
73
+ civic_extension = Crypto.civic_extension(secret: @config.secret, body: body)
74
+
75
+ "Civic #{jwt_token}.#{civic_extension}"
76
+ end
77
+
78
+ def extract_user_data(response:)
79
+ # only verify the token if it's in production (test is using an expired token)
80
+ decoded_token = Crypto.decode_jwt_token(
81
+ token: response['data'],
82
+ public_hex_key: PUBLIC_HEX,
83
+ should_verify: ENV[ENV_VAR] == PRODUCTION_ENV || ENV[RAILS_ENV] == PRODUCTION_ENV
84
+ )
85
+ data_text = if response['encrypted']
86
+ # decrypt the data attr
87
+ Crypto.decrypt(
88
+ text: decoded_token['data'],
89
+ secret: @config.secret
90
+ )
91
+ else
92
+ decoded_data['data']
93
+ end
94
+
95
+ UserData.new(
96
+ user_id: response['userId'],
97
+ data_items: JSON.parse(data_text)
98
+ )
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'securerandom'
5
+ require 'openssl'
6
+ require 'jwt'
7
+
8
+ module CivicSIPSdk
9
+ module Crypto
10
+ CIPHER_ALGO = 'AES-128-CBC'
11
+ IV_STRING_LENGTH = 32
12
+ # "prime256v1" is another name for "secp256r1"
13
+ ECDSA_CURVE = 'prime256v1'
14
+ JWT_ALGO = 'ES256'
15
+ HMAC_DIGEST = 'SHA256'
16
+
17
+ # Create encrypted text using AES-128-CBC with a IV of 16 bytes
18
+ #
19
+ # Args:
20
+ # * <tt>text</tt> - the plain text to be encrypted
21
+ # * <tt>secret</tt> - the Civic application secret in HEX format
22
+ def self.encrypt(text:, secret:)
23
+ cipher = OpenSSL::Cipher.new(CIPHER_ALGO)
24
+ cipher.encrypt
25
+ cipher.key = hex_to_string(hex: secret)
26
+ iv = cipher.random_iv
27
+ cipher.iv = iv
28
+
29
+ encrypted_text = "#{cipher.update(text)}#{cipher.final}"
30
+ "#{string_to_hex(str: iv)}#{Base64.encode64(encrypted_text)}"
31
+ end
32
+
33
+ # Decrypt the encrypted text using AES-128-CBC with a IV of 32 bytes
34
+ #
35
+ # Args:
36
+ # * <tt>text</tt> - the encrypted text to be decrypted
37
+ # * <tt>secret</tt> - the Civic application secret in HEX format
38
+ def self.decrypt(text:, secret:)
39
+ cipher = OpenSSL::Cipher.new(CIPHER_ALGO)
40
+ cipher.decrypt
41
+ cipher.key = hex_to_string(hex: secret)
42
+ iv_hex = text[0..(IV_STRING_LENGTH - 1)]
43
+ cipher.iv = hex_to_string(hex: iv_hex)
44
+ encrypted_text = Base64.decode64(text[IV_STRING_LENGTH..-1])
45
+
46
+ "#{cipher.update(encrypted_text)}#{cipher.final}"
47
+ end
48
+
49
+ def self.jwt_token(app_id:, sip_base_url:, data:, private_key:)
50
+ now = Time.now.to_i
51
+
52
+ payload = {
53
+ iat: now,
54
+ exp: now + 60 * 3,
55
+ iss: app_id,
56
+ aud: sip_base_url,
57
+ sub: app_id,
58
+ jti: SecureRandom.uuid,
59
+ data: data
60
+ }
61
+
62
+ JWT.encode(
63
+ payload,
64
+ private_signing_key(private_hex_key: private_key),
65
+ JWT_ALGO
66
+ )
67
+ end
68
+
69
+ def self.decode_jwt_token(token:, public_hex_key:, should_verify:)
70
+ public_key = public_signing_key(public_hex_key: public_hex_key)
71
+ data, = JWT.decode(token, public_key, should_verify, algorithm: JWT_ALGO)
72
+
73
+ data
74
+ end
75
+
76
+ def self.civic_extension(secret:, body:)
77
+ hmac = OpenSSL::HMAC.digest(HMAC_DIGEST, secret, JSON.generate(body))
78
+ Base64.encode64(hmac)
79
+ end
80
+
81
+ class << self
82
+ private
83
+
84
+ def private_signing_key(private_hex_key:)
85
+ key = OpenSSL::PKey::EC.new(ECDSA_CURVE)
86
+ key.private_key = OpenSSL::BN.new(private_hex_key.to_i(16))
87
+
88
+ key
89
+ end
90
+
91
+ def public_signing_key(public_hex_key:)
92
+ group = OpenSSL::PKey::EC::Group.new(ECDSA_CURVE)
93
+ point = OpenSSL::PKey::EC::Point.new(
94
+ group,
95
+ OpenSSL::BN.new(public_hex_key.to_i(16))
96
+ )
97
+ key = OpenSSL::PKey::EC.new(ECDSA_CURVE)
98
+ key.public_key = point
99
+
100
+ key
101
+ end
102
+
103
+ def string_to_hex(str:)
104
+ str.unpack1('H*')
105
+ end
106
+
107
+ def hex_to_string(hex:)
108
+ hex.scan(/../).map(&:hex).pack('c*')
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'civic_sip_sdk/user_data_item'
4
+
5
+ module CivicSIPSdk
6
+ class UserData
7
+ attr_reader :user_id, :data_items
8
+
9
+ # Creates a UserData insteance, which creates a list of UserDataItem instances from
10
+ # +data_items+.
11
+ #
12
+ # Args:
13
+ # * <tt>user_id</tt> - user id
14
+ # * <tt>data_items</tt> - a list of Hash that contains the key-value pairs for
15
+ # instantiating CivicSIPSdk::UserDataItem instances
16
+ def initialize(user_id:, data_items:)
17
+ @user_id = user_id
18
+ @data_items = data_items
19
+ @indexed_data_items = index_data_items
20
+ end
21
+
22
+ # Returns a +UserDataItem+ instance by matching the value of +label+, or +nil+
23
+ # if the label doesn't exist
24
+ def by_label(label:)
25
+ @indexed_data_items.fetch(label, nil)
26
+ end
27
+
28
+ private
29
+
30
+ def index_data_items
31
+ @data_items.each_with_object({}) do |data_item, memo|
32
+ memo[data_item['label']] = UserDataItem.new(
33
+ label: data_item['label'],
34
+ value: data_item['value'],
35
+ is_valid: data_item['isValid'],
36
+ is_owner: data_item['isOwner']
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CivicSIPSdk
4
+ class UserDataItem
5
+ attr_reader :label, :value, :is_valid, :is_owner
6
+
7
+ # Creates an instance of UserDataItem.
8
+ #
9
+ # Args:
10
+ # * <tt>label</tt> - Descriptive value identifier.
11
+ # * <tt>value</tt> - Item value of requested user data.
12
+ # * <tt>is_valid</tt> - Indicates whether or not the data is still considered valid on the blockchain.
13
+ # * <tt>is_owner</tt> - Civic SIP service challenges the user during scope request approval to ensure
14
+ # the user is in control of the private key originally used in the issuance of the data attestation.
15
+ def initialize(label:, value:, is_valid:, is_owner:)
16
+ @label = label
17
+ @value = value
18
+ @is_valid = is_valid
19
+ @is_owner = is_owner
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CivicSIPSdk
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift('lib')
4
+
5
+ require 'civic_sip_sdk/app_config'
6
+ require 'civic_sip_sdk/client'
7
+
8
+ module CivicSIPSdk
9
+ module_function
10
+
11
+ def new_client(id, env, private_key, secret)
12
+ app_config = AppConfig.new(
13
+ id: id,
14
+ env: env,
15
+ private_key: private_key,
16
+ secret: secret
17
+ )
18
+
19
+ Client.new(config: app_config)
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: civic_sip_sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Han Wang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-11-13 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: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coveralls
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.22
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.8.22
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.4'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: httparty
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.16'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.16'
97
+ - !ruby/object:Gem::Dependency
98
+ name: jwt
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '='
102
+ - !ruby/object:Gem::Version
103
+ version: 2.1.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '='
109
+ - !ruby/object:Gem::Version
110
+ version: 2.1.0
111
+ description: A ruby SDK for Civic SIP integration
112
+ email:
113
+ - han@binarystorms.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/civic_sip_sdk.rb
119
+ - lib/civic_sip_sdk/app_config.rb
120
+ - lib/civic_sip_sdk/client.rb
121
+ - lib/civic_sip_sdk/crypto.rb
122
+ - lib/civic_sip_sdk/user_data.rb
123
+ - lib/civic_sip_sdk/user_data_item.rb
124
+ - lib/civic_sip_sdk/version.rb
125
+ homepage: https://github.com/BinaryStorms/civic-sip-ruby-sdk
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '2.1'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.7.6
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: A ruby SDK for Civic SIP integration
149
+ test_files: []