kstor 0.4.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.
- checksums.yaml +7 -0
- data/README.md +7 -0
- data/bin/kstor +286 -0
- data/bin/kstor-srv +26 -0
- data/lib/kstor/config.rb +66 -0
- data/lib/kstor/controller/authentication.rb +79 -0
- data/lib/kstor/controller/secret.rb +201 -0
- data/lib/kstor/controller/users.rb +62 -0
- data/lib/kstor/controller.rb +80 -0
- data/lib/kstor/crypto/ascii_armor.rb +27 -0
- data/lib/kstor/crypto/keys.rb +116 -0
- data/lib/kstor/crypto.rb +240 -0
- data/lib/kstor/error.rb +85 -0
- data/lib/kstor/log.rb +56 -0
- data/lib/kstor/message.rb +132 -0
- data/lib/kstor/model.rb +437 -0
- data/lib/kstor/server.rb +51 -0
- data/lib/kstor/session.rb +80 -0
- data/lib/kstor/socket_server.rb +113 -0
- data/lib/kstor/sql_connection.rb +74 -0
- data/lib/kstor/store.rb +383 -0
- data/lib/kstor/systemd.rb +25 -0
- data/lib/kstor/version.rb +5 -0
- data/lib/kstor.rb +10 -0
- metadata +141 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: af39f01820f488e20a4b602f5151e87e07176268cf543aecc90f19bcb6166ec2
|
4
|
+
data.tar.gz: 84352bc5dddd13f4e33c1efac2aae1d360c4a53fb1be24b2fd32d8e61111b770
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 646da6b5709b454e971d4a129890c6718ad1b177c614f113fd77556df12bfb4feb7c6d44d8beb4e17af4bb9b44d68c2321acf8c54b42fc5f6bc728d6b4e0a40d
|
7
|
+
data.tar.gz: 71264b16719b3d516ff7c5bdea5fc362bedee48c23ed804401877010a65f5cb7cc75336266c70699bbebc63737ef1f775eb4986193ba7722cb289e1a18c5c709
|
data/README.md
ADDED
data/bin/kstor
ADDED
@@ -0,0 +1,286 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'kstor'
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
require 'socket'
|
8
|
+
require 'etc'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'slop'
|
11
|
+
|
12
|
+
module KStor
|
13
|
+
# Manage KStor client configuration and state on disk.
|
14
|
+
module ClientState
|
15
|
+
class << self
|
16
|
+
DEFAULT_CONFIG = {
|
17
|
+
'socket' => '/home/jpi/code/kstor/testworkdir/kstor.socket'
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
def load_config(progr)
|
21
|
+
DEFAULT_CONFIG.merge(load_config_file(progr))
|
22
|
+
end
|
23
|
+
|
24
|
+
def session_id_file(progr)
|
25
|
+
dir = File.join(xdg_runtime, progr)
|
26
|
+
FileUtils.mkdir_p(dir)
|
27
|
+
file = File.join(dir, 'session-id')
|
28
|
+
FileUtils.touch(file)
|
29
|
+
FileUtils.chmod(0o600, file)
|
30
|
+
file
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_session_id(progr)
|
34
|
+
sid = nil
|
35
|
+
File.open(session_id_file(progr)) { |f| sid = f.read.chomp }
|
36
|
+
sid = nil if sid.empty?
|
37
|
+
|
38
|
+
sid
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def xdg_config
|
44
|
+
ENV.fetch('XDG_CONFIG_HOME', File.join(Dir.home, '.config'))
|
45
|
+
end
|
46
|
+
|
47
|
+
def xdg_state
|
48
|
+
ENV.fetch('XDG_STATE_HOME', File.join(Dir.home, '.local', 'state'))
|
49
|
+
end
|
50
|
+
|
51
|
+
def xdg_runtime
|
52
|
+
dir = ENV.fetch('XDG_RUNTIME_DIR', nil)
|
53
|
+
return dir if dir
|
54
|
+
|
55
|
+
warn('XDG_RUNTIME_DIR is undefined, using XDG_STATE_HOME instead')
|
56
|
+
xdg_state
|
57
|
+
end
|
58
|
+
|
59
|
+
def config_file(progr)
|
60
|
+
dir = File.join(xdg_config, progr)
|
61
|
+
FileUtils.mkdir_p(dir)
|
62
|
+
File.join(dir, 'session-id')
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_config_file(progr)
|
66
|
+
data = File.read(config_file(progr))
|
67
|
+
YAML.parse(data)
|
68
|
+
rescue Errno::ENOENT
|
69
|
+
{}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sub-commands that can be invoked from the command-line.
|
75
|
+
module ClientSubCommands
|
76
|
+
def group_create
|
77
|
+
request('group-create') do |o|
|
78
|
+
o.string('-n', '--name', 'Group name')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def secret_create
|
83
|
+
req = request('secret-create') do |o|
|
84
|
+
o.string('-p', '--plaintext', 'Value of the secret')
|
85
|
+
o.array('-g', '--group_ids', 'Groups that can unlock the secret')
|
86
|
+
o.string('-a', '--app', 'application of this secret')
|
87
|
+
o.string('-d', '--database', 'database of this secret')
|
88
|
+
o.string('-l', '--login', 'login of this secret')
|
89
|
+
o.string('-S', '--server', 'server of this secret')
|
90
|
+
o.string('-u', '--url', 'url for this secret')
|
91
|
+
end
|
92
|
+
reorganize_secret_meta_args(req)
|
93
|
+
end
|
94
|
+
|
95
|
+
def secret_search
|
96
|
+
request('secret-search') do |o|
|
97
|
+
o.string('-a', '--app', 'secrets for this application')
|
98
|
+
o.string('-d', '--database', 'secrets for this database')
|
99
|
+
o.string('-l', '--login', 'secrets for this login')
|
100
|
+
o.string('-s', '--server', 'secrets for this server')
|
101
|
+
o.string('-u', '--url', 'secrets for this url')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def secret_unlock
|
106
|
+
request('secret-unlock') do |o|
|
107
|
+
o.string('-s', '--secret-id', 'secret ID to unlock')
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def secret_update_meta
|
112
|
+
req = request('secret-update-meta') do |o|
|
113
|
+
o.string('-s', '--secret-id', 'secret ID to modify')
|
114
|
+
o.string('-a', '--app', 'new application of this secret')
|
115
|
+
o.string('-d', '--database', 'new database of this secret')
|
116
|
+
o.string('-l', '--login', 'new login of this secret')
|
117
|
+
o.string('-S', '--server', 'new server of this secret')
|
118
|
+
o.string('-u', '--url', 'new url for this secret')
|
119
|
+
end
|
120
|
+
reorganize_secret_meta_args(req)
|
121
|
+
end
|
122
|
+
|
123
|
+
def secret_update_value
|
124
|
+
request('secret-update-value') do |o|
|
125
|
+
o.string('-s', '--secret-id', 'secret ID to modify')
|
126
|
+
o.string('-p', '--plaintext', 'new plaintext value')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def secret_delete
|
131
|
+
request('secret-delete') do |o|
|
132
|
+
o.string('-s', '--secret-id', 'secret ID to delete')
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# KStor command-line client.
|
138
|
+
class Client
|
139
|
+
include ClientSubCommands
|
140
|
+
|
141
|
+
def initialize
|
142
|
+
@progr = File.basename($PROGRAM_NAME)
|
143
|
+
@config = ClientState.load_config(@progr)
|
144
|
+
@user = user_from_argv
|
145
|
+
end
|
146
|
+
|
147
|
+
def run
|
148
|
+
request_type = ARGV.shift
|
149
|
+
resp = send_request(request_type)
|
150
|
+
handle_error!(resp) if resp.error?
|
151
|
+
|
152
|
+
puts format_response(resp)
|
153
|
+
return unless resp.respond_to?(:session_id)
|
154
|
+
|
155
|
+
File.open(ClientState.session_id_file(@progr), 'w') do |f|
|
156
|
+
f.puts(resp.session_id)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def format_response(resp)
|
163
|
+
data = resp.args.dup
|
164
|
+
data.delete('session_id')
|
165
|
+
JSON.pretty_generate(data)
|
166
|
+
end
|
167
|
+
|
168
|
+
def reorganize_secret_meta_args(req)
|
169
|
+
req.args['meta'] = {
|
170
|
+
'app' => req.args.delete('app'),
|
171
|
+
'database' => req.args.delete('database'),
|
172
|
+
'login' => req.args.delete('login'),
|
173
|
+
'server' => req.args.delete('server'),
|
174
|
+
'url' => req.args.delete('url')
|
175
|
+
}
|
176
|
+
req.args['meta'].compact!
|
177
|
+
req
|
178
|
+
end
|
179
|
+
|
180
|
+
def handle_error!(resp)
|
181
|
+
if resp.args['code'] == 'AUTH/BADSESSION'
|
182
|
+
FileUtils.rm(ClientState.session_id_file(@progr))
|
183
|
+
warn('session expired')
|
184
|
+
else
|
185
|
+
warn(resp.args['message'])
|
186
|
+
end
|
187
|
+
exit 1
|
188
|
+
end
|
189
|
+
|
190
|
+
def send_request(request_type)
|
191
|
+
meth = method_name(request_type)
|
192
|
+
|
193
|
+
req = __send__(meth)
|
194
|
+
socket = UNIXSocket.new(@config['socket'])
|
195
|
+
socket.send(req.serialize, 0)
|
196
|
+
|
197
|
+
data, = socket.recvfrom(4096)
|
198
|
+
Response.parse(data)
|
199
|
+
rescue UnparsableResponse
|
200
|
+
warn('server speaks funny; look at logs!')
|
201
|
+
exit(1)
|
202
|
+
end
|
203
|
+
|
204
|
+
REQUEST_METHODS = {
|
205
|
+
'group-create' => :group_create,
|
206
|
+
'secret-create' => :secret_create,
|
207
|
+
'secret-search' => :secret_search,
|
208
|
+
'secret-unlock' => :secret_unlock,
|
209
|
+
'secret-update-meta' => :secret_update_meta,
|
210
|
+
'secret-update-value' => :secret_update_value,
|
211
|
+
'secret-delete' => :secret_delete
|
212
|
+
}.freeze
|
213
|
+
|
214
|
+
def method_name(request_type)
|
215
|
+
if request_type.nil?
|
216
|
+
base_usage
|
217
|
+
exit 0
|
218
|
+
end
|
219
|
+
meth = REQUEST_METHODS[request_type]
|
220
|
+
if meth.nil?
|
221
|
+
warn("Unknown request type #{request_type.inspect}")
|
222
|
+
base_usage
|
223
|
+
exit 1
|
224
|
+
end
|
225
|
+
meth
|
226
|
+
end
|
227
|
+
|
228
|
+
def user_from_argv
|
229
|
+
md = /^(-u|--user)$/.match(ARGV.first)
|
230
|
+
md ? ARGV.shift(2).last : Etc.getlogin
|
231
|
+
end
|
232
|
+
|
233
|
+
def base_usage
|
234
|
+
puts "usage: #{@progr} [--user USER] <req-type> [--help | REQ-ARGS]"
|
235
|
+
request_types = %w[group-create secret-create secret-search secret-unlock]
|
236
|
+
puts "request types: #{request_types.join(', ')}"
|
237
|
+
end
|
238
|
+
|
239
|
+
def ask_password
|
240
|
+
require 'io/console'
|
241
|
+
|
242
|
+
$stdout.print 'Password: '
|
243
|
+
password = $stdin.noecho(&:gets)
|
244
|
+
$stdout.puts('')
|
245
|
+
|
246
|
+
password.chomp
|
247
|
+
end
|
248
|
+
|
249
|
+
def auth
|
250
|
+
session_id = ClientState.load_session_id(@progr)
|
251
|
+
|
252
|
+
if session_id
|
253
|
+
{ 'session_id' => session_id }
|
254
|
+
else
|
255
|
+
{ 'login' => @user, 'password' => ask_password }
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def request(type, &block)
|
260
|
+
hreq = {}
|
261
|
+
hreq['type'] = type
|
262
|
+
hreq['args'] = parse_opts(type) do |o|
|
263
|
+
block.call(o)
|
264
|
+
end
|
265
|
+
KStor::Message.parse_request(hreq.merge(auth).to_json)
|
266
|
+
end
|
267
|
+
|
268
|
+
def parse_opts(request_type)
|
269
|
+
opts = Slop.parse do |o|
|
270
|
+
o.banner = <<-EOUSAGE
|
271
|
+
usage: #{@progr} [--user USER] #{request_type} [--help | REQ-ARGS]
|
272
|
+
EOUSAGE
|
273
|
+
o.on('-h', '--help', 'show this text') do
|
274
|
+
puts o
|
275
|
+
exit 0
|
276
|
+
end
|
277
|
+
o.separator('')
|
278
|
+
yield o
|
279
|
+
end
|
280
|
+
opts.to_hash.compact
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
cli = KStor::Client.new
|
286
|
+
cli.run
|
data/bin/kstor-srv
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'kstor'
|
5
|
+
|
6
|
+
KStor::Log.reporting_level = Journald::LOG_DEBUG
|
7
|
+
|
8
|
+
config = KStor::Config.load(ARGV.shift)
|
9
|
+
|
10
|
+
store = KStor::Store.new(config.database)
|
11
|
+
session_store = KStor::SessionStore.new(
|
12
|
+
config.session_idle_timeout,
|
13
|
+
config.session_life_timeout
|
14
|
+
)
|
15
|
+
request_handler = KStor::Controller::RequestHandler.new(store, session_store)
|
16
|
+
|
17
|
+
server = KStor::Server.new(
|
18
|
+
controller: request_handler,
|
19
|
+
socket_path: config.socket,
|
20
|
+
nworkers: config.nworkers
|
21
|
+
)
|
22
|
+
|
23
|
+
me = File.basename($PROGRAM_NAME)
|
24
|
+
Process.setproctitle(me)
|
25
|
+
|
26
|
+
server.start
|
data/lib/kstor/config.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module KStor
|
6
|
+
# Configuration items stored as YAML.
|
7
|
+
class Config
|
8
|
+
# Default values for configuration items.
|
9
|
+
#
|
10
|
+
# They are used when loading configuration from a file, and for defining
|
11
|
+
# accessor methods.
|
12
|
+
#
|
13
|
+
# @!attribute [r] database
|
14
|
+
# @return [String] path to SQLite database file
|
15
|
+
#
|
16
|
+
# @!attribute [r] socket
|
17
|
+
# @return [String] path to KStor server listening socket
|
18
|
+
#
|
19
|
+
# @!attribute [r] nworkers
|
20
|
+
# @return [Integer] number of worker threads
|
21
|
+
#
|
22
|
+
# @!attribute [r] session_idle_timeout
|
23
|
+
# @return [Integer] seconds of inactivity before a session is closed
|
24
|
+
#
|
25
|
+
# @!attribute [r] session_life_timeout
|
26
|
+
# @return [Integer] seconds before a session is closed
|
27
|
+
DEFAULTS = {
|
28
|
+
'database' => 'data/db.sqlite',
|
29
|
+
'socket' => 'run/kstor-server.socket',
|
30
|
+
'nworkers' => 5,
|
31
|
+
'session_idle_timeout' => 15 * 60,
|
32
|
+
'session_life_timeout' => 4 * 60 * 60
|
33
|
+
}.freeze
|
34
|
+
|
35
|
+
class << self
|
36
|
+
# Load configuration from a file.
|
37
|
+
#
|
38
|
+
# For each missing configuration item in file, use the default from
|
39
|
+
# DEFAULTS.
|
40
|
+
#
|
41
|
+
# @param path [String] path to config file
|
42
|
+
# @return [KStor::Config] configuration object
|
43
|
+
def load(path)
|
44
|
+
hash = if path && File.file?(path)
|
45
|
+
YAML.load_file(path)
|
46
|
+
else
|
47
|
+
{}
|
48
|
+
end
|
49
|
+
new(DEFAULTS.merge(hash))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create configuration from hash data.
|
54
|
+
#
|
55
|
+
# @param hash [Hash] configuration items
|
56
|
+
def initialize(hash)
|
57
|
+
@data = hash
|
58
|
+
end
|
59
|
+
|
60
|
+
DEFAULTS.each_key do |k|
|
61
|
+
define_method(k.to_sym) do
|
62
|
+
@data[k]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KStor
|
4
|
+
module Controller
|
5
|
+
# Handle user authentication and sessions.
|
6
|
+
class Authentication
|
7
|
+
def initialize(store, session_store)
|
8
|
+
@store = store
|
9
|
+
@sessions = session_store
|
10
|
+
end
|
11
|
+
|
12
|
+
def authenticate(req)
|
13
|
+
if @store.users?
|
14
|
+
unlock_user(req)
|
15
|
+
else
|
16
|
+
create_first_user(req)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# return true if login is allowed to access the database.
|
21
|
+
def allowed?(user)
|
22
|
+
user.status == 'new' || user.status == 'active'
|
23
|
+
end
|
24
|
+
|
25
|
+
def unlock_user(req)
|
26
|
+
if req.respond_to?(:session_id)
|
27
|
+
session_id = req.session_id
|
28
|
+
user, secret_key = load_session(session_id)
|
29
|
+
else
|
30
|
+
user = load_user(req.login)
|
31
|
+
secret_key = user.secret_key(req.password)
|
32
|
+
session = Session.create(user, secret_key)
|
33
|
+
@sessions << session
|
34
|
+
session_id = session.id
|
35
|
+
end
|
36
|
+
user.unlock(secret_key)
|
37
|
+
|
38
|
+
[user, session_id]
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_session(sid)
|
42
|
+
Log.debug("loading session #{sid}")
|
43
|
+
session = @sessions[sid]
|
44
|
+
raise Error.for_code('AUTH/BADSESSION', sid) unless session
|
45
|
+
|
46
|
+
[session.user, session.secret_key]
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_user(login)
|
50
|
+
Log.debug("authenticating user #{login.inspect}")
|
51
|
+
user = @store.user_by_login(login)
|
52
|
+
Log.debug("loaded user ##{user.id} #{user.login}")
|
53
|
+
unless user && allowed?(user)
|
54
|
+
raise Error.for_code('AUTH/FORBIDDEN', login)
|
55
|
+
end
|
56
|
+
|
57
|
+
user
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_first_user(req)
|
61
|
+
raise Error.for_code('AUTH/MISSING') unless req.respond_to?(:login)
|
62
|
+
|
63
|
+
Log.info("no user in database, creating #{req.login.inspect}")
|
64
|
+
user = Model::User.new(
|
65
|
+
login: req.login, name: req.login, status: 'new', keychain: {}
|
66
|
+
)
|
67
|
+
secret_key = user.secret_key(req.password)
|
68
|
+
user.unlock(secret_key)
|
69
|
+
@store.user_create(user)
|
70
|
+
Log.info("user #{user.login} created")
|
71
|
+
|
72
|
+
session = Session.create(user, secret_key)
|
73
|
+
@sessions << session
|
74
|
+
|
75
|
+
[user, session.id]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'kstor/store'
|
4
|
+
require 'kstor/model'
|
5
|
+
require 'kstor/crypto'
|
6
|
+
|
7
|
+
module KStor
|
8
|
+
class SecretNotFound < Error
|
9
|
+
error_code 'SECRET/NOTFOUND'
|
10
|
+
error_message 'Secret #%s not found.'
|
11
|
+
end
|
12
|
+
|
13
|
+
module Controller
|
14
|
+
# Handle secret-related requests.
|
15
|
+
class Secret
|
16
|
+
def initialize(store)
|
17
|
+
@store = store
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_request(user, req)
|
21
|
+
case req.type
|
22
|
+
when 'secret-create' then handle_create(user, req)
|
23
|
+
when 'secret-search' then handle_search(user, req)
|
24
|
+
when 'secret-unlock' then handle_unlock(user, req)
|
25
|
+
when 'secret-update-meta' then handle_update_meta(user, req)
|
26
|
+
when 'secret-update-value' then handle_update_value(user, req)
|
27
|
+
when 'secret-delete' then handle_delete(user, req)
|
28
|
+
else
|
29
|
+
raise Error.for_code('REQ/UNKNOWN', req.type)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def handle_create(user, req)
|
36
|
+
meta = Model::SecretMeta.new(**req.args['meta'])
|
37
|
+
secret_groups = req.args['group_ids'].map { |gid| groups[gid.to_i] }
|
38
|
+
secret_id = create(
|
39
|
+
user, req.args['plaintext'], secret_groups, meta
|
40
|
+
)
|
41
|
+
Response.new('secret.created', 'secret_id' => secret_id)
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_search(user, req)
|
45
|
+
secrets = search(user, Model::SecretMeta.new(**req.args))
|
46
|
+
args = secrets.map do |s|
|
47
|
+
h = s.to_h
|
48
|
+
h.delete('group_id')
|
49
|
+
h
|
50
|
+
end
|
51
|
+
Response.new(
|
52
|
+
'secret.list',
|
53
|
+
'secrets' => args
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def handle_unlock(user, req)
|
58
|
+
secret_id = req.args['secret_id']
|
59
|
+
secret = unlock(user, secret_id)
|
60
|
+
args = unlock_format(secret)
|
61
|
+
|
62
|
+
Response.new('secret.value', **args)
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle_update_meta(user, req)
|
66
|
+
meta = Model::SecretMeta.new(req.args['meta'])
|
67
|
+
Log.debug("secret#handle_update_meta: meta=#{meta.to_h.inspect}")
|
68
|
+
update_meta(user, req.args['secret_id'], meta)
|
69
|
+
Response.new('secret.updated', 'secret_id' => req.args['secret_id'])
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_update_value(user, req)
|
73
|
+
update_value(user, req.args['secret_id'], req.args['plaintext'])
|
74
|
+
Response.new('secret.updated', 'secret_id' => req.args['secret_id'])
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_delete(user, req)
|
78
|
+
delete(user, req.args['secret_id'])
|
79
|
+
Response.new('secret.deleted', 'secret_id' => req.args['secret_id'])
|
80
|
+
end
|
81
|
+
|
82
|
+
def users
|
83
|
+
@users ||= @store.users
|
84
|
+
end
|
85
|
+
|
86
|
+
def groups
|
87
|
+
@groups ||= @store.groups
|
88
|
+
end
|
89
|
+
|
90
|
+
# in: metadata wildcards
|
91
|
+
# needs: private key of one common group between user and secrets
|
92
|
+
# out: array of:
|
93
|
+
# - secret id
|
94
|
+
# - secret metadata
|
95
|
+
# - secret metadata and value authors
|
96
|
+
def search(user, meta)
|
97
|
+
return [] if user.keychain.empty?
|
98
|
+
|
99
|
+
@store.secrets_for_user(user.id).select do |secret|
|
100
|
+
unlock_metadata(user, secret)
|
101
|
+
secret.metadata.match?(meta)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# in: secret_id
|
106
|
+
# needs: private key of one common group between user and secret
|
107
|
+
# out: plaintext
|
108
|
+
def unlock(user, secret_id)
|
109
|
+
secret = @store.secret_fetch(secret_id, user.id)
|
110
|
+
group_privk = user.keychain[secret.group_id].privk
|
111
|
+
|
112
|
+
value_author = users[secret.value_author_id]
|
113
|
+
secret.unlock(value_author.pubk, group_privk)
|
114
|
+
|
115
|
+
meta_author = users[secret.meta_author_id]
|
116
|
+
secret.unlock_metadata(meta_author.pubk, group_privk)
|
117
|
+
|
118
|
+
secret
|
119
|
+
end
|
120
|
+
|
121
|
+
# in: plaintext, group ids, metadata
|
122
|
+
# needs: encrypted metadata and ciphertext for each group
|
123
|
+
# out: secret id
|
124
|
+
def create(user, plaintext, groups, meta)
|
125
|
+
encrypted_data = {}
|
126
|
+
Log.debug("secret#create: group_ids = #{groups.inspect}")
|
127
|
+
groups.each do |g|
|
128
|
+
encrypted_data[g.id] = [
|
129
|
+
Crypto.encrypt_secret_value(g.pubk, user.privk, plaintext),
|
130
|
+
Crypto.encrypt_secret_metadata(g.pubk, user.privk, meta.to_h)
|
131
|
+
]
|
132
|
+
end
|
133
|
+
@store.secret_create(user.id, encrypted_data)
|
134
|
+
end
|
135
|
+
|
136
|
+
# in: secret id, metadata
|
137
|
+
# needs: every group public key for this secret, user private key
|
138
|
+
# out: nil
|
139
|
+
def update_meta(user, secret_id, partial_meta)
|
140
|
+
secret = @store.secret_fetch(secret_id, user.id)
|
141
|
+
unlock_metadata(user, secret)
|
142
|
+
meta = secret.metadata.merge(partial_meta)
|
143
|
+
group_ids = @store.groups_for_secret(secret.id)
|
144
|
+
group_encrypted_metadata = group_ids.to_h do |group_id|
|
145
|
+
group_pubk = groups[group_id].pubk
|
146
|
+
author_privk = user.privk
|
147
|
+
encrypted_meta = Crypto.encrypt_secret_metadata(
|
148
|
+
group_pubk, author_privk, meta.to_h
|
149
|
+
)
|
150
|
+
[group_id, encrypted_meta]
|
151
|
+
end
|
152
|
+
@store.secret_setmeta(secret.id, user.id, group_encrypted_metadata)
|
153
|
+
end
|
154
|
+
|
155
|
+
# in: secret id, plaintext
|
156
|
+
# needs: every group public key for this secret, user private key
|
157
|
+
# out: nil
|
158
|
+
def update_value(user, secret_id, plaintext)
|
159
|
+
secret = @store.secret_fetch(secret_id, user.id)
|
160
|
+
group_ids = @store.groups_for_secret(secret.id)
|
161
|
+
group_ciphertexts = group_ids.to_h do |group_id|
|
162
|
+
group_pubk = groups[group_id].pubk
|
163
|
+
author_privk = user.privk
|
164
|
+
encrypted_value = Crypto.encrypt_secret_value(
|
165
|
+
group_pubk, author_privk, plaintext
|
166
|
+
)
|
167
|
+
[group_id, encrypted_value]
|
168
|
+
end
|
169
|
+
@store.secret_setvalue(secret.id, user.id, group_ciphertexts)
|
170
|
+
end
|
171
|
+
|
172
|
+
# in: secret id
|
173
|
+
# needs: nil
|
174
|
+
# out: nil
|
175
|
+
def delete(user, secret_id)
|
176
|
+
# Check if user can see this secret:
|
177
|
+
secret = @store.secret_fetch(secret_id, user.id)
|
178
|
+
raise Error.for_code('SECRET/NOTFOUND', secret_id) if secret.nil?
|
179
|
+
|
180
|
+
@store.secret_delete(secret_id)
|
181
|
+
end
|
182
|
+
|
183
|
+
def unlock_format(secret)
|
184
|
+
args = secret.to_h
|
185
|
+
args['value_author'] = users[secret.value_author_id].to_h
|
186
|
+
args['metadata_author'] = users[secret.meta_author_id].to_h
|
187
|
+
|
188
|
+
group_ids = @store.groups_for_secret(secret.id)
|
189
|
+
args['groups'] = groups.values_at(*group_ids).map(&:to_h)
|
190
|
+
|
191
|
+
args
|
192
|
+
end
|
193
|
+
|
194
|
+
def unlock_metadata(user, secret)
|
195
|
+
group_privk = user.keychain[secret.group_id].privk
|
196
|
+
author = users[secret.meta_author_id]
|
197
|
+
secret.unlock_metadata(author.pubk, group_privk)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|