selfsdk 0.0.124

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: df6729e3320be7cc2abd4ea22a94ff757b8e8d72ee5e1f2b2ddbafed509483c4
4
+ data.tar.gz: c7202790ad7ac8c0fcb984a3c494983b08549e9e516b238cc247d869a630d2a2
5
+ SHA512:
6
+ metadata.gz: 860606a05e95e553e6c1e36c63b15a1e3b93922d56e0a58a773d911da0ea29d95f5395406b13124b005e05c468d1c2740308e55908decffc77eb79d6c569ac14
7
+ data.tar.gz: 7a3ba36d29276730966cc01ce4d2730c0613142ce6d844deb61380a3b3055596e724bfe78160061436cf43f9f510b49b7407a3a9eb107e0e057c094f9d8e7f32
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ # Namespace for classes and modules that handle Self interactions.
6
+ module SelfSDK
7
+ # Access control list
8
+ class ACL
9
+ def initialize(messaging)
10
+ @messaging = messaging
11
+ @jwt = @messaging.jwt
12
+ end
13
+
14
+ # Lists allowed connections.
15
+ def list
16
+ SelfSDK.logger.info "Listing allowed connections"
17
+ rules = {}
18
+ @messaging.list_acl_rules.each do |c|
19
+ rules[c['acl_source']] = DateTime.parse(c['acl_exp'])
20
+ end
21
+ rules
22
+ end
23
+
24
+ # Allows incomming messages from the given identity.
25
+ def allow(id)
26
+ SelfSDK.logger.info "Allowing connections from #{id}"
27
+ @messaging.add_acl_rule(@jwt.prepare(jti: SecureRandom.uuid,
28
+ cid: SecureRandom.uuid,
29
+ typ: 'acl.permit',
30
+ iss: @jwt.id,
31
+ sub: @jwt.id,
32
+ iat: (SelfSDK::Time.now - 5).strftime('%FT%TZ'),
33
+ exp: (SelfSDK::Time.now + 60).strftime('%FT%TZ'),
34
+ acl_source: id,
35
+ acl_exp: (SelfSDK::Time.now + 360_000).to_datetime.rfc3339))
36
+ end
37
+
38
+ # Deny incomming messages from the given identity.
39
+ def deny(id)
40
+ SelfSDK.logger.info "Denying connections from #{id}"
41
+ @messaging.remove_acl_rule(@jwt.prepare(jti: SecureRandom.uuid,
42
+ cid: SecureRandom.uuid,
43
+ typ: 'acl.revoke',
44
+ iss: @jwt.id,
45
+ sub: @jwt.id,
46
+ iat: (SelfSDK::Time.now - 5).strftime('%FT%TZ'),
47
+ exp: (SelfSDK::Time.now + 60).strftime('%FT%TZ'),
48
+ acl_source: id,
49
+ acl_exp: (SelfSDK::Time.now + 360_000).to_datetime.rfc3339))
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ module SelfSDK
2
+ class Authenticated
3
+ attr_accessor :payload, :uuid, :selfsdk, :status
4
+
5
+ def initialize(payload)
6
+ return if payload.nil?
7
+
8
+ @payload = payload
9
+ @uuid = payload[:cid]
10
+ @selfsdk = payload[:sub]
11
+ end
12
+
13
+ def accepted?
14
+ return false if @payload.nil?
15
+
16
+ @payload[:status] == "accepted"
17
+ end
18
+
19
+ def to_hash
20
+ { uuid: @uuid,
21
+ selfsdk: @selfsdk,
22
+ accepted: accepted? }
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+
5
+ module SelfSDK
6
+ class RestClient
7
+ attr_reader :self_url, :jwt, :env
8
+
9
+ # RestClient initializer
10
+ #
11
+ # @param url [string] self-messaging url
12
+ # @param token [string] jwt token identifying the authenticated user
13
+ def initialize(url, app_id, app_key, env)
14
+ SelfSDK.logger.info "client setup with #{url}"
15
+ @self_url = url
16
+ @env = env
17
+ @jwt = SelfSDK::JwtService.new(app_id, app_key)
18
+ end
19
+
20
+ # Get identity details
21
+ #
22
+ # @param id [string] identity self_id.
23
+ def identity(id)
24
+ get_identity "/v1/identities/#{id}"
25
+ end
26
+
27
+ # Get app details
28
+ #
29
+ # @param id [string] app self_id.
30
+ def app(id)
31
+ get_identity "/v1/apps/#{id}"
32
+ end
33
+
34
+ # Get app/identity details
35
+ #
36
+ # @param id [string] app/identity self_id.
37
+ def entity(id)
38
+ #TODO : Consider a better check for this conditional
39
+ if id.length == 11
40
+ return identity(id)
41
+ else
42
+ return app(id)
43
+ end
44
+ end
45
+
46
+ # Lists all devices assigned to the given identity
47
+ #
48
+ # @param id [string] identity id
49
+ def devices(id)
50
+ res = get "/v1/identities/#{id}/devices"
51
+ body = JSON.parse(res.body, symbolize_names: true)
52
+ if res.code != 200
53
+ SelfSDK.logger.error "identity response : #{body[:message]}"
54
+ raise "you need connection permissions"
55
+ end
56
+ body
57
+ end
58
+
59
+ # Lists all public keys stored on self for the given ID
60
+ #
61
+ # @param id [string] identity id
62
+ def public_keys(id)
63
+ i = entity(id)
64
+ i[:public_keys]
65
+ end
66
+
67
+ private
68
+
69
+ def get_identity(endpoint)
70
+ res = get endpoint
71
+ body = JSON.parse(res.body, symbolize_names: true)
72
+ if res.code != 200
73
+ SelfSDK.logger.error "app response : #{body[:message]}"
74
+ raise body[:message]
75
+ end
76
+ body
77
+ end
78
+
79
+ def get(endpoint)
80
+ HTTParty.get("#{@self_url}#{endpoint}", headers: {
81
+ 'Content-Type' => 'application/json',
82
+ 'Authorization' => "Bearer #{@jwt.auth_token}"
83
+ })
84
+ end
85
+
86
+ def post(endpoint, body)
87
+ HTTParty.post("#{@self_url}#{endpoint}",
88
+ headers: {
89
+ 'Content-Type' => 'application/json',
90
+ 'Authorization' => "Bearer #{@jwt.auth_token}"
91
+ },
92
+ body: body)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+
6
+ module SelfSDK
7
+ class JwtService
8
+ attr_reader :id, :key
9
+
10
+ # Jwt initializer
11
+ #
12
+ # @param app_id [string] the app id.
13
+ # @param app_key [string] the app api key provided by developer portal.
14
+ def initialize(app_id, app_key)
15
+ @id = app_id
16
+ @key = app_key
17
+ end
18
+
19
+ # Prepares a jwt object based on an input
20
+ #
21
+ # @param input [string] input to be prepared
22
+ def prepare(input)
23
+ signed(input).to_json
24
+ end
25
+
26
+ def signed(input)
27
+ payload = encode(input.to_json)
28
+ {
29
+ payload: payload,
30
+ protected: header,
31
+ signature: sign("#{header}.#{payload}")
32
+ }
33
+ end
34
+
35
+ def parse(input)
36
+ JSON.parse(input, symbolize_names: true)
37
+ end
38
+
39
+ # Encodes the input with base64
40
+ #
41
+ # @param input [string] the string to be encoded.
42
+ def encode(input)
43
+ Base64.urlsafe_encode64(input, padding: false)
44
+ end
45
+
46
+ # Base64 decodes the input string
47
+ #
48
+ # @param input [string] the string to be decoded.
49
+ def decode(input)
50
+ Base64.urlsafe_decode64(input)
51
+ end
52
+
53
+ # Signs the given input with the configured Ed25519 key.
54
+ #
55
+ # @param input [string] the string to be signed.
56
+ def sign(input)
57
+ signing_key = Ed25519::SigningKey.new(decode(@key))
58
+ signature = signing_key.sign(input)
59
+ encode(signature)
60
+ end
61
+
62
+ def verify(payload, key)
63
+ verify_key = Ed25519::VerifyKey.new(decode(key))
64
+ if verify_key.verify(decode(payload[:signature]), "#{payload[:protected]}.#{payload[:payload]}")
65
+ return true
66
+ end
67
+ rescue StandardError
68
+ false
69
+ end
70
+
71
+ # Generates the auth_token based on the app's private key.
72
+ def auth_token
73
+ payload = header + "." + encode({
74
+ jti: SecureRandom.uuid,
75
+ cid: SecureRandom.uuid,
76
+ typ: 'auth.token',
77
+ iat: (SelfSDK::Time.now - 5).to_i,
78
+ exp: (SelfSDK::Time.now + 60).to_i,
79
+ iss: @id,
80
+ sub: @id}.to_json)
81
+ signature = sign(payload)
82
+ "#{payload}.#{signature}"
83
+ end
84
+
85
+ private
86
+
87
+ def header
88
+ encode({ alg: "EdDSA", typ: "JWT" }.to_json)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module SelfSDK
6
+ class << self
7
+ attr_writer :logger
8
+
9
+ def logger
10
+ @logger ||= ::Logger.new($stdout).tap do |log|
11
+ log.progname = name
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelfSDK
4
+ module Messages
5
+ class Attestation
6
+ attr_accessor :verified, :origin, :source, :value, :operator, :expected_value, :fact_name, :to, :audience
7
+
8
+ def initialize(messaging)
9
+ @messaging = messaging
10
+ end
11
+
12
+ def parse(name, attestation)
13
+ payload = JSON.parse(@messaging.jwt.decode(attestation[:payload]), symbolize_names: true)
14
+ @origin = payload[:iss]
15
+ @to = payload[:sub]
16
+ @audience = payload[:aud]
17
+ @source = payload[:source]
18
+ @verified = valid_signature?(attestation)
19
+ @expected_value = payload[:expected_value]
20
+ @operator = payload[:operator]
21
+ @fact_name = name.to_s
22
+ unless payload[name].nil?
23
+ @value = payload[name]
24
+ end
25
+ end
26
+
27
+ def valid_signature?(body)
28
+ k = @messaging.client.public_keys(@origin).first[:key]
29
+ raise ::StandardError.new("invalid signature") unless @messaging.jwt.verify(body, k)
30
+
31
+ true
32
+ end
33
+
34
+ def validate!(original)
35
+ raise ::StandardError.new("invalid origin") if @to != original.to
36
+ end
37
+
38
+ def signed
39
+ o = {
40
+ sub: @to,
41
+ iss: @origin,
42
+ source: @source,
43
+ fact: @fact_name,
44
+ expected_value: @expected_value,
45
+ operator: @operator,
46
+ }
47
+ o[:aud] = @audience unless @audience.nil?
48
+ o[@fact_name.to_sym] = @value
49
+ @messaging.jwt.signed(o)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../ntptime'
5
+
6
+ module SelfSDK
7
+ module Messages
8
+ class AuthenticationMessage < Base
9
+
10
+ def parse(input, original=nil)
11
+ @input = input
12
+ @typ = @typ
13
+ @payload = get_payload input
14
+ @id = payload[:cid]
15
+ @from = payload[:iss]
16
+ @to = payload[:sub]
17
+ @audience = payload[:aud]
18
+ @from_device = payload[:device_id]
19
+ @expires = ::Time.parse(payload[:exp])
20
+ @issued = ::Time.parse(payload[:iat])
21
+ @status = payload[:status]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../ntptime'
5
+ require_relative 'authentication_message'
6
+
7
+ module SelfSDK
8
+ module Messages
9
+ class AuthenticationReq < AuthenticationMessage
10
+ MSG_TYPE = "identities.authenticate.req"
11
+ DEFAULT_EXP_TIMEOUT = 300
12
+
13
+ def initialize(messaging)
14
+ @typ = MSG_TYPE
15
+ super
16
+ end
17
+
18
+ def populate(selfid, opts)
19
+ @id = SecureRandom.uuid
20
+ @from = @client.jwt.id
21
+ @to = selfid
22
+
23
+ @id = opts[:cid] if opts.include?(:cid)
24
+ @description = opts.include?(:description) ? opts[:description] : nil
25
+ @exp_timeout = opts.fetch(:exp_timeout, DEFAULT_EXP_TIMEOUT)
26
+ end
27
+
28
+ def body
29
+ { typ: MSG_TYPE,
30
+ iss: @jwt.id,
31
+ sub: @to,
32
+ aud: @to,
33
+ iat: SelfSDK::Time.now.strftime('%FT%TZ'),
34
+ exp: (SelfSDK::Time.now + @exp_timeout).strftime('%FT%TZ'),
35
+ cid: @id,
36
+ jti: SecureRandom.uuid }
37
+ end
38
+
39
+ protected
40
+
41
+ def proto
42
+ @to_device = @client.devices(@to).first
43
+ Msgproto::Message.new(type: Msgproto::MsgType::MSG,
44
+ sender: "#{@jwt.id}:#{@messaging.device_id}",
45
+ id: @id,
46
+ recipient: "#{@to}:#{@to_device}",
47
+ ciphertext: @jwt.prepare(body))
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative '../ntptime'
5
+ require_relative 'authentication_message'
6
+
7
+ module SelfSDK
8
+ module Messages
9
+ class AuthenticationResp < AuthenticationMessage
10
+ MSG_TYPE = "identities.authenticate.resp"
11
+ def initialize(messaging)
12
+ @typ = MSG_TYPE
13
+ super
14
+ end
15
+
16
+ protected
17
+
18
+ def proto
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ end