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.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/store'
4
+ require 'kstor/model'
5
+ require 'kstor/crypto'
6
+ require 'kstor/log'
7
+
8
+ module KStor
9
+ module Controller
10
+ # Handle user and group related requests.
11
+ class User
12
+ def initialize(store)
13
+ @store = store
14
+ end
15
+
16
+ def handle_request(user, req)
17
+ case req.type
18
+ when /^group-create$/ then handle_group_create(user, req)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def handle_group_create(user, req)
25
+ unless req.args['name']
26
+ raise Error.for_code('REQ/MISSINGARG', 'name', req.type)
27
+ end
28
+
29
+ group = group_create(user, req.args['name'])
30
+ @groups = nil
31
+ Response.new(
32
+ 'group.created',
33
+ 'group_id' => group.id,
34
+ 'group_name' => group.name,
35
+ 'group_pubk' => group.pubk
36
+ )
37
+ end
38
+
39
+ def group_create(user, name)
40
+ pubk, privk = Crypto.generate_key_pair
41
+ group_id = @store.group_create(name, pubk)
42
+ encrypted_privk = Crypto.encrypt_group_privk(
43
+ user.pubk, privk
44
+ )
45
+ Log.debug("encrypted_privk = #{encrypted_privk}")
46
+ keychain_item_create(user, group_id, pubk, encrypted_privk)
47
+
48
+ Model::Group.new(id: group_id, name:, pubk:)
49
+ end
50
+
51
+ def keychain_item_create(user, group_id, pubk, encrypted_privk)
52
+ item = Model::KeychainItem.new(
53
+ group_id:,
54
+ group_pubk: pubk,
55
+ encrypted_privk:
56
+ )
57
+ user.keychain[item.group_id] = item
58
+ @store.keychain_item_create(user.id, group_id, encrypted_privk)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/error'
4
+ require 'kstor/log'
5
+ require 'kstor/store'
6
+ require 'kstor/message'
7
+ require 'kstor/controller/authentication'
8
+ require 'kstor/controller/secret'
9
+ require 'kstor/controller/users'
10
+
11
+ module KStor
12
+ # Error: user was not allowed to access application.
13
+ class UserNotAllowed < Error
14
+ error_code 'AUTH/FORBIDDEN'
15
+ error_message 'User %s not allowed.'
16
+ end
17
+
18
+ # Error: invalid session ID
19
+ class InvalidSession < Error
20
+ error_code 'AUTH/BADSESSION'
21
+ error_message 'Invalid session ID %s'
22
+ end
23
+
24
+ class MissingLoginPassword < Error
25
+ error_code 'AUTH/MISSING'
26
+ error_message 'Missing login and password'
27
+ end
28
+
29
+ # Error: unknown request type.
30
+ class UnknownRequestType < Error
31
+ error_code 'REQ/UNKNOWN'
32
+ error_message 'Unknown request type %s'
33
+ end
34
+
35
+ # Error: missing request argument.
36
+ class MissingArgument < Error
37
+ error_code 'REQ/MISSINGARG'
38
+ error_message 'Missing argument %s for request type %s'
39
+ end
40
+
41
+ module Controller
42
+ # Request handler.
43
+ class RequestHandler
44
+ def initialize(store, session_store)
45
+ @auth = Controller::Authentication.new(store, session_store)
46
+ @secret = Controller::Secret.new(store)
47
+ @user = Controller::User.new(store)
48
+ @store = store
49
+ end
50
+
51
+ def handle_request(req)
52
+ user, sid = @auth.authenticate(req)
53
+ controller = controller_from_request_type(req)
54
+ resp = @store.transaction { controller.handle_request(user, req) }
55
+ user.lock
56
+ resp.session_id = sid
57
+ resp
58
+ rescue RbNaClError => e
59
+ Log.exception(e)
60
+ Error.for_code('CRYPTO/UNSPECIFIED').response
61
+ rescue Error => e
62
+ Log.info(e.message)
63
+ e.response
64
+ end
65
+
66
+ private
67
+
68
+ def controller_from_request_type(req)
69
+ case req.type
70
+ when /^secret-(create|delete|search|unlock|update-(meta|value)?)$/
71
+ @secret
72
+ when /^group-create$/
73
+ @user
74
+ else
75
+ raise Error.for_code('REQ/UNKNOWN', req.type)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KStor
4
+ # Cryptographic functions for KStor.
5
+ module Crypto
6
+ # Encode and decode binary data to ASCII.
7
+ module ASCIIArmor
8
+ class << self
9
+ # ASCII-armor a String of bytes.
10
+ #
11
+ # @param bytes [String] raw string
12
+ # @return [String] ASCII-armored string
13
+ def encode(bytes)
14
+ Base64.strict_encode64(bytes)
15
+ end
16
+
17
+ # Decode an ASCII-armored string back to raw data.
18
+ #
19
+ # @param str [String] ASCII-armored string
20
+ # @return [String] raw string
21
+ def decode(str)
22
+ Base64.strict_decode64(str)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/crypto/ascii_armor'
4
+
5
+ module KStor
6
+ module Crypto
7
+ # Holds together a secret key value and the KDF associated parameters.
8
+ class SecretKey
9
+ # The secret key as an ASCII-armored String
10
+ attr_reader :value
11
+ # KDF parameters as an ASCII-armored String
12
+ attr_reader :kdf_params
13
+
14
+ # Create a SecretKey instance.
15
+ #
16
+ # @param [String] value ASCII-armored secret key derived from passphrase
17
+ # @param [String] kdf_params ASCII-armored key derivation parameters
18
+ # @return [SecretKey] the SecretKey object
19
+ def initialize(value, kdf_params)
20
+ @value = value
21
+ @kdf_params = kdf_params
22
+ end
23
+ end
24
+
25
+ # Holds together a public and private key pair.
26
+ class KeyPair
27
+ # ASCII-armored public key
28
+ attr_reader :pubk
29
+ # ASCII-armored private key
30
+ attr_reader :privk
31
+
32
+ # Create a KeyPair instance.
33
+ #
34
+ # @param [String] pubk ASCII-armored public key
35
+ # @param [String] privk ASCII-armored private key
36
+ def initialize(pubk, privk)
37
+ @pubk = pubk
38
+ @privk = privk
39
+ end
40
+ end
41
+
42
+ # Wrapper class for an ASCII-armored value.
43
+ class ArmoredValue
44
+ def initialize(value)
45
+ @value = value
46
+ end
47
+
48
+ def to_ascii
49
+ @value
50
+ end
51
+ alias to_s to_ascii
52
+
53
+ def to_binary
54
+ ASCIIArmor.decode(@value)
55
+ end
56
+
57
+ def self.from_binary(bin_str)
58
+ new(ASCIIArmor.encode(bin_str))
59
+ end
60
+ end
61
+
62
+ # A Hash.
63
+ class ArmoredHash < ArmoredValue
64
+ def self.from_hash(hash)
65
+ from_binary(hash.to_json)
66
+ end
67
+
68
+ def to_hash
69
+ JSON.parse(to_binary)
70
+ end
71
+
72
+ def [](key)
73
+ to_hash[key]
74
+ end
75
+
76
+ def []=(key, val)
77
+ h = to_hash
78
+ h[key] = val
79
+ @value = ASCIIArmor.encode(h.to_json)
80
+ end
81
+ end
82
+
83
+ # KDF parameters.
84
+ class KDFParams < ArmoredHash
85
+ def self.from_hash(hash)
86
+ hash['salt'] = ASCIIArmor.encode(hash['salt'])
87
+ hash['opslimit'] = hash['opslimit'].to_s
88
+ hash['memlimit'] = hash['memlimit'].to_s
89
+ super(hash)
90
+ end
91
+
92
+ def to_hash
93
+ hash = super
94
+ hash['salt'] = ASCIIArmor.decode(hash['salt'])
95
+ hash['opslimit'] = hash['opslimit'].to_sym
96
+ hash['memlimit'] = hash['memlimit'].to_sym
97
+
98
+ hash
99
+ end
100
+ end
101
+
102
+ # A private key.
103
+ class PrivateKey < ArmoredValue
104
+ def to_rbnacl
105
+ RbNaCl::PrivateKey.new(to_binary)
106
+ end
107
+ end
108
+
109
+ # A public key.
110
+ class PublicKey < ArmoredValue
111
+ def to_rbnacl
112
+ RbNaCl::PublicKey.new(to_binary)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbnacl'
4
+ require 'json'
5
+ require 'base64'
6
+
7
+ require 'kstor/error'
8
+ require 'kstor/crypto/ascii_armor'
9
+ require 'kstor/crypto/keys'
10
+
11
+ module KStor
12
+ # Generic crypto error.
13
+ class CryptoError < Error
14
+ error_code 'CRYPTO/UNSPECIFIED'
15
+ error_message 'Cryptographic error.'
16
+ end
17
+
18
+ # Error in key derivation.
19
+ class RbNaClError < Error
20
+ error_code 'CRYPTO/RBNACL'
21
+ error_message 'RbNaCl error: %s'
22
+ end
23
+
24
+ # Cryptographic functions for KStor.
25
+ #
26
+ # @version 1.0
27
+ module Crypto
28
+ VERSION = 1
29
+
30
+ class << self
31
+ # Derive a secret key suitable for symetric encryption from a passphrase.
32
+ #
33
+ # Key derivation function can use previously stored parameters (as an
34
+ # opaque String) or pass nil to generate random parameters.
35
+ #
36
+ # @param passphrase [String] user passphrase as clear text
37
+ # @param params [KDFParams, nil] KDF parameters;
38
+ # if nil, use defaults.
39
+ # @return [SecretKey] secret key and KDF parameters
40
+ def key_derive(passphrase, params = nil)
41
+ params ||= key_derive_params_generate
42
+ Log.debug("crypto: kdf params = #{params.to_hash}")
43
+ data = RbNaCl::PasswordHash.argon2(
44
+ passphrase, params['salt'],
45
+ params['opslimit'], params['memlimit'], params['digest_size']
46
+ )
47
+ SecretKey.new(ArmoredValue.from_binary(data), params)
48
+ rescue RbNaCl::CryptoError => e
49
+ raise Error.for_code('CRYPTO/RBNACL', e.message)
50
+ end
51
+
52
+ # Check if KDF params match current code in this library.
53
+ #
54
+ # If it is obsolete, you should generate a new secret key from the user's
55
+ # passphrase, and re-encrypt everything that was encrypted with the old
56
+ # secret key.
57
+ #
58
+ # @param params [String] KDF params as an opaque string.
59
+ # @return [Boolean] false if parameters match current library version.
60
+ def kdf_params_obsolete?(params)
61
+ return true if params_str.nil?
62
+
63
+ params['_version'] != VERSION
64
+ rescue RbNaCl::CryptoError => e
65
+ raise Error.for_code('CRYPTO/RBNACL', e.message)
66
+ end
67
+
68
+ # Generate new key pair.
69
+ #
70
+ # @return [Array<PublicKey, PrivateKey>] new key pair
71
+ def generate_key_pair
72
+ privk = RbNaCl::PrivateKey.generate
73
+ pubk = privk.public_key
74
+ [PublicKey.from_binary(pubk.to_bytes),
75
+ PrivateKey.from_binary(privk.to_bytes)]
76
+ end
77
+
78
+ # Encrypt user private key.
79
+ #
80
+ # @param [SecretKey] secret_key secret key derived from passphrase
81
+ # @param [PrivateKey] privk private key
82
+ # @return [ArmoredValue] encrypted user private key
83
+ def encrypt_user_privk(secret_key, privk)
84
+ box_secret_encrypt(secret_key, privk.to_binary)
85
+ end
86
+
87
+ # Decrypt user private key.
88
+ #
89
+ # @param [SecretKey] secret_key secret key derived from passphrase
90
+ # @param [ArmoredValue] ciphertext encrypted private key
91
+ # @return [PrivateKey] user private key
92
+ def decrypt_user_privk(secret_key, ciphertext)
93
+ privk_data = box_secret_decrypt(secret_key, ciphertext)
94
+ PrivateKey.from_binary(privk_data)
95
+ end
96
+
97
+ # Encrypt and sign group private key.
98
+ #
99
+ # @param [PublicKey] owner_pubk user public key
100
+ # @param [PrivateKey] group_privk group private key
101
+ # @return [ArmoredValue] encrypted group private key
102
+ def encrypt_group_privk(owner_pubk, group_privk)
103
+ box_pair_encrypt(owner_pubk, group_privk, group_privk.to_binary)
104
+ end
105
+
106
+ # Decrypt and verify group private key.
107
+ #
108
+ # @param [PublicKey] group_pubk group public key to verify signature
109
+ # @param [PrivateKey] owner_privk user private key
110
+ # @param [ArmoredValue] encrypted_group_privk encrypted group private key
111
+ # @return [PrivateKey] group private key
112
+ def decrypt_group_privk(group_pubk, owner_privk, encrypted_group_privk)
113
+ PrivateKey.from_binary(
114
+ box_pair_decrypt(group_pubk, owner_privk, encrypted_group_privk)
115
+ )
116
+ end
117
+
118
+ # Encrypt and sign secret value.
119
+ #
120
+ # @param [PublicKey] group_pubk group public key
121
+ # @param [PrivateKey] author_privk user private key
122
+ # @param [String] value secret value
123
+ # @return [ArmoredValue] ASCII-armored encrypted secret value
124
+ def encrypt_secret_value(group_pubk, author_privk, value)
125
+ box_pair_encrypt(group_pubk, author_privk, value)
126
+ end
127
+
128
+ # Decrypt and verify secret value.
129
+ #
130
+ # @param [PublicKey] author_pubk user secret key
131
+ # @param [PrivateKey] group_privk group private key
132
+ # @param [ArmoredValue] val encrypted secret value
133
+ # @return [String] original secret value
134
+ def decrypt_secret_value(author_pubk, group_privk, val)
135
+ box_pair_decrypt(author_pubk, group_privk, val)
136
+ end
137
+
138
+ # Encrypt and sign secret metadata.
139
+ #
140
+ # @param [PublicKey] group_pubk group public key
141
+ # @param [PrivateKey] author_privk user private key
142
+ # @param [Hash] metadata_as_hash Hash of keys and values
143
+ # @return [ArmoredValue] encrypted secret metadata
144
+ def encrypt_secret_metadata(group_pubk, author_privk, metadata_as_hash)
145
+ meta = ArmoredHash.from_hash(metadata_as_hash)
146
+ encrypt_secret_value(group_pubk, author_privk, meta.to_binary)
147
+ end
148
+
149
+ # Decrypt and verify secret metadata.
150
+ #
151
+ # @param [PublicKey] author_pubk user public key
152
+ # @param [PrivateKey] group_privk group private key
153
+ # @param [ArmoredValue] val encrypted secret metadata
154
+ # @return [Hash] Hash of keys and values
155
+ def decrypt_secret_metadata(author_pubk, group_privk, val)
156
+ bytes = decrypt_secret_value(author_pubk, group_privk, val)
157
+ ArmoredHash.from_binary(bytes).to_hash
158
+ end
159
+
160
+ private
161
+
162
+ # Encrypt raw data with a secret key.
163
+ #
164
+ # @param [SecretKey] secret_key secret key
165
+ # @param [String] bytes raw data to encrypt
166
+ # @return [ArmoredValue] ciphertext
167
+ def box_secret_encrypt(secret_key, bytes)
168
+ ciphertext = make_secret_box(secret_key).encrypt(bytes)
169
+ ArmoredValue.from_binary(ciphertext)
170
+ rescue RbNaCl::CryptoError => e
171
+ raise Error.for_code('CRYPTO/RBNACL', e.message)
172
+ end
173
+
174
+ # Decrypt data with a secret key.
175
+ #
176
+ # @param [SecretKey] secret_key secret key
177
+ # @param [ArmoredValue] val ciphertext to decrypt
178
+ # @return [String] raw decrypted plaintext
179
+ def box_secret_decrypt(secret_key, val)
180
+ make_secret_box(secret_key).decrypt(val.to_binary)
181
+ rescue RbNaCl::CryptoError => e
182
+ raise Error.for_code('CRYPTO/RBNACL', e.message)
183
+ end
184
+
185
+ # Encrypt and authenticate data with public-key crypto.
186
+ #
187
+ # @param [PublicKey] pubk public key
188
+ # @param [PrivateKey] privk private key
189
+ # @param [String] bytes raw data to encrypt
190
+ # @return [ArmoredValue] ciphertext
191
+ def box_pair_encrypt(pubk, privk, bytes)
192
+ ArmoredValue.from_binary(make_pair_box(pubk, privk).encrypt(bytes))
193
+ rescue RbNaCl::CryptoError => e
194
+ raise Error.for_code('CRYPTO/RBNACL', e.message)
195
+ end
196
+
197
+ # Decrypt and authentify data with public-key crypto.
198
+ #
199
+ # @param [PublicKey] pubk public key
200
+ # @param [PrivateKey] privk private key
201
+ # @param [ArmoredValue] val ciphertext to decrypt
202
+ # @return [String] raw decrypted plaintext
203
+ def box_pair_decrypt(pubk, privk, val)
204
+ make_pair_box(pubk, privk).decrypt(val.to_binary)
205
+ rescue RbNaCl::CryptoError => e
206
+ raise Error.for_code('CRYPTO/RBNACL', e.message)
207
+ end
208
+
209
+ # Make a SimpleBox for symetric crypto.
210
+ #
211
+ # @param secret_key [SecretKey] secret_key secret key
212
+ # @return [RbNaCl::SimpleBox] the box
213
+ def make_secret_box(secret_key)
214
+ RbNaCl::SimpleBox.from_secret_key(secret_key.value.to_binary)
215
+ end
216
+
217
+ # Make a SimpleBox for asymetric cypto.
218
+ #
219
+ # @param [PublicKey] pubk public key
220
+ # @param [PrivateKey] privk private key
221
+ # @return [RbNaCl::SimpleBox] the box
222
+ def make_pair_box(pubk, privk)
223
+ RbNaCl::SimpleBox.from_keypair(pubk.to_binary, privk.to_binary)
224
+ end
225
+
226
+ # Generate new parameters for the Key Derivation Function.
227
+ #
228
+ # @return [KDFParams] newly generated KDF parameters
229
+ def key_derive_params_generate
230
+ salt = RbNaCl::Random.random_bytes(
231
+ RbNaCl::PasswordHash::Argon2::SALTBYTES
232
+ )
233
+ h = { '_version' => VERSION, 'salt' => salt,
234
+ 'opslimit' => :moderate, 'memlimit' => :moderate,
235
+ 'digest_size' => RbNaCl::SecretBox.key_bytes }
236
+ KDFParams.from_hash(h)
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kstor/message'
4
+
5
+ module KStor
6
+ # Base class of KStor errors.
7
+ #
8
+ # Each subclass declares a code and is stored in a global registry.
9
+ class Error < StandardError
10
+ class << self
11
+ attr_reader :code
12
+ attr_reader :message
13
+ attr_reader :registry
14
+
15
+ def error_code(str)
16
+ @code = str
17
+ end
18
+
19
+ def error_message(str)
20
+ @message = str
21
+ end
22
+
23
+ def for_code(code, *args)
24
+ @registry[code].new(*args)
25
+ end
26
+
27
+ def list
28
+ @registry.classes
29
+ end
30
+ end
31
+
32
+ def self.inherited(subclass)
33
+ super
34
+ Log.debug("#{subclass} inherits from Error")
35
+ @registry ||= ErrorRegistry.new
36
+ if @registry.key?(subclass.code)
37
+ code = subclass.code
38
+ klass = @registry[code]
39
+ raise "duplicate error code #{code} in #{subclass}, " \
40
+ "already defined in #{klass}"
41
+ end
42
+
43
+ @registry << subclass
44
+ end
45
+
46
+ def initialize(*args)
47
+ super(format("ERR/%s #{self.class.message}", self.class.code, *args))
48
+ end
49
+
50
+ def response
51
+ Response.new('error', 'code' => self.class.code, 'message' => message)
52
+ end
53
+ end
54
+
55
+ # @api private
56
+ class ErrorRegistry
57
+ def initialize
58
+ @error_classes = []
59
+ @index = nil
60
+ end
61
+
62
+ def classes
63
+ @error_classes.values
64
+ end
65
+
66
+ def <<(klass)
67
+ @error_classes << klass
68
+ @index = nil
69
+ end
70
+
71
+ def key?(code)
72
+ index.key?(code)
73
+ end
74
+
75
+ def [](code)
76
+ index[code]
77
+ end
78
+
79
+ private
80
+
81
+ def index
82
+ @index ||= @error_classes.to_h { |c| [c.code, c] }
83
+ end
84
+ end
85
+ end
data/lib/kstor/log.rb ADDED
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'journald/logger'
4
+
5
+ module KStor
6
+ # Central logging to systemd-journald.
7
+ module Log
8
+ class << self
9
+ def exception(exc)
10
+ logger.exception(exc)
11
+ end
12
+
13
+ def debug(msg)
14
+ logger.debug(msg)
15
+ end
16
+
17
+ def info(msg)
18
+ logger.info(msg)
19
+ end
20
+
21
+ def notice(msg)
22
+ logger.notice(msg)
23
+ end
24
+
25
+ def warn(msg)
26
+ logger.warn(msg)
27
+ end
28
+
29
+ def error(msg)
30
+ logger.error(msg)
31
+ end
32
+
33
+ def critical(msg)
34
+ logger.critical(msg)
35
+ end
36
+
37
+ def alert(msg)
38
+ logger.alert(msg)
39
+ end
40
+
41
+ def emergency(msg)
42
+ logger.emergency(msg)
43
+ end
44
+
45
+ def reporting_level=(lvl)
46
+ logger.min_priority = lvl
47
+ end
48
+
49
+ private
50
+
51
+ def logger
52
+ @logger ||= Journald::Logger.new('kstor')
53
+ end
54
+ end
55
+ end
56
+ end