selfsdk 0.0.216 → 0.0.218

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b598cf272958bc5ccdeb6e1ec1acfc4fff973b7989069654249f13cac7f7422
4
- data.tar.gz: 3d08894ddb644c19bdb28b905569a64a84988980d5ab964008eba5b7e4479feb
3
+ metadata.gz: 6bff9337b676123f000fdea33206ecb138fff8656f2d77e182212334228d996e
4
+ data.tar.gz: e543e7a2040a6fcc218c4e187fd8304de0189e272e95926a35e438cfd13fe5e9
5
5
  SHA512:
6
- metadata.gz: c24d8ed0c7f7f8fa97c061f39414e5634a09a5bdb65697fa5224086d86b044fa23789fd4f5b458072339eddd0aa2fe4bb3e474f853b1bc6d5526f43e247879b4
7
- data.tar.gz: e803be8107ed18bb0f7750948a786dd303dbd478f3979a83fa870f55dc6bed243e89458d57ffd19f92c876c69629109d1af9b432dec8a6a23f7c0f4542b2be46
6
+ metadata.gz: 13edf88e46cc57be0503755f7b54cf080e261bad472ff2e8820277bdaea0ed1775f88f5fe46f407ddbf4df0d3b1f566c734e4d19a55cfb0a0ba4f6bc07d88cc3
7
+ data.tar.gz: e25a198896eca2e236c96405b143343323a35638b78b680ee8e96abdf426b10838d1c40786460decf44f856f03802c6e77857c10062fd1c62679a6bb288d95e7
data/lib/client.rb CHANGED
@@ -99,7 +99,7 @@ module SelfSDK
99
99
  next if body[:error_id] == 'token_expired'
100
100
 
101
101
  break
102
- rescue StandardError => e
102
+ rescue StandardError
103
103
  # retry if the server is down
104
104
  end
105
105
  sleep 2
data/lib/crypto.rb CHANGED
@@ -1,37 +1,43 @@
1
1
  # Copyright 2020 Self Group Ltd. All Rights Reserved.
2
2
 
3
3
  require 'self_crypto'
4
+ require_relative 'storage'
4
5
 
5
6
  module SelfSDK
6
7
  class Crypto
7
- def initialize(client, device, storage_folder, storage_key)
8
+ def initialize(client, device, storage, storage_key)
8
9
  @client = client
9
10
  @device = device
10
11
  @storage_key = storage_key
11
- @storage_folder = "#{storage_folder}/#{@client.jwt.key_id}"
12
12
  @lock_strategy = true
13
13
  @mode = "r+"
14
+ @storage = storage
15
+ @keys = {}
16
+ @mutex = Mutex.new
17
+
18
+ @storage.tx do
19
+ ::SelfSDK.logger.debug('Checking if account exists...')
20
+ if @storage.account_exists?
21
+ # 1a) if alice's account file exists load the pickle from the file
22
+ @account = SelfCrypto::Account.from_pickle(@storage.account_olm, @storage_key)
23
+ else
24
+ # 1b-i) if create a new account for alice if one doesn't exist already
25
+ @account = SelfCrypto::Account.from_seed(@client.jwt.key)
14
26
 
15
- if File.exist?(account_path)
16
- # 1a) if alice's account file exists load the pickle from the file
17
- @account = SelfCrypto::Account.from_pickle(File.read(account_path), @storage_key)
18
- else
19
- # 1b-i) if create a new account for alice if one doesn't exist already
20
- @account = SelfCrypto::Account.from_seed(@client.jwt.key)
21
-
22
- # 1b-ii) generate some keys for alice and publish them
23
- @account.gen_otk(100)
27
+ # 1b-ii) generate some keys for alice and publish them
28
+ @account.gen_otk(100)
24
29
 
25
- # 1b-iii) convert those keys to json
26
- keys = @account.otk['curve25519'].map{|k,v| {id: k, key: v}}.to_json
30
+ # 1b-iii) convert those keys to json
31
+ @keys = @account.otk['curve25519'].map{|k,v| {id: k, key: v}}
32
+ keys = @keys.to_json
27
33
 
28
- # 1b-iv) post those keys to POST /v1/identities/<selfid>/devices/1/pre_keys/
29
- res = @client.post("/v1/apps/#{@client.jwt.id}/devices/#{@device}/pre_keys", keys)
30
- raise 'unable to push prekeys, please try in a few minutes' if res.code != 200
34
+ # 1b-iv) post those keys to POST /v1/identities/<selfid>/devices/1/pre_keys/
35
+ res = @client.post("/v1/apps/#{@client.jwt.id}/devices/#{@device}/pre_keys", keys)
36
+ raise 'unable to push prekeys, please try in a few minutes' if res.code != 200
31
37
 
32
- # 1b-v) store the account to a file
33
- FileUtils.mkdir_p(@storage_folder)
34
- File.write(account_path, @account.to_pickle(storage_key))
38
+ # 1b-v) store the account to a file
39
+ @storage.account_create(@account.to_pickle(@storage_key))
40
+ end
35
41
  end
36
42
  end
37
43
 
@@ -43,119 +49,79 @@ module SelfSDK
43
49
  gs = SelfCrypto::GroupSession.new("#{@client.jwt.id}:#{@device}")
44
50
 
45
51
  sessions = {}
46
- locks = {}
47
- ::SelfSDK.logger.debug('- [crypto] managing sessions with all recipients')
48
-
49
- recipients.each do |r|
50
- f = nil
51
- next if r[:id] == @client.jwt.id && r[:device_id] == @device
52
-
53
- session_file_name = session_path(r[:id], r[:device_id])
54
- session_with_bob = nil
55
-
56
- begin
57
- if File.exist?(session_file_name)
58
- # Lock the session file
59
- locks[session_file_name] = File.open(session_file_name, @mode)
60
- locks[session_file_name].flock(File::LOCK_EX)
61
- end
62
- session_with_bob = get_outbound_session_with_bob(locks[session_file_name], r[:id], r[:device_id])
52
+ ct = ""
53
+ tx do
54
+ gs = SelfCrypto::GroupSession.new("#{@client.jwt.id}:#{@device}")
55
+ recipients.each do |r|
56
+ sid = @storage.sid(r[:id], r[:device_id])
57
+ next if sid == @storage.app_id
58
+
59
+ session_with_bob = get_outbound_session_with_bob(@storage.session_get_olm(sid), r[:id], r[:device_id])
60
+ ::SelfSDK.logger.debug("- [crypto] adding group participant #{r[:id]}:#{r[:device_id]}")
61
+ gs.add_participant("#{r[:id]}:#{r[:device_id]}", session_with_bob)
62
+ sessions[sid] = session_with_bob
63
63
  rescue => e
64
64
  ::SelfSDK.logger.warn("- [crypto] there is a problem adding group participant #{r[:id]}:#{r[:device_id]}, skipping...")
65
65
  ::SelfSDK.logger.warn("- [crypto] #{e}")
66
66
  next
67
67
  end
68
68
 
69
- ::SelfSDK.logger.debug("- [crypto] adding group participant #{r[:id]}:#{r[:device_id]}")
70
- gs.add_participant("#{r[:id]}:#{r[:device_id]}", session_with_bob)
71
- sessions[session_file_name] = session_with_bob
72
- end
73
-
74
- # 5) encrypt a message
75
- ::SelfSDK.logger.debug("- [crypto] group encrypting message")
76
- ct = gs.encrypt(message).to_s
69
+ # 5) encrypt a message
70
+ ::SelfSDK.logger.debug("- [crypto] group encrypting message")
71
+ ct = gs.encrypt(message).to_s
77
72
 
78
- # 6) store the session to a file
79
- ::SelfSDK.logger.debug("- [crypto] storing sessions")
80
- sessions.each do |session_file_name, session_with_bob|
81
- pickle = session_with_bob.to_pickle(@storage_key)
82
- if locks[session_file_name]
83
- locks[session_file_name].rewind
84
- locks[session_file_name].write(pickle)
85
- locks[session_file_name].truncate(locks[session_file_name].pos)
86
- else
87
- File.write(session_file_name, pickle)
73
+ # 6) store the session to a file
74
+ ::SelfSDK.logger.debug("- [crypto] storing sessions")
75
+ sessions.each do |sid, session_with_bob|
76
+ pickle = session_with_bob.to_pickle(@storage_key)
77
+ @storage.session_update(sid, pickle)
88
78
  end
89
79
  end
90
-
91
80
  ct
92
- ensure
93
- locks.each do |session_file_name, lock|
94
- # Unlock the file
95
- if lock
96
- lock.flock(File::LOCK_UN)
97
- end
98
- end
99
81
  end
100
82
 
101
- def decrypt(message, sender, sender_device)
102
- f = nil
103
- ::SelfSDK.logger.debug("- [crypto] decrypting a message")
104
- session_file_name = session_path(sender, sender_device)
83
+ def decrypt(message, sender, sender_device, offset)
84
+ ::SelfSDK.logger.debug("- [crypto] decrypting a message #{message}")
85
+ sid = @storage.sid(sender, sender_device)
105
86
 
106
- if File.exist?(session_file_name)
107
- # Lock the session file
108
- f = File.open(session_file_name, @mode)
109
- f.flock(File::LOCK_EX)
110
- end
111
-
112
- ::SelfSDK.logger.debug("- [crypto] loading sessions")
113
- session_with_bob = get_inbound_session_with_bob(f, message)
87
+ pt = ""
88
+ tx do
89
+ ::SelfSDK.logger.debug("- [crypto] loading sessions for #{sid}")
90
+ ::SelfSDK.logger.debug("- [crypto] #{@account.one_time_keys["curve25519"]}")
91
+ session_with_bob = get_inbound_session_with_bob(@storage.session_get_olm(sid), message)
114
92
 
115
- # 8) create a group session and set the identity of the account you're using
116
- ::SelfSDK.logger.debug("- [crypto] create a group session and set the identity of the account #{@client.jwt.id}:#{@device}")
117
- gs = SelfCrypto::GroupSession.new("#{@client.jwt.id}:#{@device}")
93
+ # 8) create a group session and set the identity of the account you're using
94
+ ::SelfSDK.logger.debug("- [crypto] create a group session and set the identity of the account #{@client.jwt.id}:#{@device}")
95
+ gs = SelfCrypto::GroupSession.new("#{@client.jwt.id}:#{@device}")
118
96
 
