yoti 1.0.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.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'yaml'
4
+
5
+ ################################
6
+ # Tests #
7
+ ################################
8
+
9
+ RSpec::Core::RakeTask.new
10
+ task test: :spec
11
+
12
+ ################################
13
+ # Rubocop #
14
+ ################################
15
+
16
+ require 'rubocop/rake_task'
17
+ RuboCop::RakeTask.new(:rubocop) do |t|
18
+ t.options = ['--config', 'rubocop.yml']
19
+ end
20
+
21
+ ################################
22
+ # Documentation #
23
+ ################################
24
+
25
+ require 'yard'
26
+ YARD::Rake::YardocTask.new do |t|
27
+ t.stats_options = ['--list-undoc']
28
+ end
29
+
30
+ yardstick_options = YAML.load_file('yardstick.yml')
31
+
32
+ require 'yardstick/rake/measurement'
33
+ Yardstick::Rake::Measurement.new(:measurement, yardstick_options) do |measurement|
34
+ measurement.output = 'measurement/report.txt'
35
+ end
36
+
37
+ require 'yardstick/rake/verify'
38
+ Yardstick::Rake::Verify.new(:verify_measurements, yardstick_options) do |verify|
39
+ verify.threshold = 88.5
40
+ end
41
+
42
+ ################################
43
+ # Defaults #
44
+ ################################
45
+
46
+ task default: [:spec, :rubocop]
@@ -0,0 +1,13 @@
1
+ module Yoti
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ desc 'This generator creates an Yoti initializer file at config/initializers'
7
+
8
+ def copy_initializer
9
+ template 'yoti.rb', 'config/initializers/yoti.rb'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,4 @@
1
+ Yoti.configure do |config|
2
+ config.client_sdk_id = ENV['YOTI_CLIENT_SDK_ID']
3
+ config.key_file_path = ENV['YOTI_KEY_FILE_PATH']
4
+ end
@@ -0,0 +1,31 @@
1
+ require 'net/http'
2
+
3
+ module Yoti
4
+ # Encapsulates the user profile data
5
+ class ActivityDetails
6
+ # @return [String] the outcome of the profile request, eg: SUCCESS
7
+ attr_reader :outcome
8
+
9
+ # @return [String] the Yoti ID
10
+ attr_reader :user_id
11
+
12
+ # @return [Hash] the decoded profile attributes
13
+ attr_reader :user_profile
14
+
15
+ # @param receipt [Hash] the receipt from the API request
16
+ # @param decrypted_profile [Object] Protobuf AttributeList decrypted object containing the profile attributes
17
+ def initialize(receipt, decrypted_profile)
18
+ @decrypted_profile = decrypted_profile
19
+ @user_profile = {}
20
+
21
+ if @decrypted_profile.respond_to_has_and_present?(:attributes)
22
+ @decrypted_profile.attributes.each do |field|
23
+ @user_profile[field.name] = Yoti::Protobuf.value_based_on_content_type(field.value, field.content_type)
24
+ end
25
+ end
26
+
27
+ @user_id = receipt['remember_me_id']
28
+ @outcome = receipt['sharing_outcome']
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module Yoti
2
+ # Handles all the publicly accesible Yoti methods for
3
+ # geting data using an encrypted connect token
4
+ module Client
5
+ # Performs all the steps required to get the decrypted profile from an API request
6
+ # @param encrypted_connect_token [String] token provided as a base 64 string
7
+ # @return [Object] an ActivityDetails instance encapsulating the user profile
8
+ def self.get_activity_details(encrypted_connect_token)
9
+ receipt = Yoti::Request.new(encrypted_connect_token).receipt
10
+ encrypted_data = Protobuf.current_user(receipt)
11
+ unwrapped_key = Yoti::SSL.decrypt_token(receipt['wrapped_receipt_key'])
12
+ decrypted_data = Yoti::SSL.decipher(unwrapped_key, encrypted_data.iv, encrypted_data.cipher_text)
13
+ decrypted_profile = Protobuf.attribute_list(decrypted_data)
14
+ ActivityDetails.new(receipt, decrypted_profile)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,84 @@
1
+ module Yoti
2
+ class Configuration
3
+ attr_accessor :client_sdk_id, :key_file_path, :key, :api_url,
4
+ :api_port, :api_version, :api_endpoint
5
+
6
+ # Set config variables by using a configuration block
7
+ def initialize
8
+ @client_sdk_id = ''
9
+ @key_file_path = ''
10
+ @key = ''
11
+ @api_url = 'https://api.yoti.com'
12
+ @api_port = 443
13
+ @api_version = 'v1'
14
+ end
15
+
16
+ # @return [String] the API endpoint for the selected API version
17
+ def api_endpoint
18
+ @api_endpoint ||= "#{@api_url}/api/#{@api_version}"
19
+ end
20
+
21
+ # Validates the configuration values set in instance variables
22
+ # @return [nil]
23
+ def validate
24
+ validate_required_all(%w(client_sdk_id))
25
+ validate_required_any(%w(key_file_path key))
26
+ validate_value('api_version', ['v1'])
27
+ end
28
+
29
+ private
30
+
31
+ # Loops through the list of required configurations and raises an error
32
+ # if a it can't find all the configuration values set
33
+ # @return [nil]
34
+ def validate_required_all(required_configs)
35
+ required_configs.each do |config|
36
+ unless config_set?(config)
37
+ message = "Configuration value `#{config}` is required."
38
+ raise ConfigurationError, message
39
+ end
40
+ end
41
+ end
42
+
43
+ # Loops through the list of required configurations and raises an error
44
+ # if a it can't find at least one configuration value set
45
+ # @return [nil]
46
+ def validate_required_any(required_configs)
47
+ valid = required_configs.select { |config| config_set?(config) }
48
+
49
+ return if valid.any?
50
+
51
+ config_list = required_configs.map { |conf| '`' + conf + '`' }.join(', ')
52
+ message = "At least one of the configuration values has to be set: #{config_list}."
53
+ raise ConfigurationError, message
54
+ end
55
+
56
+ # Raises an error if the setting receives an invalid value
57
+ # @param value [String] the value to be assigned
58
+ # @param allowed_values [Array] an array of allowed values for the variable
59
+ # @return [nil]
60
+ def validate_value(config, allowed_values)
61
+ value = instance_variable_get("@#{config}")
62
+
63
+ return unless invalid_value?(value, allowed_values)
64
+
65
+ message = "Configuration value `#{value}` is not allowed for `#{config}`."
66
+ raise ConfigurationError, message
67
+ end
68
+
69
+ # Checks if an allowed array of values includes the setting value
70
+ # @param value [String] the value to be checked
71
+ # @param allowed_values [Array] an array of allowed values for the variable
72
+ # @return [Boolean]
73
+ def invalid_value?(value, allowed_values)
74
+ allowed_values.any? && !allowed_values.include?(value)
75
+ end
76
+
77
+ # Checks if a configuration has been set as a instance variable
78
+ # @param name [String] the name of the configuration
79
+ # @return [Boolean]
80
+ def config_set?(config)
81
+ instance_variable_get("@#{config}").to_s != ''
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,13 @@
1
+ module Yoti
2
+ # Raises exceptions related to Protobuf decoding
3
+ class ProtobufError < StandardError; end
4
+
5
+ # Raises exceptions related to API requests
6
+ class RequestError < StandardError; end
7
+
8
+ # Raises exceptions related to OpenSSL actions
9
+ class SslError < StandardError; end
10
+
11
+ # Raises exceptions realted to an incorrect gem configuration value
12
+ class ConfigurationError < StandardError; end
13
+ end
@@ -0,0 +1,45 @@
1
+ require 'protobuf/message'
2
+
3
+ module Yoti
4
+ module Protobuf
5
+ module V1
6
+ module Attrpubapi
7
+ ##
8
+ # Enum Classes
9
+ #
10
+ class ContentType < ::Protobuf::Enum
11
+ define :UNDEFINED, 0
12
+ define :STRING, 1
13
+ define :JPEG, 2
14
+ define :DATE, 3
15
+ define :PNG, 4
16
+ end
17
+
18
+ ##
19
+ # Message Classes
20
+ #
21
+ class Attribute < ::Protobuf::Message; end
22
+ class Anchor < ::Protobuf::Message; end
23
+
24
+ ##
25
+ # Message Fields
26
+ #
27
+ class Attribute
28
+ optional :string, :name, 1
29
+ optional :bytes, :value, 2
30
+ optional Yoti::Protobuf::V1::Attrpubapi::ContentType, :content_type, 3
31
+ repeated Yoti::Protobuf::V1::Attrpubapi::Anchor, :anchors, 4
32
+ end
33
+
34
+ class Anchor
35
+ optional :bytes, :artifact_link, 1
36
+ repeated :bytes, :origin_server_certs, 2
37
+ optional :bytes, :artifact_signature, 3
38
+ optional :string, :sub_type, 4
39
+ optional :bytes, :signature, 5
40
+ optional :bytes, :signed_time_stamp, 6
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ require 'protobuf/message'
2
+ require_relative 'attribute.pb'
3
+
4
+ module Yoti
5
+ module Protobuf
6
+ module V1
7
+ module Attrpubapi
8
+ ##
9
+ # Message Classes
10
+ #
11
+ class AttributeAndId < ::Protobuf::Message; end
12
+ class AttributeAndIdList < ::Protobuf::Message; end
13
+ class AttributeList < ::Protobuf::Message; end
14
+
15
+ ##
16
+ # Message Fields
17
+ #
18
+ class AttributeAndId
19
+ optional Yoti::Protobuf::V1::Attrpubapi::Attribute, :attribute, 1
20
+ optional :bytes, :attribute_id, 2
21
+ end
22
+
23
+ class AttributeAndIdList
24
+ repeated Yoti::Protobuf::V1::Attrpubapi::AttributeAndId, :attribute_and_id_list, 1
25
+ end
26
+
27
+ class AttributeList
28
+ repeated Yoti::Protobuf::V1::Attrpubapi::Attribute, :attributes, 1
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ require 'protobuf/message'
2
+ require_relative 'attribute.pb'
3
+
4
+ module Yoti
5
+ module Protobuf
6
+ module V1
7
+ module Attrpubapi
8
+ ##
9
+ # Message Classes
10
+ #
11
+ class AttributeSigning < ::Protobuf::Message; end
12
+
13
+ ##
14
+ # Message Fields
15
+ #
16
+ class AttributeSigning
17
+ optional :string, :name, 1
18
+ optional :bytes, :value, 2
19
+ optional Yoti::Protobuf::V1::Attrpubapi::ContentType, :content_type, 3
20
+ optional :bytes, :artifact_signature, 4
21
+ optional :string, :sub_type, 5
22
+ optional :bytes, :signed_time_stamp, 6
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ require 'protobuf/message'
2
+
3
+ module Yoti
4
+ module Protobuf
5
+ module V1
6
+ module Compubapi
7
+ ##
8
+ # Message Classes
9
+ #
10
+ class EncryptedData < ::Protobuf::Message; end
11
+
12
+ ##
13
+ # Message Fields
14
+ #
15
+ class EncryptedData
16
+ optional :bytes, :iv, 1
17
+ optional :bytes, :cipher_text, 2
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,52 @@
1
+ //syntax = "proto2";
2
+
3
+ package attrpubapi_v1;
4
+
5
+ option java_package = "com.yoti.attrpubapi_v1";
6
+ option java_outer_classname = "AttrProto";
7
+
8
+
9
+ // ContentType indicates how to interpret the ‘Value’ field of an Attribute.
10
+ enum ContentType {
11
+ // UNDEFINED should not be seen, and is used as an error placeholder
12
+ // value.
13
+ UNDEFINED = 0;
14
+
15
+ // STRING means the value is UTF-8 encoded text.
16
+ STRING = 1;
17
+
18
+ // JPEG indicates a standard .jpeg image.
19
+ JPEG = 2;
20
+
21
+ // Date as string in RFC3339 format (YYYY-MM-DD).
22
+ DATE = 3;
23
+
24
+ // PNG indicates a standard .png image.
25
+ PNG = 4;
26
+ }
27
+
28
+
29
+ message Attribute {
30
+ optional string name = 1;
31
+
32
+ optional bytes value = 2;
33
+
34
+ optional ContentType content_type = 3;
35
+
36
+ repeated Anchor anchors = 4;
37
+ }
38
+
39
+
40
+ message Anchor {
41
+ optional bytes artifact_link = 1;
42
+
43
+ repeated bytes origin_server_certs = 2;
44
+
45
+ optional bytes artifact_signature = 3;
46
+
47
+ optional string sub_type = 4;
48
+
49
+ optional bytes signature = 5;
50
+
51
+ optional bytes signed_time_stamp = 6;
52
+ }
@@ -0,0 +1,27 @@
1
+ //syntax = "proto2";
2
+
3
+ package attrpubapi_v1;
4
+
5
+ import "Attribute.proto";
6
+
7
+ option java_package = "com.yoti.attrpubapi_v1";
8
+ option java_outer_classname = "AttrProto";
9
+
10
+
11
+ // AttributeAndId is a simple container for holding an attribute's value
12
+ // alongside its ID.
13
+ message AttributeAndId {
14
+ optional Attribute attribute = 1;
15
+
16
+ optional bytes attribute_id = 2;
17
+ }
18
+
19
+
20
+ message AttributeAndIdList{
21
+ repeated AttributeAndId attribute_and_id_list = 1;
22
+ }
23
+
24
+
25
+ message AttributeList {
26
+ repeated Attribute attributes = 1;
27
+ }
@@ -0,0 +1,23 @@
1
+ //syntax = "proto2";
2
+
3
+ package attrpubapi_v1;
4
+
5
+ import "Attribute.proto";
6
+
7
+ option java_package = "com.yoti.attrpubapi_v1";
8
+ option java_outer_classname = "AttrProto";
9
+
10
+
11
+ message AttributeSigning {
12
+ optional string name = 1;
13
+
14
+ optional bytes value = 2;
15
+
16
+ optional ContentType content_type = 3;
17
+
18
+ optional bytes artifact_signature = 4;
19
+
20
+ optional string sub_type = 5;
21
+
22
+ optional bytes signed_time_stamp = 6;
23
+ }
@@ -0,0 +1,15 @@
1
+ // syntax = "proto2";
2
+
3
+ package compubapi_v1;
4
+
5
+ option java_package = "com.yoti.compubapi_v1";
6
+ option java_outer_classname = "EncryptedDataProto";
7
+
8
+ message EncryptedData {
9
+ // the iv will be used in conjunction with the secret key
10
+ // received via other channel in order to decrypt the cipher_text
11
+ optional bytes iv = 1;
12
+
13
+ // block of bytes to be decrypted
14
+ optional bytes cipher_text = 2;
15
+ }
@@ -0,0 +1,49 @@
1
+ require 'protobuf'
2
+ require_relative 'attribute_public_api/list.pb'
3
+ require_relative 'common_public_api/encrypted_data.pb'
4
+
5
+ module Yoti
6
+ module Protobuf
7
+ class << self
8
+ CT_UNDEFINED = 0 # should not be seen, and is used as an error placeholder
9
+ CT_STRING = 1 # UTF-8 encoded text.
10
+ CT_JPEG = 2 # standard .jpeg image.
11
+ CT_DATE = 3 # string in RFC3339 format (YYYY-MM-DD)
12
+ CT_PNG = 4 # standard .png image
13
+
14
+ def current_user(receipt)
15
+ raise ProtobufError, 'The receipt has invalid data.' unless valid_recepit?(receipt)
16
+ profile_content = receipt['other_party_profile_content']
17
+ decoded_profile_content = Base64.decode64(profile_content)
18
+ V1::Compubapi::EncryptedData.decode(decoded_profile_content)
19
+ end
20
+
21
+ def attribute_list(data)
22
+ V1::Attrpubapi::AttributeList.decode(data)
23
+ end
24
+
25
+ def value_based_on_content_type(value, content_type = nil)
26
+ case content_type
27
+ when CT_UNDEFINED
28
+ raise ProtobufError, 'The content type is invalid.'
29
+ when CT_STRING
30
+ value.encode('utf-8')
31
+ when CT_JPEG
32
+ 'data:image/jpeg;base64,'.concat(Base64.strict_encode64(value))
33
+ when CT_DATE
34
+ value.encode('utf-8')
35
+ when CT_PNG
36
+ 'data:image/png;base64,'.concat(Base64.strict_encode64(value))
37
+ else
38
+ value
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def valid_recepit?(receipt)
45
+ receipt.key?('other_party_profile_content') && !receipt['other_party_profile_content'].nil?
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,58 @@
1
+ module Yoti
2
+ # Manage the API's HTTPS requests
3
+ class Request
4
+ def initialize(encrypted_connect_token)
5
+ @encrypted_connect_token = encrypted_connect_token
6
+ @auth_key = Yoti::SSL.auth_key_from_pem
7
+ end
8
+
9
+ # @return [Hash] the receipt key from the request hash response
10
+ def receipt
11
+ request['receipt']
12
+ end
13
+
14
+ private
15
+
16
+ def request
17
+ res = Net::HTTP.start(uri.hostname, Yoti.configuration.api_port, use_ssl: https_uri?) do |http|
18
+ http.request(req)
19
+ end
20
+
21
+ raise RequestError, "Unsuccessful Yoti API call: #{res.message}" unless res.code == '200'
22
+ JSON.parse(res.body)
23
+ end
24
+
25
+ def req
26
+ http_req = Net::HTTP::Get.new(uri)
27
+ http_req['X-Yoti-Auth-Key'] = @auth_key
28
+ http_req['X-Yoti-Auth-Digest'] = message_signature
29
+ http_req['Content-Type'] = 'application/json'
30
+ http_req['Accept'] = 'application/json'
31
+ http_req
32
+ end
33
+
34
+ def message_signature
35
+ @message_signature ||= Yoti::SSL.get_secure_signature("GET&#{path}")
36
+ end
37
+
38
+ def uri
39
+ @uri ||= URI(Yoti.configuration.api_endpoint + path)
40
+ end
41
+
42
+ def path
43
+ @path ||= begin
44
+ nonce = SecureRandom.uuid
45
+ timestamp = Time.now.to_i
46
+ "/profile/#{token}?nonce=#{nonce}&timestamp=#{timestamp}&appId=#{Yoti.configuration.client_sdk_id}"
47
+ end
48
+ end
49
+
50
+ def token
51
+ @token ||= Yoti::SSL.decrypt_token(@encrypted_connect_token)
52
+ end
53
+
54
+ def https_uri?
55
+ uri.scheme == 'https'
56
+ end
57
+ end
58
+ end
data/lib/yoti/ssl.rb ADDED
@@ -0,0 +1,71 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Yoti
5
+ # Manages security behaviour that requires the use of OpenSSL actions
6
+ module SSL
7
+ class << self
8
+ # Gets the private key from either a String (YOTI_KEY)
9
+ # or a pem file (YOTI_KEY_FILE_PATH)
10
+ # @return [String] the content of the private key
11
+ def pem
12
+ @pem ||= begin
13
+ if Yoti.configuration.key.to_s.empty?
14
+ File.read(Yoti.configuration.key_file_path, encoding: 'utf-8')
15
+ else
16
+ Yoti.configuration.key
17
+ end
18
+ end
19
+ end
20
+
21
+ # Uses the pem key to decrypt an encrypted connect token
22
+ # @param encrypted_connect_token [String]
23
+ # @return [String] decrypted connect token decoded in base 64
24
+ def decrypt_token(encrypted_connect_token)
25
+ raise SslError, 'Encrypted token cannot be nil.' unless encrypted_connect_token
26
+
27
+ begin
28
+ private_key.private_decrypt(Base64.urlsafe_decode64(encrypted_connect_token))
29
+ rescue => error
30
+ raise SslError, "Could not decrypt token. #{error}"
31
+ end
32
+ end
33
+
34
+ # Extracts the public key from pem key, converts it to a DER base 64 encoded value
35
+ # @return [String] base 64 encoded anthentication key
36
+ def auth_key_from_pem
37
+ public_key = private_key.public_key
38
+ Base64.strict_encode64(public_key.to_der)
39
+ end
40
+
41
+ # Sign message using a secure SHA256 hash and the private key
42
+ # @param message [String] message to be signed
43
+ # @return [String] signed message encoded in base 64
44
+ def get_secure_signature(message)
45
+ digest = OpenSSL::Digest::SHA256.new
46
+ Base64.strict_encode64(private_key.sign(digest, message))
47
+ end
48
+
49
+ # Uses the decrypted receipt key and the current user's iv to decode the text
50
+ # @param key [String] base 64 decoded key
51
+ # @param iv [String] base 64 decoded iv
52
+ # @param text [String] base 64 decoded cyphered text
53
+ # @return [String] base 64 decoded deciphered text
54
+ def decipher(key, iv, text)
55
+ ssl_decipher = OpenSSL::Cipher.new('AES-256-CBC')
56
+ ssl_decipher.decrypt
57
+ ssl_decipher.key = key
58
+ ssl_decipher.iv = iv
59
+ ssl_decipher.update(text) + ssl_decipher.final
60
+ end
61
+
62
+ private
63
+
64
+ def private_key
65
+ @private_key ||= OpenSSL::PKey::RSA.new(pem)
66
+ rescue => error
67
+ raise SslError, "The secure key is invalid. #{error}"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,4 @@
1
+ module Yoti
2
+ # @return [String] the gem's current version
3
+ VERSION = '1.0.0'.freeze
4
+ end
data/lib/yoti.rb ADDED
@@ -0,0 +1,21 @@
1
+ require_relative 'yoti/version'
2
+ require_relative 'yoti/configuration'
3
+ require_relative 'yoti/errors'
4
+ require_relative 'yoti/ssl'
5
+ require_relative 'yoti/request'
6
+ require_relative 'yoti/activity_details'
7
+ require_relative 'yoti/client'
8
+ require_relative 'yoti/protobuf/v1/protobuf'
9
+
10
+ # The main module namespace of the Yoti gem
11
+ module Yoti
12
+ class << self
13
+ attr_accessor :configuration
14
+ end
15
+
16
+ def self.configure
17
+ self.configuration ||= Configuration.new
18
+ yield(configuration)
19
+ configuration.validate
20
+ end
21
+ end
data/login_flow.png ADDED
Binary file