kstor 0.4.0

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