119
- # 9) add all recipients and their sessions
120
- ::SelfSDK.logger.debug("- [crypto] add all recipients and their sessions #{@sender}:#{@sender_device}")
121
- gs.add_participant("#{sender}:#{sender_device}", session_with_bob)
97
+ # 9) add all recipients and their sessions
98
+ ::SelfSDK.logger.debug("- [crypto] add all recipients and their sessions #{sender}:#{sender_device}")
99
+ gs.add_participant("#{sender}:#{sender_device}", session_with_bob)
122
100
 
123
- # 10) decrypt the message ciphertext
124
- ::SelfSDK.logger.debug("- [crypto] decrypt the message ciphertext")
125
- pt = gs.decrypt("#{sender}:#{sender_device}", message).to_s
101
+ # 10) decrypt the message ciphertext
102
+ ::SelfSDK.logger.debug("- [crypto] decrypt the message ciphertext")
103
+ pt = gs.decrypt("#{sender}:#{sender_device}", message).to_s
126
104
 
127
- # 11) store the session to a file
128
- ::SelfSDK.logger.debug("- [crypto] store the session to a file")
105
+ # 11) store the session to a file
106
+ ::SelfSDK.logger.debug("- [crypto] store the session to a file")
129
107
 
130
- pickle = session_with_bob.to_pickle(@storage_key)
131
- if !f.nil?
132
- f.rewind
133
- f.write(pickle)
134
- f.truncate(f.pos)
135
- else
136
- File.write(session_file_name, pickle)
108
+ pickle = session_with_bob.to_pickle(@storage_key)
109
+ @storage.session_update(sid, pickle)
110
+ @storage.account_set_offset(offset)
137
111
  end
138
-
139
112
  pt
140
- ensure
141
- # Unlock the session file
142
- f&.flock(File::LOCK_UN)
143
- f&.close
113
+ rescue => e
114
+ ::SelfSDK.logger.debug("- [crypto] ERROR DECRYPTING: original message: #{message}")
115
+ ::SelfSDK.logger.debug("- [crypto] ERROR DECRYPTING: exception message: #{e.message}")
116
+ ::SelfSDK.logger.debug("- [crypto] ERROR DECRYPTING: exception backtrace: #{e.backtrace}")
117
+ @storage.account_set_offset(offset)
118
+ pt
144
119
  end
145
120
 
146
121
  private
147
122
 
148
- def account_path
149
- "#{@storage_folder}/account.pickle"
150
- end
151
-
152
- def session_path(selfid, device)
153
- "#{@storage_folder}/#{selfid}:#{device}-session.pickle"
154
- end
155
-
156
- def get_outbound_session_with_bob(f, recipient, recipient_device)
157
- if !f.nil?
158
- pickle = f.read
123
+ def get_outbound_session_with_bob(pickle, recipient, recipient_device)
124
+ if !pickle.nil?
159
125
  # 2a) if bob's session file exists load the pickle from the file
160
126
  session_with_bob = SelfCrypto::Session.from_pickle(pickle, @storage_key)
161
127
  else
@@ -184,18 +150,24 @@ module SelfSDK
184
150
  session_with_bob
185
151
  end
186
152
 
187
- def get_inbound_session_with_bob(f, message)
188
- if !f.nil?
189
- pickle = f.read
153
+ def get_inbound_session_with_bob(pickle, message)
154
+ if !pickle.nil?
190
155
  # 7a) if carol's session file exists load the pickle from the file
191
156
  session_with_bob = SelfCrypto::Session.from_pickle(pickle, @storage_key)
192
157
  end
193
158
 
159
+ ::SelfSDK.logger.debug("- [crypto] pickle nil? #{pickle.nil?}")
160
+ ::SelfSDK.logger.debug("- [crypto] getting one time message for #{@client.jwt.id}:#{@device}")
161
+
194
162
  # 7b-i) if you have not previously sent or received a message to/from bob,
195
163
  # you should extract the initial message from the group message intended
196
164
  # for your account id.
197
165
  m = SelfCrypto::GroupMessage.new(message.to_s).get_message("#{@client.jwt.id}:#{@device}")
198
166
 
167
+ ::SelfSDK.logger.debug("- [crypto] one time message #{m}")
168
+ ::SelfSDK.logger.debug("- [crypto] session is nil? #{session_with_bob.nil?}")
169
+ ::SelfSDK.logger.debug("- [crypto] is prekey message? #{m.instance_of?(SelfCrypto::PreKeyMessage)}")
170
+
199
171
  # if there is no session, create one
200
172
  # if there is an existing session and we are sent a one time key message, check
201
173
  # if it belongs to this current session and create a new inbound session if it doesn't
@@ -204,6 +176,7 @@ module SelfSDK
204
176
  session_with_bob = @account.inbound_session(m)
205
177
 
206
178
  # 7b-iii) remove the session's prekey from the account
179
+ ::SelfSDK.logger.debug "- [crypto] removing one time keys for bob #{session_with_bob}"
207
180
  @account.remove_one_time_keys(session_with_bob)
208
181
 
209
182
  current_one_time_keys = @account.otk['curve25519']
@@ -221,12 +194,20 @@ module SelfSDK
221
194
  res = @client.post("/v1/apps/#{@client.jwt.id}/devices/#{@device}/pre_keys", keys.to_json)
