Class: KStor::Store

Inherits:
Object
  • Object
show all
Defined in:
lib/kstor/store.rb

Overview

Store and fetch objects in an SQLite database. rubocop:disable Metrics/MethodLength

Instance Method Summary collapse

Constructor Details

#initialize(file_path) ⇒ KStor::Store

Create a new store backed by the given SQLite database file.

Parameters:

  • file_path (String)

    path to SQLite database file



58
59
60
61
62
# File 'lib/kstor/store.rb', line 58

def initialize(file_path)
  @file_path = file_path
  @db = SQLConnection.new(file_path)
  @cache = StoreCache.new
end

Instance Method Details

#group_create(name, pubk) ⇒ Integer

Create a new group.

Note that it doesn’t store the group private key, as it must only exist in users keychains.

Parameters:

  • name (String)

    Name of the new group (must be unique in database)

  • pubk (KStor::Crypto::PublicKey)

    group public key

Returns:

  • (Integer)

    ID of the new group



143
144
145
146
147
148
149
150
# File 'lib/kstor/store.rb', line 143

def group_create(name, pubk)
  @db.execute(<<-EOSQL, name, pubk.to_s)
    INSERT INTO groups (name, pubk)
         VALUES (?, ?)
  EOSQL
  @cache.forget_groups
  @db.last_insert_row_id
end

#groupsArray[KStor::Model::Group]

List all groups.

Note that this list is cached in memory, so calling this method multiple times should be cheap.

Returns:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/kstor/store.rb', line 158

def groups
  @cache.groups do
    Log.debug('store: loading groups')
    rows = @db.execute(<<-EOSQL)
        SELECT id,
               name,
               pubk
          FROM groups
      ORDER BY name
    EOSQL
    rows.to_h do |r|
      a = []
      a << r['id']
      a << Model::Group.new(
        id: r['id'], name: r['name'], pubk: Crypto::PublicKey.new(r['pubk'])
      )
      a
    end
  end
end

#groups_for_secret(secret_id) ⇒ Array[KStor::Model::Group]

List of group IDs that can read this secret.

Parameters:

  • secret_id (Integer)

    ID of secret

Returns:



331
332
333
334
335
336
337
338
339
# File 'lib/kstor/store.rb', line 331

def groups_for_secret(secret_id)
  Log.debug("store: loading group IDs for secret #{secret_id}")
  rows = @db.execute(<<-EOSQL, secret_id)
      SELECT group_id
        FROM secret_values
       WHERE secret_id = ?
  EOSQL
  rows.map { |r| r['group_id'] }
end

#keychain_item_create(user_id, group_id, encrypted_privk) ⇒ Object

Add a group private key to a user keychain.

Parameters:

  • user_id (Integer)

    ID of an existing user

  • group_id (Integer)

    ID of an existing group

  • encrypted_privk (KStor::Crypto::ArmoredValue)

    group private key encrypted with user public key



128
129
130
131
132
133
# File 'lib/kstor/store.rb', line 128

def keychain_item_create(user_id, group_id, encrypted_privk)
  @db.execute(<<-EOSQL, user_id, group_id, encrypted_privk.to_s)
    INSERT INTO group_members (user_id, group_id, encrypted_privk)
         VALUES (?, ?, ?)
  EOSQL
end

#secret_create(author_id, encrypted_data) ⇒ Integer

Create a new secret.

Encrypted data should be a map of group_id to encrypted data for this group’s key pair as a two-value array, first metadata and then value.

Parameters:

  • author_id (Integer)

    ID of user that creates the new secret

  • encrypted_data
    Array[Hash[Integer, Array]]

    see above

    description for shape of value.

Returns:

  • (Integer)

    ID of new secret



314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/kstor/store.rb', line 314

def secret_create(author_id, encrypted_data)
  Log.debug("store: creating secret for user #{author_id}")
  @db.execute(<<-EOSQL, author_id, author_id)
    INSERT INTO secrets (value_author_id, meta_author_id) VALUES (?, ?)
  EOSQL
  secret_id = @db.last_insert_row_id
  encrypted_data.each do |group_id, (ciphertext, )|
    secret_value_create(secret_id, group_id, ciphertext, )
  end

  secret_id
end

#secret_delete(secret_id) ⇒ Object

