yoti 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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