222
195
  raise 'unable to push prekeys, please try in a few minutes' if res.code != 200
223
196
  end
224
-
225
- File.write(account_path, @account.to_pickle(@storage_key))
197
+ ::SelfSDK.logger.debug "- [crypto] updating account with removed keys"
198
+ @storage.account_update(@account.to_pickle(@storage_key))
226
199
  end
227
200
 
228
201
  session_with_bob
229
202
  end
230
203
 
204
+ def tx
205
+ @mutex.lock
206
+ @storage.tx do
207
+ yield
208
+ end
209
+ ensure
210
+ @mutex.unlock
211
+ end
231
212
  end
232
213
  end
@@ -31,7 +31,7 @@ module SelfSDK
31
31
  else
32
32
  envelope = input
33
33
  issuer = input.sender.split(":")
34
- messaging.encryption_client.decrypt(input.ciphertext, issuer.first, issuer.last)
34
+ messaging.encryption_client.decrypt(input.ciphertext, issuer.first, issuer.last, input.offset)
35
35
  end
36
36
 
37
37
  jwt = JSON.parse(body, symbolize_names: true)
data/lib/messaging.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'messages/message'
13
13
  module SelfSDK
14
14
  class WebsocketClient
15
15
  ON_DEMAND_CLOSE_CODE=3999
16
+ CONNECTION_SUPERCEDED=1011
16
17
 
17
18
  attr_accessor :ws
18
19
 
@@ -42,7 +43,7 @@ module SelfSDK
42
43
  @ws.on :close do |event|
43
44
  SelfSDK.logger.debug "connection closed detected : #{event.code} #{event.reason}"
44
45
 
45
- if event.code != ON_DEMAND_CLOSE_CODE
46
+ if not [ON_DEMAND_CLOSE_CODE, CONNECTION_SUPERCEDED].include? event.code
46
47
  raise StandardError('websocket connection closed') if !@auto_reconnect
47
48
 
48
49
  if !@reconnection_delay.nil?
@@ -59,6 +60,7 @@ module SelfSDK
59
60
 
60
61
  # Sends a closing message to the websocket client.
61
62
  def close
63
+ SelfSDK.logger.debug "connection closed by the client"
62
64
  @ws.close(ON_DEMAND_CLOSE_CODE, "connection closed by the client")
63
65
  end
64
66
 
@@ -80,7 +82,6 @@ module SelfSDK
80
82
  class MessagingClient
81
83
  DEFAULT_DEVICE="1"
82
84
  DEFAULT_AUTO_RECONNECT=true
83
- DEFAULT_STORAGE_DIR="./.self_storage"
84
85
 