Delete a secrete from database.

Parameters:

  • secret_id (Integer)

    ID of secret



388
389
390
391
392
393
394
# File 'lib/kstor/store.rb', line 388

def secret_delete(secret_id)
  Log.debug("store: delete secret ##{secret_id}")
  # Will cascade to secret_values:
  @db.execute(<<-EOSQL, secret_id)
    DELETE FROM secrets WHERE id = ?
  EOSQL
end

#secret_fetch(secret_id, user_id) ⇒ KStor::Model::Secret?

Fetch one secret by it’s ID.

Parameters:

  • secret_id (Integer)

    ID of secret

  • user_id (Integer)

    ID of secret reader

Returns:

  • (KStor::Model::Secret, nil)

    A secret, or nil if secret_id was not found or user_id can’t read it.



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/kstor/store.rb', line 280

def secret_fetch(secret_id, user_id)
  Log.debug(
    "store: loading secret value ##{secret_id} for user ##{user_id}"
  )
  rows = @db.execute(<<-EOSQL, user_id, secret_id)
       SELECT s.id,
              s.value_author_id,
              s.meta_author_id,
              sv.group_id,
              sv.ciphertext,
              sv.encrypted_metadata
         FROM secrets s,
              secret_values sv,
              group_members gm
        WHERE gm.user_id = ?
          AND gm.group_id = sv.group_id
          AND sv.secret_id = ?
          AND s.id = sv.secret_id
  EOSQL
  return nil if rows.empty?

  secret_from_row(rows.first)
end

#secret_setmeta(secret_id, user_id, group_encrypted_metadata) ⇒ Object

Overwrite secret metadata.

Parameters:

  • secret_id (Integer)

    ID of secret to update

  • user_id (Integer)

    ID of user that changes metadata

  • group_encrypted_metadata
    Array[Hash[Integer, KStor::Crypt::ArmoredValue]]

    map of group IDs to

    encrypted metadata.



348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/kstor/store.rb', line 348

def secret_setmeta(secret_id, user_id, )
  Log.debug("store: set metadata for secret ##{secret_id}")
  @db.execute(<<-EOSQL, user_id, secret_id)
    UPDATE secrets SET meta_author_id = ? WHERE id = ?
  EOSQL
  .each do |group_id, |
    @db.execute(<<-EOSQL, .to_s, secret_id, group_id)
      UPDATE secret_values
         SET encrypted_metadata = ?
       WHERE secret_id = ?
         AND group_id = ?
    EOSQL
  end
end

#secret_setvalue(secret_id, user_id, group_ciphertexts) ⇒ Object

Overwrite secret value.

Parameters:

  • secret_id (Integer)

    ID of secret to update

  • user_id (Integer)

    ID of user that changes the value

  • group_ciphertexts
    Array[Hash[Integer, KStor::Crypt::ArmoredValue]]

    map of group IDs to

    encrypted values.



370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/kstor/store.rb', line 370

def secret_setvalue(secret_id, user_id, group_ciphertexts)
  Log.debug("store: set value for secret ##{secret_id}")
  @db.execute(<<-EOSQL, user_id, secret_id)
    UPDATE secrets SET value_author_id = ? WHERE id = ?
  EOSQL
  group_ciphertexts.each do |group_id, ciphertext|
    @db.execute(<<-EOSQL, ciphertext.to_s, secret_id, group_id)
      UPDATE secret_values
         SET ciphertext = ?
       WHERE secret_id = ?
         AND group_id = ?
    EOSQL
  end
end

#secrets_for_user(user_id) ⇒ Array[KStor::Model::Secret]

List of all secrets that should be readable by a user.

Parameters:

  • user_id (Integer)

    ID of user that will read the secrets

Returns:



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/kstor/store.rb', line 252

def secrets_for_user(user_id)
  Log.debug("store: loading secrets for user ##{user_id}")
  rows = @db.execute(<<-EOSQL, user_id)
       SELECT s.id,
              s.value_author_id,
              s.meta_author_id,
              sv.group_id,
              sv.ciphertext,
              sv.encrypted_metadata
         FROM secrets s,
              secret_values sv,
              group_members gm
        WHERE gm.user_id = ?
          AND gm.group_id = sv.group_id
          AND sv.secret_id = s.id
     GROUP BY s.id
     ORDER BY s.id, sv.group_id
  EOSQL

  rows.map { |r| secret_from_row(r) }
end

#transactionObject

Execute the given block in a database transaction.



65
66
67
# File 'lib/kstor/store.rb', line 65

def transaction(&)
  @db.transaction(&)
end

#user_by_id(user_id) ⇒ KStor::Model::User?

Lookup user by ID.

Parameters:

  • user_id (Integer)

    User ID

Returns:

  • (KStor::Model::User, nil)

    a user object instance with encrypted private data, or nil if login was not found in database.



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/kstor/store.rb', line 230

def user_by_id(user_id)
  Log.debug("store: loading user by ID ##{user_id}")
  rows = @db.execute(<<-EOSQL, user_id)
       SELECT u.id,
              u.login,
              u.name,
              u.status,
              c.kdf_params,
              c.pubk,
              c.encrypted_privk,
         FROM users u
    LEFT JOIN users_crypto_data c ON (c.user_id = u.id)
        WHERE u.id = ?
  EOSQL
  user_from_resultset(rows, include_crypto_data: true)
end

#user_by_login(login) ⇒ KStor::Model::User?

Lookup user by login.

Parameters:

  • login (String)

    User login

Returns:

  • (KStor::Model::User, nil)

    a user object instance with encrypted private data, or nil if login was not found in database.



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/kstor/store.rb', line 208

def ()
  Log.debug("store: loading user by login #{.inspect}")
  rows = @db.execute(<<-EOSQL, )
       SELECT u.id,
              u.login,
              u.name,
              u.status,
              c.kdf_params,
              c.pubk,
              c.encrypted_privk
         FROM users u
    LEFT JOIN users_crypto_data c ON (c.user_id = u.id)
        WHERE u.login = ?
  EOSQL
  user_from_resultset(rows, include_crypto_data: true)
end

#user_create(user) ⇒ KStor::Model::User

Create a new user in database.

Parameters:

Returns:



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/kstor/store.rb', line 84

def user_create(user)
  @db.execute(<<-EOSQL, user., user.name, 'new')
    INSERT INTO users (login, name, status)
         VALUES (?, ?, ?)
  EOSQL
  user_id = @db.last_insert_row_id
  Log.debug("store: stored new user #{user.}")
  params = [user.kdf_params, user.pubk, user.encrypted_privk].map(&:to_s)
  return user_id if params.any?(&:nil?)

  @db.execute(<<-EOSQL, user.id, *params)
    INSERT INTO users_crypto_data (user_id, kdf_params, pubk, encrypted_privk)
         VALUES (?, ?, ?, ?)
  EOSQL
  Log.debug("store: stored user crypto data for #{user.}")
  @cache.forget_users

  user_id
end

#user_update(user) ⇒ Object

Update user name, status and keychain.

Parameters:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/kstor/store.rb', line 107

def user_update(user)
  @db.execute(<<-EOSQL, user.name, user.status, user.id)
    UPDATE users SET name = ?, status = ?
     WHERE id = ?
  EOSQL
  params = [user.kdf_params, user.pubk, user.encrypted_privk, user.id]
  @db.execute(<<-EOSQL, *params)
    UPDATE users_crypto_data SET
           kdf_params = ?,
           pubk = ?
           encrypted_params = ?
     WHERE user_id = ?
  EOSQL
end

#usersArray[KStor::Model::User]

List all users.

Note that this list is cached in memory, so calling this method multiple times should be cheap.

Returns:



185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/kstor/store.rb', line 185

def users
  @cache.users do
    Log.debug('store: loading users')
    rows = @db.execute(<<-EOSQL)
         SELECT u.id,
                u.login,
                u.name,
                u.status,
                c.pubk
           FROM users u
      LEFT JOIN users_crypto_data c ON (c.user_id = u.id)
       ORDER BY u.login
    EOSQL

    users_from_resultset(rows)
  end
end

#users?Boolean

True if database contains any users.

Returns:

  • (Boolean)

    false if user table is empty



72
73
74
75
76
77
78
# File 'lib/kstor/store.rb', line 72

def users?
  rows = @db.execute('SELECT count(*) AS n FROM users')
  count = Integer(rows.first['n'])
  Log.debug("store: count of users is #{count}")

  count.positive?
end