selfsdk 0.0.217 → 0.0.218

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b46d28c22f1f2bc5580d68f247bec53aac495765ea0199e59e21ecefd6f29e9
4
- data.tar.gz: b0b84f89bdda28f94c2a81e56c71c420c480fe3b69e9425ab393e85e92a0e199
3
+ metadata.gz: 6bff9337b676123f000fdea33206ecb138fff8656f2d77e182212334228d996e
4
+ data.tar.gz: e543e7a2040a6fcc218c4e187fd8304de0189e272e95926a35e438cfd13fe5e9
5
5
  SHA512:
6
- metadata.gz: 77173c08e398e75416d6711da44edc8a12062fae4dcf51e7b302dd9fab7fb8654af3904bd504aad7a980dd269fe6bd831453d005ec6352f7d60c0d08b754bb71
7
- data.tar.gz: c9c443f7570a352bcfd5c65250e7b6b7ca5b3c9bc0dd1017170186df0bf706aa227e74924a24427dfd350ff0f967e126fd55da1517e7e8a6c1d6541cd982a6b1
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]
@@ -352,7 +348,7 @@ module SelfSDK
352
348
 
353
349
  SelfSDK.logger.debug " - notifying by type"
354
350
  SelfSDK.logger.debug " - #{message.typ}"
355
- SelfSDK.logger.debug " - #{message}"
351
+ SelfSDK.logger.debug " - #{message.payload}"
356
352
  SelfSDK.logger.debug " - #{@type_observer.keys.join(',')}"
357
353
 
358
354
  # Return if there is no observer setup for this kind of message
@@ -371,6 +367,7 @@ module SelfSDK
371
367
 
372
368
  def subscribe(type, &block)
373
369
  type = @source.message_type(type) if type.is_a? Symbol
370
+ SelfSDK.logger.debug "Subscribing to messages by type: #{type}"
374
371
  @type_observer[type] = { block: block }
375
372
  end
376
373
 
@@ -431,7 +428,7 @@ module SelfSDK
431
428
  end
432
429
 
433
430
  def process_incomming_message(input)
434
- message = parse_and_write_offset(input)
431
+ message = parse(input)
435
432
 
436
433
  if @messages.include? message.id
437
434
  message.validate! @messages[message.id][:original_message]
@@ -450,21 +447,17 @@ module SelfSDK
450
447
  nil
451
448
  end
452
449
 
453
- def parse_and_write_offset(input)
450
+ def parse(input)
454
451
  msg = SelfSDK::Messages.parse(input, self)
455
- write_offset(input.offset)
456
- # Avoid catching any other decryption errors.
457
452
  msg
458
- rescue SelfSDK::Messages::UnmappedMessage => e
459
- # this is an ummapped message, let's ignore it but write the offset.
460
- write_offset(input.offset)
453
+ rescue SelfSDK::Messages::UnmappedMessage
461
454
  nil
462
455
  end
463
456
 
464
457
  # Authenticates current client on the websocket server.
465
458
  def authenticate
466
459
  @auth_id = SecureRandom.uuid if @auth_id.nil?
467
- @offset = read_offset
460
+ @offset = @storage.account_offset
468
461
 
469
462
  SelfSDK.logger.debug "authenticating with offset (#{@offset})"
470
463
 
@@ -500,23 +493,6 @@ module SelfSDK
500
493
  end
501
494
  end
502
495
 
503
- def read_offset
504
- return 0 unless File.exist? @offset_file
505
-
506
- File.open(@offset_file, 'rb') do |f|
507
- return f.read.to_i
508
- end
509
- end
510
-
511
- def write_offset(offset)
512
- File.open(@offset_file, 'wb') do |f|
513
- f.flock(File::LOCK_EX)
514
- f.write(offset.to_s.rjust(19, "0"))
515
- end
516
- SelfSDK.logger.debug "offset written #{offset}"
517
- @offset = offset
518
- end
519
-
520
496
  def select_priority(mtype)
521
497
  PRIORITIES[mtype] || SelfSDK::Messages::PRIORITY_VISIBLE
522
498
  end
data/lib/selfsdk.rb CHANGED
@@ -49,6 +49,7 @@ module SelfSDK
49
49
  # @option opts [String] :messaging_url The messaging self provider url.
50
50
  # @option opts [Bool] :auto_reconnect Automatically reconnects to websocket if connection is lost (defaults to true).
51
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.
52
53
  def initialize(app_id, app_key, storage_key, storage_dir, opts = {})
53
54
  app_key = cleanup_key(app_key)
54
55
 
@@ -60,9 +61,12 @@ module SelfSDK
60
61
  messaging_url = messaging_url(opts)
61
62
  @started = false
62
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))
63
66
  @messaging_client = MessagingClient.new(messaging_url,
64
67
  @client,
65
68
  storage_key,
69
+ storage,
66
70
  storage_dir: storage_dir,
67
71
  auto_reconnect: opts.fetch(:auto_reconnect, MessagingClient::DEFAULT_AUTO_RECONNECT),
68
72
  device_id: opts.fetch(:device_id, MessagingClient::DEFAULT_DEVICE))
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.217
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
@@ -395,6 +409,7 @@ files:
395
409
  - lib/signature_graph.rb
396
410
  - lib/source_definition.rb
397
411
  - lib/sources.rb
412
+ - lib/storage.rb
398
413
  homepage: https://www.joinself.com/
399
414
  licenses:
400
415
  - MIT