85
86
  PRIORITIES = {
86
87
  'chat.invite': SelfSDK::Messages::PRIORITY_VISIBLE,
@@ -104,12 +105,12 @@ module SelfSDK
104
105
  #
105
106
  # @param url [string] self-messaging url
106
107
  # @params client [Object] SelfSDK::Client object
107
- # @option opts [string] :storage_dir the folder where encryption sessions and settings will be stored
108
+ # @option opts [string] :storage the library used to persist session data.
108
109
  # @params storage_key [String] seed to encrypt messaging
109
110
  # @params storage_folder [String] folder to perist messaging encryption
110
111
  # @option opts [Bool] :auto_reconnect Automatically reconnects to websocket if connection is lost (defaults to true).
111
112
  # @option opts [String] :device_id The device id to be used by the app defaults to "1".
112
- def initialize(url, client, storage_key, options = {})
113
+ def initialize(url, client, storage_key, storage, options = {})
113
114
  @mon = Monitor.new
114
115
  @messages = {}
115
116
  @acks = {}
@@ -121,18 +122,13 @@ module SelfSDK
121
122
  @timeout = 120 # seconds
122
123
  @auth_id = SecureRandom.uuid
123
124
  @device_id = options.fetch(:device_id, DEFAULT_DEVICE)
124
- @raw_storage_dir = options.fetch(:storage_dir, DEFAULT_STORAGE_DIR)
125
- @storage_dir = "#{@raw_storage_dir}/apps/#{@jwt.id}/devices/#{@device_id}"
126
- FileUtils.mkdir_p @storage_dir unless File.exist? @storage_dir
127
- @offset_file = "#{@storage_dir}/#{@jwt.id}:#{@device_id}.offset"
128
- @offset = read_offset
129
125
  @source = SelfSDK::Sources.new("#{__dir__}/sources.json")
126
+ @storage = storage
130
127
 
131
128
  unless options.include? :no_crypto
132
- crypto_path = "#{@storage_dir}/keys"
133
- FileUtils.mkdir_p crypto_path unless File.exist? crypto_path
134
- @encryption_client = Crypto.new(@client, @device_id, crypto_path, storage_key)
129
+ @encryption_client = Crypto.new(@client, @device_id, @storage, storage_key)
135
130
  end
131
+ @offset = @storage.account_offset
136
132
 
137
133
  @ws = if options.include? :ws
138
134
  options[:ws]
@@ -246,17 +242,6 @@ module SelfSDK
246
242
  end
247
243
  end
248
244
 
249
- # Sends a command to list ACL rules.
250
- def list_acl_rules
251
- wait_for 'acl_list' do
252
- a = SelfMsg::Acl.new
253
- a.id = SecureRandom.uuid
254
- a.command = SelfMsg::AclCommandLIST
255
-
256
- @ws.send a
257
- end
258
- end
259
-
260
245
  # Sends a message and waits for the response
261
246
  #
262
247
  # @params msg [SelfMsg::Message] message object to be sent
@@ -363,7 +348,7 @@ module SelfSDK
363
348
 
364
349
  SelfSDK.logger.debug " - notifying by type"
365
350
  SelfSDK.logger.debug " - #{message.typ}"
366
- SelfSDK.logger.debug " - #{message}"
351
+ SelfSDK.logger.debug " - #{message.payload}"
367
352
  SelfSDK.logger.debug " - #{@type_observer.keys.join(',')}"
368
353
 
369
354
  # Return if there is no observer setup for this kind of message
@@ -382,12 +367,12 @@ module SelfSDK
382
367
 
383
368
  def subscribe(type, &block)
384
369
  type = @source.message_type(type) if type.is_a? Symbol
370
+ SelfSDK.logger.debug "Subscribing to messages by type: #{type}"
385
371
  @type_observer[type] = { block: block }
386
372
  end
387
373
 
388
374
  private
389
375
 
390
-
391
376
  # Cleans expired messages
392
377
  def clean_timeouts
393
378
  clean_observers
@@ -413,7 +398,6 @@ module SelfSDK
413
398
  @ws.start
414
399
  end
415
400
 
416
-
417
401
  # Process an event when it arrives through the websocket connection.
418
402
  def on_message(event)
419
403
  data = event.data.pack('c*')
@@ -438,29 +422,13 @@ module SelfSDK
438
422
  @messages[hdr.id][:response] = {error: e.error}
439
423
  mark_as_acknowledged(hdr.id)
440
424
  mark_as_arrived(hdr.id)
441
- when SelfMsg::MsgTypeACL
442
- SelfSDK.logger.debug "#{hdr.id} ACL received"
443
- a = SelfMsg::Acl.new(data: data)
444
- process_incomming_acl a
445
425
  end
446
426
  rescue TypeError
447
427
  SelfSDK.logger.debug "invalid array message"
448
428
  end
449
429
 
450
- def process_incomming_acl(input)
451
- list = JSON.parse(input.payload)
452
-
453
- @messages['acl_list'][:response] = list
454
- mark_as_arrived 'acl_list'
455
- rescue StandardError => e
456
- p "Error processing incoming ACL #{input.id} #{input.payload}"
457
- SelfSDK.logger.debug e
458
- SelfSDK.logger.debug e.backtrace
459
- nil
460
- end
461
-
462
430
  def process_incomming_message(input)
463
- message = parse_and_write_offset(input)
431
+ message = parse(input)
464
432
 
465
433
  if @messages.include? message.id
466
434
  message.validate! @messages[message.id][:original_message]
@@ -479,21 +447,17 @@ module SelfSDK
479
447
  nil
480
448
  end
481
449
 
482
- def parse_and_write_offset(input)
450
+ def parse(input)
483
451
  msg = SelfSDK::Messages.parse(input, self)
484
- write_offset(input.offset)
485
- # Avoid catching any other decryption errors.
486
452
  msg
487
- rescue SelfSDK::Messages::UnmappedMessage => e
488
- # this is an ummapped message, let's ignore it but write the offset.
489
- write_offset(input.offset)
453
+ rescue SelfSDK::Messages::UnmappedMessage
490
454
  nil
491
455
  end
492
456
 
493
457
  # Authenticates current client on the websocket server.
494
458
  def authenticate
495
459
  @auth_id = SecureRandom.uuid if @auth_id.nil?
496
- @offset = read_offset
460
+ @offset = @storage.account_offset
497
461
 
498
462
  SelfSDK.logger.debug "authenticating with offset (#{@offset})"
499
463
 
@@ -529,23 +493,6 @@ module SelfSDK
529
493
  end
530
494
  end
531
495
 
532
- def read_offset
533
- return 0 unless File.exist? @offset_file
534
-
535
- File.open(@offset_file, 'rb') do |f|
536
- return f.read.to_i
537
- end
538
- end
539
-
540
- def write_offset(offset)
541
- File.open(@offset_file, 'wb') do |f|
542
- f.flock(File::LOCK_EX)
543
- f.write(offset.to_s.rjust(19, "0"))
544
- end
545
- SelfSDK.logger.debug "offset written #{offset}"
546
- @offset = offset
547
- end
548
-
549
496
  def select_priority(mtype)
550
497
  PRIORITIES[mtype] || SelfSDK::Messages::PRIORITY_VISIBLE
551
498
  end
data/lib/selfsdk.rb CHANGED
@@ -14,7 +14,6 @@ require_relative 'client'
14
14
  require_relative 'messaging'
15
15
  require_relative 'ntptime'
16
16
  require_relative 'authenticated'
17
- require_relative 'acl'
18
17
  require_relative 'sources'
19
18
  require_relative 'services/auth'
20
19
  require_relative 'services/requester'
@@ -50,6 +49,7 @@ module SelfSDK
50
49
  # @option opts [String] :messaging_url The messaging self provider url.
51
50
  # @option opts [Bool] :auto_reconnect Automatically reconnects to websocket if connection is lost (defaults to true).
52
51
  # @option opts [Symbol] :env The environment to be used, defaults to ":production".
52
+ # @option opts [String] :storage the library used to persist session data.
53
53
  def initialize(app_id, app_key, storage_key, storage_dir, opts = {})
54
54
  app_key = cleanup_key(app_key)
55
55
 
@@ -61,9 +61,12 @@ module SelfSDK
61
61
  messaging_url = messaging_url(opts)
62
62
  @started = false
63
63
  unless messaging_url.nil?
64
+ device_id = opts.fetch(:device_id, MessagingClient::DEFAULT_DEVICE)
65
+ storage = opts.fetch(:storage, SelfSDK::Storage.new(@client.jwt.id, device_id, storage_dir, storage_key))
64
66
  @messaging_client = MessagingClient.new(messaging_url,
65
67
  @client,
66
68
  storage_key,
69
+ storage,
67
70
  storage_dir: storage_dir,
68
71
  auto_reconnect: opts.fetch(:auto_reconnect, MessagingClient::DEFAULT_AUTO_RECONNECT),
69
72
  device_id: opts.fetch(:device_id, MessagingClient::DEFAULT_DEVICE))
@@ -32,37 +32,6 @@ module SelfSDK
32
32
  @client.subscribe(type, &block)
33
33
  end
34
34
 
35
- # Permits incoming messages from the a identity.
36
- #
37
- # @param [String] selfid to be allowed.
38
- # @return [Boolean] success / failure
39
- def permit_connection(selfid)
40
- acl.allow selfid
41
- end
42
-
43
- # Lists app allowed connections.
44
- # @return [Array] array of self ids allowed to connect to your app.
45
- def allowed_connections
46
- acl.list
47
- end
48
-
49
- # Checks if you're permitting messages from a specific self identifier
50
- # @return [Boolean] yes|no
51
- def is_permitted?(id)
52
- conns = allowed_connections
53
- return true if conns.include? "*"
54
- return true if conns.include? id
55
- return false
56
- end
57
-
58
- # Revokes incoming messages from the given identity.
59
- #
60
- # @param [String] selfid to be denied
61
- # @return [Boolean] success / failure
62
- def revoke_connection(selfid)
63
- acl.deny selfid
64
- end
65
-
66
35
  # Gets the device id for the authenticated app.
67
36
  #
68
37
  # @return [String] device_id of the running app.
@@ -101,10 +70,6 @@ module SelfSDK
101
70
  end
102
71
 
103
72
  private
104
-
105
- def acl
106
- @acl ||= ACL.new(@client)
107
- end
108
73
  end
109
74
  end
110
75
  end
@@ -48,9 +48,6 @@ module SelfSDK
48
48
  def request(selfid, facts, opts = {}, &block)
49
49
  SelfSDK.logger.info "authenticating #{selfid}"
50
50
  rq = opts.fetch(:request, true)
51
- if rq
52
- raise "You're not permitting connections from #{selfid}" unless @messaging_service.is_permitted?(selfid)
53
- end
54
51
 
55
52
  req = SelfSDK::Messages::FactRequest.new(@messaging)
56
53
  req.populate(selfid, prepare_facts(facts), opts)
data/lib/storage.rb ADDED
@@ -0,0 +1,236 @@
1
+ # Copyright 2020 Self Group Ltd. All Rights Reserved.
2
+ require 'sqlite3'
3
+
4
+ module SelfSDK
5
+ class Storage
6
+ attr_accessor :app_id
7
+
8
+ def initialize(app_id, app_device, storage_folder, _key_id)
9
+ @app_id = sid(app_id, app_device)
10
+
11
+ # Create the storage folder if it does not exist
12
+ create_directory_skel("#{storage_folder}/identities/")
13
+
14
+ # Create the database
15
+ @db = SQLite3::Database.new(File.join("#{storage_folder}/identities/", 'self.db'))
16
+ set_pragmas
17
+ create_accounts_table
18
+ create_sessions_table
19
+ m = StorageMigrator.new(@db, "#{storage_folder}/apps", @app_id)
20
+ m.migrate
21
+ end
22
+
23
+ def tx
24
+ @db.transaction
25
+ yield
26
+ @db.commit
27
+ rescue SQLite3::Exception => e
28
+ @db.rollback
29
+ rescue => e
30
+ @db.rollback
31
+ raise e
32
+ end
33
+
34
+ def account_exists?
35
+ row = @db.execute("SELECT olm_account FROM accounts WHERE as_identifier = ?", [@app_id.encode("UTF-8")]).first
36
+ !row.nil?
37
+ end
38
+
39
+ def account_create(olm)
40
+ @db.execute("INSERT INTO accounts (as_identifier, offset, olm_account) VALUES (?, ?, ?);", [ @app_id.encode("UTF-8"), 0, olm.encode("UTF-8") ])
41
+ rescue
42
+ end
43
+
44
+ def account_update(olm)
45
+ @db.execute("UPDATE accounts SET olm_account = ? WHERE as_identifier = ?", [ olm.encode("UTF-8"), @app_id.encode("UTF-8") ])
46
+ end
47
+
48
+ def account_olm
49
+ row = @db.execute("SELECT olm_account FROM accounts WHERE as_identifier = ?;", [@app_id.encode("UTF-8")]).first
50
+ return nil unless row
51
+
52
+ row.first
53
+ end
54
+
55
+ def account_offset
56
+ row = @db.execute("SELECT offset FROM accounts WHERE as_identifier = ?;", @app_id.encode("UTF-8")).first
57
+ return nil unless row
58
+
59
+ row.first
60
+ end
61
+
62
+ def account_set_offset(offset)
63
+ @db.execute("UPDATE accounts SET offset = ? WHERE as_identifier = ?;", [ offset, @app_id.encode("UTF-8") ])
64
+ end
65
+
66
+ def session_create(sid, olm)
67
+ @db.execute("INSERT INTO sessions (as_identifier, with_identifier, olm_session) VALUES (?, ?, ?);", [ @app_id.encode("UTF-8"), sid.encode("UTF-8"), olm.encode("UTF-8") ])
68
+ end
69
+
70
+ def session_update(sid, olm)
71
+ row = @db.execute("SELECT olm_session FROM sessions WHERE as_identifier = ? AND with_identifier = ?", [@app_id.encode("UTF-8"), sid.encode("UTF-8")]).first
72
+ if row.nil?
73
+ session_create(sid, olm)
74
+ else
75
+ @db.execute("UPDATE sessions SET olm_session = ? WHERE as_identifier = ? AND with_identifier = ?;", [ olm.encode("UTF-8"), @app_id.encode("UTF-8"), sid.encode("UTF-8") ])
76
+ end
77
+ end
78
+
79
+ def session_get_olm(sid)
80
+ row = @db.execute("SELECT olm_session FROM sessions WHERE as_identifier = ? AND with_identifier = ?", [@app_id.encode("UTF-8"), sid.encode("UTF-8")]).first
81
+ return nil if row.nil?
82
+
83
+ row.first
84
+ end
85
+
86
+ def sid(selfid, device)
87
+ "#{selfid}:#{device}"
88
+ end
89
+
90
+ private
91
+
92
+ # Create a folder if it does not exist
93
+ def create_directory_skel(storage_folder)
94
+ FileUtils.mkdir_p storage_folder unless File.exist? storage_folder
95
+ rescue Errno::ENOENT
96
+ raise ERR_INVALID_DIRECTORY
97
+ rescue
98
+ end
99
+
100
+ def set_pragmas
101
+ pragma_statement = <<~SQL
102
+ PRAGMA synchronous = NORMAL;
103
+ PRAGMA journal_mode = WAL;
104
+ PRAGMA temp_store = MEMORY;
105
+ SQL
106
+
107
+ @db.execute_batch(pragma_statement)
108
+ rescue SQLite3::Exception => e
109
+ puts "Exception occurred: #{e}"
110
+ end
111
+
112
+ def create_accounts_table
113
+ accounts_table_statement = <<~SQL
114
+ CREATE TABLE IF NOT EXISTS accounts (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ as_identifier TEXT NOT NULL,
117
+ offset INTEGER NOT NULL,
118
+ olm_account BLOB NOT NULL
119
+ );
120
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_as_identifier
121
+ ON accounts (as_identifier);
122
+ SQL
123
+
124
+ @db.execute_batch(accounts_table_statement)
125
+ rescue SQLite3::Exception => e
126
+ puts "Exception occurred: #{e}"
127
+ end
128
+
129
+ def create_sessions_table
130
+ # TODO we could deduplicate as_identifier and with_identifier here
131
+ # by creating a record for each on a new identifier table,
132
+ # but this is only temporary
133
+ session_table_statement = <<~SQL
134
+ CREATE TABLE IF NOT EXISTS sessions (
135
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
136
+ as_identifier TEXT NOT NULL,
137
+ with_identifier TEXT NOT NULL,
138
+ olm_session BLOB NOT NULL
139
+ );
140
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_with_identifier
141
+ ON sessions (as_identifier, with_identifier);
142
+ SQL
143
+
144
+ @db.execute_batch(session_table_statement)
145
+ rescue SQLite3::Exception => e
146
+ puts "Exception occurred: #{e}"
147
+ end
148
+ end
149
+
150
+ class StorageMigrator
151
+ def initialize(db, storage_folder, app_id)
152
+ @db = db
153
+ # Old versions of the sdk using that same storage folder shouldn't be affected in any way
154
+
155
+ @base_path = "#{storage_folder}/#{app_id.split(':').first}"
156
+ @app_id = app_id
157
+ end
158
+
159
+ def migrate
160
+ return unless File.exist?(@base_path)
161
+
162
+ # Parse the account information.
163
+ accounts = parse_accounts
164
+
165
+ persist_accounts(accounts)
166
+
167
+ # Depreciate the base path.
168
+ File.rename("#{@base_path}", "#{@base_path}-depreciated")
169
+ end
170
+
171
+ private
172
+
173
+ def parse_accounts
174
+ accounts = {}
175
+
176
+ Dir.glob(File.join(@base_path, "**/*")).each do |path|
177
+ if File.directory?(path)
178
+ next
179
+ end
180
+
181
+ case File.extname(path)
182
+ when ".offset"
183
+ file_name = File.basename(path, ".offset")
184
+ offset = File.read(path)[0, 19].to_i
185
+
186
+ accounts[file_name] = {} unless accounts.key? file_name
187
+ accounts[file_name][:offset] = offset
188
+ when ".pickle"
189
+ file_name = File.basename(path, ".pickle")
190
+ content = File.read(path)
191
+
192
+ accounts[@app_id] = {} unless accounts.key? @app_id
193
+ if file_name == "account"
194
+ accounts[@app_id][:account] = content
195
+ else
196
+ if accounts.key? @app_id
197
+ accounts[@app_id][:sessions] = [] unless accounts[@app_id].key? :sessions
198
+ accounts[@app_id][:sessions] << {
199
+ with: file_name.sub("-session", ""),
200
+ session: content
201
+ }
202
+ next
203
+ end
204
+ accounts[@app_id][:account] = content
205
+ end
206
+ end
207
+ end
208
+
209
+ accounts
210
+ end
211
+
212
+ def persist_accounts(accounts)
213
+ @db.transaction
214
+ accounts.each do |inbox_id, account|
215
+ @db.execute(
216
+ "INSERT INTO accounts (as_identifier, offset, olm_account) VALUES ($1, $2, $3)",
217
+ [inbox_id, account[:offset], account[:account]]
218
+ )
219
+
220
+ account[:sessions].each do |session|
221
+ @db.execute(
222
+ "INSERT INTO sessions (as_identifier, with_identifier, olm_session) VALUES ($1, $2, $3)",
223
+ [inbox_id, session[:with], session[:session]]
224
+ )
225
+ end
226
+ end
227
+
228
+ @db.commit
229
+ rescue SQLite3::Exception => e
230
+ puts "Exception occurred"
231
+ puts e
232
+ puts e.backtrace
233
+ @db.rollback
234
+ end
235
+ end
236
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: selfsdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.216
4
+ version: 0.0.218
5
5
  platform: ruby
6
6
  authors:
7
7
  - Self Group Ltd.
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: sqlite3
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :runtime
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  - !ruby/object:Gem::Dependency
182
196
  name: bundler
183
197
  requirement: !ruby/object:Gem::Requirement
@@ -350,7 +364,6 @@ executables: []
350
364
  extensions: []
351
365
  extra_rdoc_files: []
352
366
  files:
353
- - lib/acl.rb
354
367
  - lib/authenticated.rb
355
368
  - lib/chat/file_object.rb
356
369
  - lib/chat/group.rb
@@ -396,6 +409,7 @@ files:
396
409
  - lib/signature_graph.rb
397
410
  - lib/source_definition.rb
398
411
  - lib/sources.rb
412
+ - lib/storage.rb
399
413
  homepage: https://www.joinself.com/
400
414
  licenses:
401
415
  - MIT
data/lib/acl.rb DELETED
@@ -1,67 +0,0 @@
1
- # Copyright 2020 Self Group Ltd. All Rights Reserved.
2
-
3
- # frozen_string_literal: true
4
-
5
- require 'date'
6
-
7
- # Namespace for classes and modules that handle Self interactions.
8
- module SelfSDK
9
- # Access control list
10
- class ACL
11
- def initialize(messaging)
12
- @messaging = messaging
13
- @jwt = @messaging.jwt
14
- @acl_rules = []
15
- end
16
-
17
- # Lists allowed connections.
18
- def list
19
- SelfSDK.logger.info "Listing allowed connections"
20
- @acl_rules = @messaging.list_acl_rules if @acl_rules.empty?
21
- @acl_rules
22
- end
23
-
24
- # Allows incomming messages from the given identity.
25
- def allow(id)
26
- @acl_rules << id
27
- SelfSDK.logger.info "Allowing connections from #{id}"
28
- payload = @jwt.prepare(jti: SecureRandom.uuid,
29
- cid: SecureRandom.uuid,
30
- typ: 'acl.permit',
31
- iss: @jwt.id,
32
- sub: @jwt.id,
33
- iat: (SelfSDK::Time.now - 5).strftime('%FT%TZ'),
34
- exp: (SelfSDK::Time.now + 60).strftime('%FT%TZ'),
35
- acl_source: id,
36
- acl_exp: (SelfSDK::Time.now + 360_000).to_datetime.rfc3339)
37
-
38
- a = SelfMsg::Acl.new
39
- a.id = SecureRandom.uuid
40
- a.command = SelfMsg::AclCommandPERMIT
41
- a.payload = payload
42
-
43
- @messaging.send_message a
44
- end
45
-
46
- # Deny incomming messages from the given identity.
47
- def deny(id)
48
- @acl_rules.delete(id)
49
- SelfSDK.logger.info "Denying connections from #{id}"
50
- payload = @jwt.prepare(jti: SecureRandom.uuid,
51
- cid: SecureRandom.uuid,
52
- typ: 'acl.revoke',
53
- iss: @jwt.id,
54
- sub: @jwt.id,
55
- iat: (SelfSDK::Time.now - 5).strftime('%FT%TZ'),
56
- exp: (SelfSDK::Time.now + 60).strftime('%FT%TZ'),
57
- acl_source: id,
58
- acl_exp: (SelfSDK::Time.now + 360_000).to_datetime.rfc3339)
59
-
60
- a = SelfMsg::Acl.new
61
- a.id = SecureRandom.uuid
62
- a.command = SelfMsg::AclCommandREVOKE
63
- a.payload = payload
64
- @messaging.send_message a
65
- end
66
- end
67
- end