diff --git a/examples/fact_request/custom_objects.rb b/examples/fact_request/custom_objects.rb new file mode 100644 index 0000000..3af17e8 --- /dev/null +++ b/examples/fact_request/custom_objects.rb @@ -0,0 +1,67 @@ +# Copyright 2020 Self Group Ltd. All Rights Reserved. + +# frozen_string_literal: true + +require_relative '../../lib/selfsdk' + +# Process input data +abort("provide self_id to request information to") if ARGV.length != 1 +user = ARGV.first +SelfSDK.logger = ::Logger.new($stdout).tap do |log| + log.progname = "SelfSDK examples" +end if ENV.has_key?'LOGS' + +# You can point to a different environment by passing optional values to the initializer +opts = ENV.has_key?('SELF_ENV') ? { env: ENV["SELF_ENV"] } : {} +storage_dir = "#{File.expand_path("..", File.dirname(__FILE__))}/self_storage" + +# Connect your app to Self network, get your connection details creating a new +# app on https://developer.selfsdk.net/ +puts 'connecting...' +@app = SelfSDK::App.new(ENV["SELF_APP_ID"], ENV["SELF_APP_DEVICE_SECRET"], ENV["STORAGE_KEY"], storage_dir, opts).start + +# Create a custom fact and send it to the user. +puts 'issuing custom facts' + + +begin + data = File.binread("my_image.png") +rescue => e + puts "\u26A0 You must provide a PNG image to be stored as a user's fact" + exit! +end +obj = @app.new_object("my_image", data, "image/png") + +my_fact = SelfSDK::Services::Facts::Fact.new( + "display image", + obj, + "source12") + +@app.facts.issue(user, [my_fact]) +sleep 5 + +# Request the custom fact +begin + @app.facts.request(user, [{ fact: my_fact.key, issuers: [ENV["SELF_APP_ID"]] }]) do |res| + # Information request has been rejected by the user + if res.status == "rejected" + puts 'Information request rejected' + exit! + end + + # Response comes in form of facts easy to access with facts method + hash = res.attestation(my_fact.key.to_sym).value + puts "Your stored fact is #{res.attestation(my_fact.key.to_sym).value}!" + o = res.object(hash) + + o.save("/tmp/received.jpg") + puts "Received object hash is #{o.object_hash}" + exit! + end +rescue => e + puts "ERROR : #{e}" + exit! +end + +# Wait for asyncrhonous process to finish +sleep 1000 diff --git a/lib/chat/file_object.rb b/lib/chat/file_object.rb index 4350543..02e8c9d 100644 --- a/lib/chat/file_object.rb +++ b/lib/chat/file_object.rb @@ -6,7 +6,7 @@ module SelfSDK module Chat class FileObject - attr_accessor :name, :link, :mime, :content, :key, :nonce, :ciphertext + attr_accessor :name, :link, :mime, :content, :object_hash, :key, :nonce, :ciphertext def initialize(token, url) @token = token @@ -17,6 +17,7 @@ def build_from_data(name, data, mime, opts = {}) @key = SelfCrypto::Util.aead_xchacha20poly1305_ietf_keygen @nonce = SelfCrypto::Util.aead_xchacha20poly1305_ietf_nonce @content = data + @object_hash = calculate_hash(@content) @name = name @mime = mime @@ -54,6 +55,7 @@ def build_from_object(input) end @content = ciphertext + @key = nil @nonce = nil if input.key?(:key) && !input[:key].empty? @@ -65,6 +67,11 @@ def build_from_object(input) @content = SelfCrypto::Util.aead_xchacha20poly1305_ietf_decrypt(@key, @nonce, ciphertext) end + @object_hash = calculate_hash(@content) + if @object_hash != input[:object_hash] + raise "File hash does not match the originally signed hash." + end + @name = input[:name] @link = input[:link] @mime = input[:mime] @@ -81,7 +88,8 @@ def to_payload key: k, mime: @mime, expires: @expires, - public: (k == "") + public: (k == ""), + object_hash: @object_hash } end @@ -111,6 +119,12 @@ def extract_key(shareable_key) { key: k[0, 32], nonce: k[32, (k.length - 32)] } end + + def calculate_hash(ct) + d = Digest::SHA256.digest(ct) + Base64.urlsafe_encode64(d).gsub(/=+\Z/, '') + end + end end end diff --git a/lib/client.rb b/lib/client.rb index 5b05f35..67124e6 100644 --- a/lib/client.rb +++ b/lib/client.rb @@ -80,7 +80,7 @@ def post(endpoint, body) def get(endpoint) safe_request do - HTTParty.get("#{@self_url}#{endpoint}", + HTTParty.get("#{@self_url}#{endpoint}", headers: { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{@jwt.auth_token}" diff --git a/lib/messages/fact_issue.rb b/lib/messages/fact_issue.rb index 5fe725f..2ae15d8 100644 --- a/lib/messages/fact_issue.rb +++ b/lib/messages/fact_issue.rb @@ -25,6 +25,7 @@ def populate(selfid, facts, opts) @from = @jwt.id @to = selfid @attestations = build_attestations!(facts) + @objects = build_objects(facts) end def body @@ -42,6 +43,7 @@ def body } # viewers b[:viewers] = @viewers unless @viewers.nil? + b[:objects] = @objects unless @objects.empty? b end @@ -68,7 +70,8 @@ def build_attestations!(facts) raise 'facts must be provided in the form of an array' unless facts.kind_of?(Array) attestations = [] - facts.each do |fact| + facts.each do |f| + fact = f.to_hash att = fact.transform_keys(&:to_sym) raise 'invalid attestation : does not provide a key' if !att.has_key?(:key) || att[:key].empty? @@ -81,6 +84,16 @@ def build_attestations!(facts) attestations end + def build_objects(facts) + objects = [] + facts.each do |fact| + if fact.respond_to? :object and !fact.object.nil? + objects << fact.object.to_payload + end + end + objects + end + def sign(source, facts) fact = { jti: SecureRandom.uuid, sub: @to, diff --git a/lib/messages/fact_response.rb b/lib/messages/fact_response.rb index 5b6f71f..ab12d64 100644 --- a/lib/messages/fact_response.rb +++ b/lib/messages/fact_response.rb @@ -113,12 +113,13 @@ def auth_response? def object(hash) payload[:objects].each do |o| - if o[:image_hash] == hash + if o[:object_hash] == hash || o[:image_hash] == hash return SelfSDK::Chat::FileObject.new( @messaging.client.jwt.auth_token, @messaging.client.self_url).build_from_object(o) end end + return nil end end end diff --git a/lib/selfsdk.rb b/lib/selfsdk.rb index 2630d13..07cf1b6 100644 --- a/lib/selfsdk.rb +++ b/lib/selfsdk.rb @@ -132,6 +132,12 @@ def close @messaging_client.close end + def new_object(name, content, mime) + o = SelfSDK::Chat::FileObject.new(client.jwt.auth_token, client.self_url) + o.build_from_data(name, content, mime) + o + end + protected def requester @@ -152,9 +158,9 @@ def messaging_url(opts) def cleanup_key(key) return key unless key.include? '_' - + key.split('_').last end - + end end diff --git a/lib/services/facts.rb b/lib/services/facts.rb index 47a4122..20bec75 100644 --- a/lib/services/facts.rb +++ b/lib/services/facts.rb @@ -100,24 +100,26 @@ def generate_deep_link(facts, callback, opts = {}) # @param facts [Array] facts to be sent to the user # @option opts [String] :viewers list of self identifiers for the user that will have access to this facts. def issue(selfid, facts, opts = {}) - hased_facts = [] - facts.each do |f| - hased_facts << f.to_hash - end - SelfSDK.logger.info "issuing facts for #{selfid}" msg = SelfSDK::Messages::FactIssue.new(@requester.messaging) - msg.populate(selfid, hased_facts, opts) + msg.populate(selfid, facts, opts) msg.send_message end # Facts to be issued class Fact - attr_accessor :key, :value, :group + attr_accessor :key, :value, :group, :object def initialize(key, value, source, opts = {}) + if value.is_a?(SelfSDK::Chat::FileObject) + @object = value + value = @object.object_hash + elsif !value.is_a?(String) + raise "supported values are strings and FileObject" + end + @key = key @value = value @source = source diff --git a/test/messages/fact_test.rb b/test/messages/fact_test.rb index 0dd27ad..e5c8dee 100644 --- a/test/messages/fact_test.rb +++ b/test/messages/fact_test.rb @@ -39,7 +39,7 @@ def test_parse assert_equal "display_name", parsed_fact[:fact] assert_equal "==", parsed_fact[:operator] - assert_equal nil, parsed_fact[:attestations] + assert_nil parsed_fact[:attestations] assert_equal "lol", parsed_fact[:expected_value] end end