kstor 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|