ephemeral_calc 0.2.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.
@@ -0,0 +1,32 @@
1
+ #include "ruby.h"
2
+
3
+ VALUE curve25519_module = Qnil;
4
+
5
+ int curve25519_donna(uint8_t *mypublic, const uint8_t *secret, const uint8_t *basepoint);
6
+
7
+ VALUE method_mult(VALUE klass, VALUE a, VALUE b)
8
+ {
9
+ VALUE result;
10
+ if (TYPE(a) != T_STRING || RSTRING_LEN(a) != 32 ||
11
+ TYPE(b) != T_STRING || RSTRING_LEN(b) != 32)
12
+ {
13
+ rb_raise(rb_eArgError, "Both arguments must be 32 byte strings");
14
+ }
15
+ result = rb_str_buf_new(32);
16
+ rb_str_set_len(result, 32);
17
+ curve25519_donna(
18
+ (uint8_t*)RSTRING_PTR(result),
19
+ (uint8_t*)RSTRING_PTR(a),
20
+ (uint8_t*)RSTRING_PTR(b)
21
+ );
22
+ return result;
23
+ }
24
+
25
+ void Init_curve25519()
26
+ {
27
+ VALUE ephemeral_calc_module = rb_const_get(rb_cObject, rb_intern("EphemeralCalc"));
28
+ curve25519_module = rb_define_module_under(ephemeral_calc_module, "Curve25519");
29
+ rb_define_singleton_method(curve25519_module, "mult", method_mult, 2);
30
+ uint8_t basepoint[32] = {9};
31
+ rb_define_const(curve25519_module, "BASEPOINT", rb_str_new( (char*)basepoint, 32) );
32
+ }
@@ -0,0 +1,11 @@
1
+ # Loads mkmf which is used to make makefiles for Ruby extensions
2
+ require 'mkmf'
3
+
4
+ # Give it a name
5
+ extension_name = 'ephemeral_calc/curve25519'
6
+
7
+ # The destination
8
+ dir_config(extension_name)
9
+
10
+ create_makefile(extension_name)
11
+
@@ -0,0 +1,94 @@
1
+ require 'openssl'
2
+
3
+ module EphemeralCalc
4
+ class Encryptor
5
+
6
+ attr_accessor :identity_key
7
+ attr_accessor :rotation_scalar
8
+ attr_accessor :initial_time
9
+
10
+ def initialize(identity_key, rotation_scalar, initial_time = Time.now)
11
+ if identity_key.size == 32
12
+ # 32 characters means this is a 16-byte hex string
13
+ self.identity_key = [identity_key].pack("H*")
14
+ else
15
+ self.identity_key = identity_key
16
+ end
17
+ self.rotation_scalar = rotation_scalar
18
+ self.initial_time = initial_time.to_i
19
+ end
20
+
21
+ def beacon_time
22
+ Time.now.to_i - self.initial_time
23
+ end
24
+
25
+ def quantum
26
+ beacon_time / (2**rotation_scalar)
27
+ end
28
+
29
+ # Output is an 8-byte encrypted identifier as a hex string
30
+ # e.g. "0102030405060708"
31
+ def get_identifier(beacon_time = nil)
32
+ beacon_time ||= self.beacon_time
33
+ return nil if beacon_time < 0
34
+ temporary_key = do_aes_encryption(self.identity_key, key_generation_data_block(beacon_time))
35
+ encrypted_data = do_aes_encryption(temporary_key, data_block(beacon_time)).bytes.to_a
36
+ # the identifier is the first 8 bytes of the encrypted output
37
+ identifier_array = encrypted_data[0,8]
38
+ identifier_array.map{|b| sprintf("%02X",b)}.join
39
+ end
40
+
41
+ # yields the current EID and each subsuquent EID, until the block returns :stop
42
+ def each_identifier
43
+ last_quantum = nil
44
+ loop do
45
+ if quantum != last_quantum
46
+ last_quantum = quantum
47
+ break if :stop == yield( get_identifier )
48
+ end
49
+ sleep 1
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def seconds_counter_32_bit(time_in_seconds)
56
+ time_in_seconds ||= Time.now.to_i
57
+ time_in_seconds.to_i & 0xffffffff
58
+ end
59
+
60
+ def key_generation_data_block(time_in_seconds=nil)
61
+ time_b2 = (seconds_counter_32_bit(time_in_seconds) >> 16) & 0xff
62
+ time_b3 = (seconds_counter_32_bit(time_in_seconds) >> 24) & 0xff
63
+ [
64
+ 0, 0, 0, 0,
65
+ 0, 0, 0, 0,
66
+ 0, 0, 0, 0xff,
67
+ 0, 0, time_b3, time_b2
68
+ ].pack('c*')
69
+ end
70
+
71
+ # this is the 16 byte block to encrypt as an array of 16 numbers
72
+ def data_block(time_in_seconds=nil)
73
+ time = seconds_counter_32_bit(time_in_seconds) & (0xffffffff - (2**rotation_scalar-1))
74
+ time_b0 = time & 0xff
75
+ time_b1 = (time >> 8) & 0xff
76
+ time_b2 = (time >> 16) & 0xff
77
+ time_b3 = (time >> 24) & 0xff
78
+ [
79
+ 0, 0, 0, 0,
80
+ 0, 0, 0, 0,
81
+ 0, 0, 0, rotation_scalar,
82
+ time_b3, time_b2, time_b1, time_b0
83
+ ].pack('c*')
84
+ end
85
+
86
+ def do_aes_encryption(key, data)
87
+ aes = OpenSSL::Cipher::Cipher.new("AES-128-ECB")
88
+ aes.encrypt
89
+ aes.key = key
90
+ #aes.iv = "what do I put here?"
91
+ aes.update(data) + aes.final
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,82 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require 'base64'
5
+
6
+ module EphemeralCalc
7
+ module GoogleAPI
8
+ class Client
9
+
10
+ PROXIMITY_BEACON_ROOT = "https://proximitybeacon.googleapis.com/v1beta1/"
11
+ EIDPARAMS_URI = URI(PROXIMITY_BEACON_ROOT + "eidparams")
12
+ BEACON_REGISTER_URI = URI(PROXIMITY_BEACON_ROOT + "beacons:register")
13
+
14
+ attr_accessor :credentials
15
+
16
+ def initialize(credentials = OAuth.new.get_credentials)
17
+ self.credentials = credentials
18
+ end
19
+
20
+ def eidparams
21
+ response = Request.get(EIDPARAMS_URI, credentials)
22
+ # response = request(:get, EIDPARAMS_URI, credentials.access_token)
23
+ return JSON.parse(response.body)
24
+ end
25
+
26
+ def register_eid(beacon_public_key, rotation_exp, initial_eid, initial_clock, uid_bytes)
27
+ service_public_key_base64 = get_eidparams["serviceEcdhPublicKey"]
28
+ response = Request.post(BEACON_REGISTER_URI, credentials) {|request|
29
+ request.add_field "Content-Type", "application/json"
30
+ request.body = {
31
+ ephemeralIdRegistration: {
32
+ beaconEcdhPublicKey: Base64.strict_encode64(beacon_public_key),
33
+ serviceEcdhPublicKey: service_public_key_base64,
34
+ rotationPeriodExponent: rotation_exp,
35
+ initialClockValue: initial_clock,
36
+ initialEid: Base64.strict_encode64(initial_eid)
37
+ },
38
+ advertisedId: {
39
+ type: "EDDYSTONE",
40
+ id: Base64.strict_encode64(uid_bytes)
41
+ },
42
+ status: "ACTIVE",
43
+ description: "EphemeralCalc Registered EID"
44
+ }.to_json
45
+ }
46
+ JSON.parse(response.body)
47
+ end
48
+
49
+ def get_resource(resource_name)
50
+ uri = URI(PROXIMITY_BEACON_ROOT + resource_name)
51
+ response = Request.get(uri, credentials)
52
+ JSON.parse(response.body)
53
+ end
54
+
55
+ def getforobserved(eids, api_key = ENV["GOOGLE_API_KEY"])
56
+ uri = URI("#{PROXIMITY_BEACON_ROOT}beaconinfo:getforobserved?key=#{api_key}")
57
+ response = Request.post(uri) {|request|
58
+ observations = Array(eids).map {|eid|
59
+ {advertisedId: {type: "EDDYSTONE_EID", id: base64_eid(eid)}}
60
+ }
61
+ request.body = {
62
+ observations: observations,
63
+ namespacedTypes: "*",
64
+ }.to_json
65
+ request.add_field "Content-Type", "application/json"
66
+ }
67
+ JSON.parse(response.body)
68
+ end
69
+
70
+ private
71
+
72
+ def base64_eid(eid)
73
+ if eid.size == 16
74
+ Base64.strict_encode64([eid].pack("H*"))
75
+ else
76
+ Base64.strict_encode64(eid)
77
+ end
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,39 @@
1
+ require 'yaml'
2
+
3
+ module EphemeralCalc
4
+ module GoogleAPI
5
+ class Credentials
6
+
7
+ DEFAULT_FILE_STORE = File.expand_path("~/.ephemeral_calc_google_credentials.yaml")
8
+ attr_accessor :access_token, :refresh_token, :expires_at
9
+
10
+ def self.from_file(file = DEFAULT_FILE_STORE)
11
+ return nil unless File.exist?(file)
12
+ self.new YAML.load_file(file)
13
+ end
14
+
15
+ def initialize(opts = {})
16
+ # convert string keys to symbols
17
+ opts = opts.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
18
+
19
+ self.access_token = opts[:access_token]
20
+ self.refresh_token = opts[:refresh_token]
21
+ self.expires_at = opts[:expires_at] || (Time.now + opts[:expires_in].to_i)
22
+ end
23
+
24
+ def save(file = DEFAULT_FILE_STORE)
25
+ data = {
26
+ access_token: access_token,
27
+ refresh_token: refresh_token,
28
+ expires_at: expires_at
29
+ }.to_yaml
30
+ File.open(file, 'w') {|f| f.write(data) }
31
+ end
32
+
33
+ def expired?
34
+ self.expires_at < Time.now
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,87 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'net/http'
4
+
5
+ module EphemeralCalc
6
+ module GoogleAPI
7
+ class OAuth
8
+
9
+ SCOPES = [
10
+ "https://www.googleapis.com/auth/userlocation.beacon.registry",
11
+ "https://www.googleapis.com/auth/cloud-platform",
12
+ ]
13
+
14
+ REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
15
+ TOKEN_URI = URI("https://www.googleapis.com/oauth2/v4/token")
16
+
17
+ attr_accessor :client_id, :secret
18
+
19
+ def initialize(client_id = ENV["GOOGLE_CLIENT_ID"], secret = ENV["GOOGLE_CLIENT_SECRET"])
20
+ if client_id.nil? || secret.nil?
21
+ raise ArgumentError, "No Google Client ID or Secret was set. These can set in the environment variables \"GOOGLE_CLIENT_ID\" and \"GOOGLE_CLIENT_SECRET\" respectively. Credentials must be created for your project at \"https://console.developers.google.com/apis/credentials\"."
22
+ end
23
+ self.client_id = client_id
24
+ self.secret = secret
25
+ end
26
+
27
+ def get_code
28
+ puts("Performing OAuth with Google...")
29
+ if RUBY_PLATFORM =~ /darwin/
30
+ system("open \"#{url}\"")
31
+ else
32
+ puts("Open this URL in your browser: \"#{url}\"\n\n")
33
+ end
34
+ printf "Copy and paste code from web browser here: "
35
+ _code = STDIN.gets.chomp
36
+ end
37
+
38
+ def get_credentials(old_credentials = Credentials.from_file)
39
+ return old_credentials if old_credentials && !old_credentials.expired?
40
+ response = Request.post(TOKEN_URI) {|request|
41
+ request.body = hash_to_params( token_request_params(old_credentials) )
42
+ }
43
+ json = JSON.parse(response.body)
44
+ credentials = Credentials.new(json)
45
+ if old_credentials
46
+ credentials.refresh_token = old_credentials.refresh_token
47
+ end
48
+ return credentials
49
+ end
50
+
51
+ def url
52
+ params = hash_to_params(
53
+ scope: SCOPES.join("%20"),
54
+ redirect_uri: REDIRECT_URI,
55
+ response_type: "code",
56
+ client_id: client_id,
57
+ )
58
+ "https://accounts.google.com/o/oauth2/v2/auth?#{params}"
59
+ end
60
+
61
+ def token_request_params(old_credentials)
62
+ if old_credentials == nil
63
+ {
64
+ code: get_code,
65
+ client_id: client_id,
66
+ client_secret: secret,
67
+ redirect_uri: REDIRECT_URI,
68
+ grant_type: "authorization_code",
69
+ }
70
+ else
71
+ # this is a refresh of the old credentials
72
+ {
73
+ refresh_token: old_credentials.refresh_token,
74
+ client_id: client_id,
75
+ client_secret: secret,
76
+ grant_type: "refresh_token",
77
+ }
78
+ end
79
+ end
80
+
81
+ def hash_to_params(hash)
82
+ hash.map {|k,v| "#{k}=#{v}"}.join("&")
83
+ end
84
+
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,69 @@
1
+ require 'forwardable'
2
+ require 'uri'
3
+ require 'net/http'
4
+ require 'base64'
5
+
6
+ module EphemeralCalc
7
+ module GoogleAPI
8
+ class Request
9
+ extend Forwardable
10
+
11
+ attr_accessor :method, :uri, :credentials
12
+ def_delegators :request, :add_field, :body=
13
+
14
+ def self.get(uri, credentials = nil)
15
+ result = self.new(:get, uri, credentials)
16
+ result.perform {|r| yield r if block_given? }
17
+ end
18
+
19
+ def self.post(uri, credentials = nil)
20
+ result = self.new(:post, uri, credentials)
21
+ result.perform {|r| yield r if block_given? }
22
+ end
23
+
24
+ def initialize(method, uri, credentials = nil)
25
+ self.method = method
26
+ self.uri = uri
27
+ self.credentials = credentials
28
+ yield self if block_given?
29
+ end
30
+
31
+ def perform
32
+ http_opts = {use_ssl: true}
33
+ response = Net::HTTP.start(uri.host, uri.port, http_opts) do |http|
34
+ add_field "Authorization", "Bearer #{credentials.access_token}" if credentials
35
+ add_field "Accept", "application/json"
36
+ yield self if block_given?
37
+ http.request request
38
+ end
39
+ if (200..299).include?(response.code.to_i)
40
+ return response
41
+ else
42
+ raise RequestError.new(response.code.to_i), "Error #{response.code} (#{response.msg}) - #{uri}\n#{response.body}"
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def request
49
+ @request ||=
50
+ begin
51
+ case method
52
+ when :get
53
+ Net::HTTP::Get.new uri.request_uri
54
+ when :post
55
+ Net::HTTP::Post.new uri.request_uri
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ class RequestError < StandardError
62
+ attr_accessor :code
63
+ def initialize(code)
64
+ self.code = code
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -0,0 +1,67 @@
1
+ require 'openssl'
2
+ require 'securerandom'
3
+
4
+ module EphemeralCalc
5
+ class KeyPair
6
+ attr_reader :private_key
7
+
8
+ def initialize(private_key = nil)
9
+ self.private_key = private_key || KeyPair.generate_private_key
10
+ end
11
+
12
+ def private_key=(new_key)
13
+ @private_key = convert_key(new_key)
14
+ end
15
+
16
+ def public_key
17
+ @public_key ||= begin
18
+ Curve25519.mult(self.private_key, Curve25519::BASEPOINT)
19
+ end
20
+ end
21
+
22
+ def shared_secret(other_public_key)
23
+ Curve25519.mult(self.private_key, other_public_key)
24
+ end
25
+
26
+ # opts must contain the key :resolver_public_key or :beacon_public_key
27
+ def identity_key(opts)
28
+ if opts[:resolver_public_key]
29
+ resolver_public_key = convert_key(opts[:resolver_public_key])
30
+ beacon_public_key = self.public_key
31
+ secret = shared_secret(resolver_public_key)
32
+ elsif opts[:beacon_public_key]
33
+ resolver_public_key = self.public_key
34
+ beacon_public_key = convert_key(opts[:beacon_public_key])
35
+ secret = shared_secret(beacon_public_key)
36
+ else
37
+ raise ArgumentError, "Must pass a resolver_public_key or a beacon_public_key"
38
+ end
39
+ salt = resolver_public_key + beacon_public_key
40
+ hkdf(secret, salt)[0..15]
41
+ end
42
+
43
+ def convert_key(key_string)
44
+ if key_string.size == 64
45
+ [key_string].pack("H*")
46
+ else
47
+ key_string
48
+ end
49
+ end
50
+
51
+ def self.generate_private_key
52
+ # reference: https://code.google.com/archive/p/curve25519-donna/
53
+ # See section on "generating a private key"
54
+ key = SecureRandom.random_bytes(32).bytes
55
+ key[0] &= 248
56
+ key[31] &= 127
57
+ key[31] |= 64
58
+ return key.pack("C*")
59
+ end
60
+
61
+ def hkdf(secret, salt)
62
+ digest = OpenSSL::Digest.new("SHA256")
63
+ prk = OpenSSL::HMAC.digest(digest, salt, secret)
64
+ OpenSSL::HMAC.digest(digest, prk, "\x01")
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,3 @@
1
+ module EphemeralCalc
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,12 @@
1
+ require "ephemeral_calc/version"
2
+ require "ephemeral_calc/curve25519"
3
+ require "ephemeral_calc/encryptor"
4
+ require "ephemeral_calc/key_pair"
5
+ require "ephemeral_calc/google_api/oauth"
6
+ require "ephemeral_calc/google_api/credentials"
7
+ require "ephemeral_calc/google_api/request"
8
+ require "ephemeral_calc/google_api/client"
9
+
10
+ module EphemeralCalc
11
+ # Your code goes here...
12
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ephemeral_calc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Radius Networks
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-08-12 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.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake-compiler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Tools to calculate Eddystone ephemeral identifiers
84
+ email:
85
+ - support@radiusnetworks.com
86
+ executables:
87
+ - ephemeral_calc
88
+ extensions:
89
+ - ext/curve25519/extconf.rb
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rspec"
94
+ - ".travis.yml"
95
+ - CODE_OF_CONDUCT.md
96
+ - Gemfile
97
+ - README.md
98
+ - Rakefile
99
+ - bin/console
100
+ - bin/rspec
101
+ - bin/setup
102
+ - ephemeral_calc.gemspec
103
+ - exe/ephemeral_calc
104
+ - ext/curve25519/curve25519-donna.c
105
+ - ext/curve25519/curve25519_module.c
106
+ - ext/curve25519/extconf.rb
107
+ - lib/ephemeral_calc.rb
108
+ - lib/ephemeral_calc/encryptor.rb
109
+ - lib/ephemeral_calc/google_api/client.rb
110
+ - lib/ephemeral_calc/google_api/credentials.rb
111
+ - lib/ephemeral_calc/google_api/oauth.rb
112
+ - lib/ephemeral_calc/google_api/request.rb
113
+ - lib/ephemeral_calc/key_pair.rb
114
+ - lib/ephemeral_calc/version.rb
115
+ homepage: https://github.com/RadiusNetworks/ephemeral_calc
116
+ licenses: []
117
+ metadata: {}
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubyforge_project:
134
+ rubygems_version: 2.4.6
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Tools to calculate Eddystone ephemeral identifiers
138
+ test_files: []