cryptor 0.0.2 → 1.0.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 +4 -4
- data/CHANGES.md +5 -0
- data/README.md +41 -5
- data/lib/cryptor.rb +2 -2
- data/lib/cryptor/symmetric_encryption.rb +44 -13
- data/lib/cryptor/symmetric_encryption/cipher.rb +39 -0
- data/lib/cryptor/symmetric_encryption/ciphers/message_encryptor.rb +50 -0
- data/lib/cryptor/symmetric_encryption/ciphers/xsalsa20poly1305.rb +30 -0
- data/lib/cryptor/symmetric_encryption/keyring.rb +22 -0
- data/lib/cryptor/symmetric_encryption/secret_key.rb +91 -0
- data/lib/cryptor/version.rb +1 -1
- data/spec/cryptor/{secret_key_spec.rb → symmetric_encryption/secret_key_spec.rb} +3 -3
- data/spec/symmetric_encryption_spec.rb +37 -2
- metadata +9 -8
- data/lib/cryptor/cipher.rb +0 -37
- data/lib/cryptor/ciphers/message_encryptor.rb +0 -48
- data/lib/cryptor/ciphers/xsalsa20poly1305.rb +0 -28
- data/lib/cryptor/secret_key.rb +0 -83
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f5fd951f1826bc584bac34d75f61ed7a75af3875
|
4
|
+
data.tar.gz: e89c7a58275dac153b47b2201fecee4194f0ad72
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fde5ba8749bc1c6fa79ff27a5e8e3fcc733abfd8d164afbdd7cca3984371ce18f1a6e6d372f7c4080dd1e9945366d0d69f960fd835e8e87f1d03f34f6a85f69d
|
7
|
+
data.tar.gz: 26c0c7f57afb309c8d993121e7d2d680f0edfd5b7c4485daed7024d7445186741637e85d7958334bfa97ddab2b5e70c7982049899f10ae8107bcd2e4e88459d8
|
data/CHANGES.md
CHANGED
data/README.md
CHANGED
@@ -20,7 +20,8 @@ Cryptor supports two backends:
|
|
20
20
|
encryption scheme provided by Rails, based on AES-CBC and HMAC.
|
21
21
|
|
22
22
|
Cryptor uses the experimental [ORDO v0 message format][ordo] for serializing
|
23
|
-
encrypted messages.
|
23
|
+
encrypted messages. Future versions may support additional message formats
|
24
|
+
like OpenPGP or JWE.
|
24
25
|
|
25
26
|
[authenticated encryption]: https://en.wikipedia.org/wiki/Authenticated_encryption
|
26
27
|
[RbNaCl::SimpleBox]: https://github.com/cryptosphere/rbnacl/wiki/SimpleBox
|
@@ -48,7 +49,7 @@ To begin with, you must select a backend:
|
|
48
49
|
|
49
50
|
### RbNaCl (recommended)
|
50
51
|
|
51
|
-
RbNaCl is a Ruby FFI binding to libsodium, a portable state-of-the-art
|
52
|
+
[RbNaCl] is a Ruby FFI binding to libsodium, a portable state-of-the-art
|
52
53
|
cryptography library.
|
53
54
|
|
54
55
|
To use Cryptor with RbNaCl, add the following to your Gemfile:
|
@@ -61,8 +62,9 @@ And in your Ruby program, require the following:
|
|
61
62
|
|
62
63
|
```ruby
|
63
64
|
require 'cryptor'
|
64
|
-
require 'cryptor/ciphers/xsalsa20poly1305'
|
65
|
+
require 'cryptor/symmetric_encryption/ciphers/xsalsa20poly1305'
|
65
66
|
```
|
67
|
+
[RbNaCl]: https://github.com/cryptosphere/rbnacl/
|
66
68
|
|
67
69
|
### Rails (ActiveSupport::MessageEncryptor)
|
68
70
|
|
@@ -79,7 +81,7 @@ from a Rails 4.0+ app or other app with ActiveSupport 4.0+ bundled:
|
|
79
81
|
|
80
82
|
```ruby
|
81
83
|
require 'cryptor'
|
82
|
-
require 'cryptor/ciphers/message_encryptor'
|
84
|
+
require 'cryptor/symmetric_encryption/ciphers/message_encryptor'
|
83
85
|
```
|
84
86
|
|
85
87
|
### Authenticated Symmetric Encryption
|
@@ -113,7 +115,8 @@ key's [ORDO secret URI].
|
|
113
115
|
To obtain the secret URI, use the `#to_secret_uri` method, which returns a string:
|
114
116
|
|
115
117
|
```ruby
|
116
|
-
|
118
|
+
>> secret_key.to_secret_uri
|
119
|
+
=> "secret.key:///xsalsa20poly1305;0saB1tfgKWDh_bX0oAquLWgAq-6yjG1u04mP-CtQG-4"
|
117
120
|
```
|
118
121
|
|
119
122
|
This string can be saved somewhere secret and safe then later loaded and passed into
|
@@ -138,6 +141,39 @@ decrypted = cryptor.decrypt(ciphertext)
|
|
138
141
|
[RFC 6920]: http://tools.ietf.org/html/rfc6920
|
139
142
|
[ORDO secret URI]: https://github.com/cryptosphere/ordo/wiki/URI-Registry
|
140
143
|
|
144
|
+
## Key Rotation
|
145
|
+
|
146
|
+
Cryptor is designed to support key rotation, allowing new ciphertexts to be
|
147
|
+
produced under an "active" key, but with old keys configured so older
|
148
|
+
ciphertexts can still be decrypted (and also rotated to the new key).
|
149
|
+
|
150
|
+
To rotate keys, first make a new key, but configure Cryptor with the old key
|
151
|
+
too using the "keyring" option:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
old_key = ...
|
155
|
+
new_key = Cryptor::SymmetricEncryption.random_key(:xsalsa20poly1305)
|
156
|
+
cryptor = Cryptor::SymmetricEncryption.new(new_key, keyring: [old_key])
|
157
|
+
```
|
158
|
+
|
159
|
+
Cryptor can support arbitrarily many old keys on its keyring. Any messages
|
160
|
+
which have been encrypted under the old keys can still be decrypted, but
|
161
|
+
newly encrypted messages will always use the new "active" key.
|
162
|
+
|
163
|
+
To rotate messages from one key to another, use the `#rotate` method:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
old_message = ...
|
167
|
+
new_message = cryptor.rotate(old_message)
|
168
|
+
```
|
169
|
+
|
170
|
+
This is useful if a key is ever compromised, and also good security hygene
|
171
|
+
in general.
|
172
|
+
|
173
|
+
Cryptor also supports the `#rotate!` method, which works just like `#rotate`,
|
174
|
+
but raises `Cryptor::AlreadyRotatedError` if asked to rotate a message that's
|
175
|
+
already up-to-date.
|
176
|
+
|
141
177
|
## Contributing
|
142
178
|
|
143
179
|
* Fork this repository on Github
|
data/lib/cryptor.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
require 'cryptor/version'
|
2
2
|
|
3
|
-
require 'cryptor/cipher'
|
4
3
|
require 'cryptor/encoding'
|
5
|
-
require 'cryptor/secret_key'
|
6
4
|
require 'cryptor/symmetric_encryption'
|
7
5
|
|
8
6
|
# Multi-backend high-level encryption library
|
@@ -11,4 +9,6 @@ module Cryptor
|
|
11
9
|
|
12
10
|
InvalidMessageError = Class.new(CryptoError)
|
13
11
|
CorruptedMessageError = Class.new(CryptoError)
|
12
|
+
KeyNotFoundError = Class.new(CryptoError)
|
13
|
+
AlreadyRotatedError = Class.new(CryptoError)
|
14
14
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'ordo'
|
2
2
|
|
3
3
|
require 'cryptor/version'
|
4
|
-
require 'cryptor/cipher'
|
4
|
+
require 'cryptor/symmetric_encryption/cipher'
|
5
|
+
require 'cryptor/symmetric_encryption/keyring'
|
6
|
+
require 'cryptor/symmetric_encryption/secret_key'
|
5
7
|
|
6
8
|
module Cryptor
|
7
9
|
# Easy-to-use authenticated symmetric encryption
|
@@ -10,34 +12,63 @@ module Cryptor
|
|
10
12
|
Cipher[cipher].random_key
|
11
13
|
end
|
12
14
|
|
13
|
-
def initialize(
|
14
|
-
@
|
15
|
+
def initialize(active_key, options = {})
|
16
|
+
@active_key = active_key.is_a?(SecretKey) ? active_key : SecretKey.new(active_key)
|
17
|
+
@keyring = nil
|
18
|
+
|
19
|
+
options.each do |name, value|
|
20
|
+
if name == :keyring
|
21
|
+
@keyring = Keyring.new(@active_key, *value)
|
22
|
+
else fail ArgumentError, "unknown option: #{name}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@keyring ||= Keyring.new(active_key)
|
15
27
|
end
|
16
28
|
|
17
29
|
def encrypt(plaintext)
|
18
|
-
ciphertext = @
|
30
|
+
ciphertext = @active_key.encrypt(plaintext)
|
19
31
|
base64 = Base64.strict_encode64(ciphertext)
|
20
32
|
|
21
33
|
ORDO::Message.new(
|
22
34
|
base64,
|
23
|
-
'Cipher' => @
|
35
|
+
'Cipher' => @active_key.cipher.algorithm,
|
24
36
|
'Content-Length' => base64.bytesize,
|
25
37
|
'Content-Transfer-Encoding' => 'base64',
|
26
|
-
'Key-Fingerprint' => @
|
38
|
+
'Key-Fingerprint' => @active_key.fingerprint
|
27
39
|
).to_string
|
28
40
|
end
|
29
41
|
|
30
42
|
def decrypt(ciphertext)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
43
|
+
message = parse(ciphertext)
|
44
|
+
fingerprint = message['Key-Fingerprint']
|
45
|
+
fail InvalidMessageError, 'no key fingerprint in message' unless fingerprint
|
46
|
+
|
47
|
+
key = @keyring[fingerprint]
|
48
|
+
key.decrypt message.body
|
49
|
+
end
|
36
50
|
|
51
|
+
def rotate!(ciphertext)
|
52
|
+
message = parse(ciphertext)
|
37
53
|
fingerprint = message['Key-Fingerprint']
|
38
|
-
fail
|
54
|
+
fail AlreadyRotatedError, 'already current' if fingerprint == @active_key.fingerprint
|
55
|
+
|
56
|
+
key = @keyring[fingerprint]
|
57
|
+
encrypt(key.decrypt(message.body))
|
58
|
+
end
|
59
|
+
|
60
|
+
def rotate(ciphertext)
|
61
|
+
rotate!(ciphertext)
|
62
|
+
rescue AlreadyRotatedError
|
63
|
+
ciphertext
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
39
67
|
|
40
|
-
|
68
|
+
def parse(message)
|
69
|
+
ORDO::Message.parse(message)
|
70
|
+
rescue ORDO::ParseError => ex
|
71
|
+
raise InvalidMessageError, ex.to_s
|
41
72
|
end
|
42
73
|
end
|
43
74
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Cryptor
|
2
|
+
class SymmetricEncryption
|
3
|
+
# Base class of all Cryptor::SymmetricEncryption ciphers
|
4
|
+
class Cipher
|
5
|
+
REGISTRY = {}
|
6
|
+
|
7
|
+
attr_reader :algorithm, :key_bytes
|
8
|
+
|
9
|
+
def self.register(algorithm, options = {})
|
10
|
+
REGISTRY[algorithm.to_s] ||= new(algorithm, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.[](algorithm)
|
14
|
+
REGISTRY[algorithm.to_s] || fail(ArgumentError, "no such cipher: #{algorithm}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(algorithm, options = {})
|
18
|
+
@algorithm = algorithm
|
19
|
+
@key_bytes = options[:key_bytes] || fail(ArgumentError, 'key_bytes not specified')
|
20
|
+
end
|
21
|
+
|
22
|
+
def random_key
|
23
|
+
SecretKey.random_key(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
def encrypt(_key, _plaintext)
|
27
|
+
#:nocov:
|
28
|
+
fail NotImplementedError, "'encrypt' method has not been implemented"
|
29
|
+
#:nocov:
|
30
|
+
end
|
31
|
+
|
32
|
+
def decrypt(_key, _ciphertext)
|
33
|
+
#:nocov:
|
34
|
+
fail NotImplementedError, "'decrypt' method has not been implemented"
|
35
|
+
#:nocov:
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'active_support/message_encryptor'
|
2
|
+
require 'active_support/message_verifier'
|
3
|
+
|
4
|
+
require 'cryptor/symmetric_encryption/cipher'
|
5
|
+
|
6
|
+
module Cryptor
|
7
|
+
class SymmetricEncryption
|
8
|
+
module Ciphers
|
9
|
+
# MessageEncryptor is a bespoke authenticated encryption scheme invented
|
10
|
+
# by rails-core. It uses AES-256-CBC and HMAC-SHA1 in an encrypt-then-MAC
|
11
|
+
# scheme with a wacky and wild semiconstant-time MAC comparison.
|
12
|
+
|
13
|
+
# Cryptor enforces the usage of independent keys for AES encryption and HMAC
|
14
|
+
# by mandating a 64-byte key (using 32-bytes for AES and 32-bytes for HMAC).
|
15
|
+
#
|
16
|
+
# This scheme is probably safe to use, but less interoperable and more
|
17
|
+
# poorly designed than xsalsa20poly1305 from RbNaCl. It does, however,
|
18
|
+
# work using only ActiveSupport and the Ruby OpenSSL extension as
|
19
|
+
# dependencies, and should be available anywhere.
|
20
|
+
#
|
21
|
+
# For the time being, this scheme is only supported for ActiveSupport 4.0+
|
22
|
+
# although support for earlier versions of ActiveSupport should be
|
23
|
+
# possible.
|
24
|
+
class MessageEncryptor < Cipher
|
25
|
+
SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
|
26
|
+
KEY_BYTES = 64
|
27
|
+
|
28
|
+
register :message_encryptor, key_bytes: KEY_BYTES
|
29
|
+
|
30
|
+
def encrypt(key, plaintext)
|
31
|
+
encryptor(key).encrypt_and_sign(plaintext)
|
32
|
+
end
|
33
|
+
|
34
|
+
def decrypt(key, ciphertext)
|
35
|
+
encryptor(key).decrypt_and_verify(ciphertext)
|
36
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature => ex
|
37
|
+
raise CorruptedMessageError, ex.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def encryptor(key)
|
43
|
+
fail ArgumentError, "wrong key size: #{key.bytesize}" unless key.bytesize == KEY_BYTES
|
44
|
+
encryption_key, hmac_key = key[0, 32], key[32, 32]
|
45
|
+
ActiveSupport::MessageEncryptor.new(encryption_key, hmac_key, serializer: SERIALIZER)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rbnacl/libsodium'
|
2
|
+
|
3
|
+
require 'cryptor/symmetric_encryption/cipher'
|
4
|
+
|
5
|
+
module Cryptor
|
6
|
+
class SymmetricEncryption
|
7
|
+
module Ciphers
|
8
|
+
# XSalsa20+Poly1305 authenticated stream cipher
|
9
|
+
class XSalsa20Poly1305 < Cipher
|
10
|
+
register :xsalsa20poly1305, key_bytes: RbNaCl::SecretBoxes::XSalsa20Poly1305.key_bytes
|
11
|
+
|
12
|
+
def encrypt(key, plaintext)
|
13
|
+
box(key).encrypt(plaintext)
|
14
|
+
end
|
15
|
+
|
16
|
+
def decrypt(key, ciphertext)
|
17
|
+
box(key).decrypt(ciphertext)
|
18
|
+
rescue RbNaCl::CryptoError => ex
|
19
|
+
raise CorruptedMessageError, ex.to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def box(key)
|
25
|
+
RbNaCl::SimpleBox.new(RbNaCl::SecretBoxes::XSalsa20Poly1305.new(key))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'cryptor/symmetric_encryption/secret_key'
|
2
|
+
|
3
|
+
module Cryptor
|
4
|
+
class SymmetricEncryption
|
5
|
+
# Stores multiple keys for the purposes of key rotation
|
6
|
+
class Keyring
|
7
|
+
def initialize(*keys)
|
8
|
+
@keys = {}
|
9
|
+
keys.each do |key|
|
10
|
+
key = SecretKey.new(key) if key.is_a? String
|
11
|
+
fail TypeError, "not a valid secret key: #{key.inspect}" unless key.is_a? SecretKey
|
12
|
+
@keys[key.fingerprint] = key
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def find(fingerprint)
|
17
|
+
@keys[fingerprint] || fail(KeyNotFoundError, "no key for fingerprint: #{fingerprint}")
|
18
|
+
end
|
19
|
+
alias_method :[], :find
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'digest/sha2'
|
3
|
+
|
4
|
+
module Cryptor
|
5
|
+
class SymmetricEncryption
|
6
|
+
# Secret key used to encrypt plaintexts
|
7
|
+
class SecretKey
|
8
|
+
attr_reader :cipher
|
9
|
+
|
10
|
+
# Generate a random secret key
|
11
|
+
#
|
12
|
+
# @param [Cryptor::Cipher, Symbol] Cryptor::Cipher or algorithm name as a symbol
|
13
|
+
#
|
14
|
+
# @return [Cryptor::SecretKey] new secret key object
|
15
|
+
def self.random_key(cipher)
|
16
|
+
case cipher
|
17
|
+
when Cryptor::SymmetricEncryption::Cipher
|
18
|
+
# we're good
|
19
|
+
when Symbol
|
20
|
+
cipher = Cryptor::SymmetricEncryption::Cipher[cipher]
|
21
|
+
else fail ArgumentError, "invalid cipher: #{cipher}"
|
22
|
+
end
|
23
|
+
|
24
|
+
bytes = RbNaCl::Random.random_bytes(cipher.key_bytes)
|
25
|
+
base64 = Cryptor::Encoding.encode(bytes)
|
26
|
+
|
27
|
+
new "secret.key:///#{cipher.algorithm};#{base64}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a new SecretKey object from a URI
|
31
|
+
#
|
32
|
+
# @param [#to_s] uri representing a secret key
|
33
|
+
#
|
34
|
+
# @raise [ArgumentError] on invalid URIs
|
35
|
+
#
|
36
|
+
# @return [Cryptor::SecretKey] new secret key object
|
37
|
+
def initialize(uri_string)
|
38
|
+
uri = URI.parse(uri_string.to_s)
|
39
|
+
fail ArgumentError, "invalid scheme: #{uri.scheme}" unless uri.scheme == 'secret.key'
|
40
|
+
|
41
|
+
components = uri.path.match(/^\/([^;]+);(.+)$/)
|
42
|
+
fail ArgumentError, "couldn't parse cipher name from secret URI" unless components
|
43
|
+
|
44
|
+
@cipher = Cryptor::SymmetricEncryption::Cipher[components[1]]
|
45
|
+
@secret_key = Cryptor::Encoding.decode(components[2])
|
46
|
+
end
|
47
|
+
|
48
|
+
# Serialize SecretKey object to a URI
|
49
|
+
#
|
50
|
+
# @return [String] serialized URI representing the key
|
51
|
+
def to_secret_uri
|
52
|
+
"secret.key:///#{@cipher.algorithm};#{Cryptor::Encoding.encode(@secret_key)}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Fingerprint of this key's secret URI
|
56
|
+
#
|
57
|
+
# @return [String] fingerprint as a ni:// URL
|
58
|
+
def fingerprint
|
59
|
+
digest = Digest::SHA256.digest(to_secret_uri)
|
60
|
+
"ni:///sha-256;#{Cryptor::Encoding.encode(digest)}"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Encrypt a plaintext under this key
|
64
|
+
#
|
65
|
+
# @param [String] plaintext string to be encrypted
|
66
|
+
#
|
67
|
+
# @return [String] ciphertext encrypted under this key
|
68
|
+
def encrypt(plaintext)
|
69
|
+
@cipher.encrypt(@secret_key, plaintext)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Decrypt ciphertext using this key
|
73
|
+
#
|
74
|
+
# @param [String] ciphertext string to be decrypted
|
75
|
+
#
|
76
|
+
# @return [String] plaintext decrypted from the given ciphertext
|
77
|
+
def decrypt(ciphertext)
|
78
|
+
@cipher.decrypt(@secret_key, ciphertext)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Inspect this key
|
82
|
+
#
|
83
|
+
# @return [String] a string representing this key
|
84
|
+
def inspect
|
85
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} " \
|
86
|
+
"cipher=#{cipher.algorithm} " \
|
87
|
+
"fingerprint=#{fingerprint}>"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/cryptor/version.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe Cryptor::SecretKey do
|
3
|
+
describe Cryptor::SymmetricEncryption::SecretKey do
|
4
4
|
let(:algorithm) { :BassOmatic }
|
5
5
|
let(:key_bytes) { 42 }
|
6
|
-
let(:cipher) { Cryptor::Cipher.new(algorithm, key_bytes: key_bytes) }
|
6
|
+
let(:cipher) { Cryptor::SymmetricEncryption::Cipher.new(algorithm, key_bytes: key_bytes) }
|
7
7
|
let(:secret_key) { "\xBA\x55" }
|
8
8
|
let(:secret_uri) { "secret.key:///#{algorithm};#{Cryptor::Encoding.encode(secret_key)}" }
|
9
9
|
|
10
10
|
before do
|
11
|
-
allow(Cryptor::Cipher).to receive(:[]).and_return(cipher)
|
11
|
+
allow(Cryptor::SymmetricEncryption::Cipher).to receive(:[]).and_return(cipher)
|
12
12
|
end
|
13
13
|
|
14
14
|
subject { described_class.new(secret_uri) }
|
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
# "Default" cipher used in non-backend specific tests
|
4
|
+
require 'cryptor/symmetric_encryption/ciphers/xsalsa20poly1305'
|
5
|
+
|
3
6
|
describe Cryptor::SymmetricEncryption do
|
4
7
|
let(:plaintext) { 'THE MAGIC WORDS ARE SQUEAMISH OSSIFRAGE' }
|
5
8
|
|
@@ -13,7 +16,7 @@ describe Cryptor::SymmetricEncryption do
|
|
13
16
|
subject { described_class.new(secret_key) }
|
14
17
|
|
15
18
|
context 'xsalsa20poly1305' do
|
16
|
-
require 'cryptor/ciphers/xsalsa20poly1305'
|
19
|
+
require 'cryptor/symmetric_encryption/ciphers/xsalsa20poly1305'
|
17
20
|
|
18
21
|
let(:secret_key) { described_class.random_key(:xsalsa20poly1305) }
|
19
22
|
|
@@ -38,7 +41,7 @@ describe Cryptor::SymmetricEncryption do
|
|
38
41
|
end
|
39
42
|
|
40
43
|
context 'message_encryptor' do
|
41
|
-
require 'cryptor/ciphers/message_encryptor'
|
44
|
+
require 'cryptor/symmetric_encryption/ciphers/message_encryptor'
|
42
45
|
|
43
46
|
let(:secret_key) { described_class.random_key(:message_encryptor) }
|
44
47
|
|
@@ -61,4 +64,36 @@ describe Cryptor::SymmetricEncryption do
|
|
61
64
|
end.to raise_exception(Cryptor::CorruptedMessageError)
|
62
65
|
end
|
63
66
|
end
|
67
|
+
|
68
|
+
context 'key rotation' do
|
69
|
+
let(:old_key) { described_class.random_key(:xsalsa20poly1305) }
|
70
|
+
let(:new_key) { described_class.random_key(:xsalsa20poly1305) }
|
71
|
+
let(:another_key) { described_class.random_key(:xsalsa20poly1305) }
|
72
|
+
|
73
|
+
it 'decrypts messages under old keys' do
|
74
|
+
old_cryptor = described_class.new(old_key, keyring: [old_key, another_key])
|
75
|
+
message = old_cryptor.encrypt(plaintext)
|
76
|
+
|
77
|
+
new_cryptor = described_class.new(new_key, keyring: [new_key, old_key])
|
78
|
+
expect(new_cryptor.decrypt(message)).to eq plaintext
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'rotates messages encrypted under old keys to the active key' do
|
82
|
+
old_cryptor = described_class.new(old_key, keyring: [old_key, another_key])
|
83
|
+
old_message = old_cryptor.encrypt(plaintext)
|
84
|
+
|
85
|
+
hybrid_cryptor = described_class.new(new_key, keyring: [new_key, old_key])
|
86
|
+
new_message = hybrid_cryptor.rotate(old_message)
|
87
|
+
|
88
|
+
new_cryptor = described_class.new(new_key)
|
89
|
+
expect(new_cryptor.decrypt(new_message)).to eq plaintext
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'raises AlreadyRotatedError on up-to-date messages if called with a bang' do
|
93
|
+
cryptor = described_class.new(new_key)
|
94
|
+
message = cryptor.encrypt(plaintext)
|
95
|
+
|
96
|
+
expect { cryptor.rotate!(message) }.to raise_exception(Cryptor::AlreadyRotatedError)
|
97
|
+
end
|
98
|
+
end
|
64
99
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cryptor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Arcieri
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ordo
|
@@ -115,14 +115,15 @@ files:
|
|
115
115
|
- cryptor.gemspec
|
116
116
|
- cryptosaur.png
|
117
117
|
- lib/cryptor.rb
|
118
|
-
- lib/cryptor/cipher.rb
|
119
|
-
- lib/cryptor/ciphers/message_encryptor.rb
|
120
|
-
- lib/cryptor/ciphers/xsalsa20poly1305.rb
|
121
118
|
- lib/cryptor/encoding.rb
|
122
|
-
- lib/cryptor/secret_key.rb
|
123
119
|
- lib/cryptor/symmetric_encryption.rb
|
120
|
+
- lib/cryptor/symmetric_encryption/cipher.rb
|
121
|
+
- lib/cryptor/symmetric_encryption/ciphers/message_encryptor.rb
|
122
|
+
- lib/cryptor/symmetric_encryption/ciphers/xsalsa20poly1305.rb
|
123
|
+
- lib/cryptor/symmetric_encryption/keyring.rb
|
124
|
+
- lib/cryptor/symmetric_encryption/secret_key.rb
|
124
125
|
- lib/cryptor/version.rb
|
125
|
-
- spec/cryptor/secret_key_spec.rb
|
126
|
+
- spec/cryptor/symmetric_encryption/secret_key_spec.rb
|
126
127
|
- spec/spec_helper.rb
|
127
128
|
- spec/symmetric_encryption_spec.rb
|
128
129
|
- tasks/rspec.rake
|
@@ -152,6 +153,6 @@ signing_key:
|
|
152
153
|
specification_version: 4
|
153
154
|
summary: An easy-to-use library for real-world Ruby cryptography
|
154
155
|
test_files:
|
155
|
-
- spec/cryptor/secret_key_spec.rb
|
156
|
+
- spec/cryptor/symmetric_encryption/secret_key_spec.rb
|
156
157
|
- spec/spec_helper.rb
|
157
158
|
- spec/symmetric_encryption_spec.rb
|
data/lib/cryptor/cipher.rb
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
module Cryptor
|
2
|
-
# Base class of all Cryptor ciphers
|
3
|
-
class Cipher
|
4
|
-
REGISTRY = {}
|
5
|
-
|
6
|
-
attr_reader :algorithm, :key_bytes
|
7
|
-
|
8
|
-
def self.register(algorithm, options = {})
|
9
|
-
REGISTRY[algorithm.to_s] ||= new(algorithm, options)
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.[](algorithm)
|
13
|
-
REGISTRY[algorithm.to_s] || fail(ArgumentError, "no such cipher: #{algorithm}")
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(algorithm, options = {})
|
17
|
-
@algorithm = algorithm
|
18
|
-
@key_bytes = options[:key_bytes] || fail(ArgumentError, 'key_bytes not specified')
|
19
|
-
end
|
20
|
-
|
21
|
-
def random_key
|
22
|
-
SecretKey.random_key(self)
|
23
|
-
end
|
24
|
-
|
25
|
-
def encrypt(_key, _plaintext)
|
26
|
-
#:nocov:
|
27
|
-
fail NotImplementedError, "'encrypt' method has not been implemented"
|
28
|
-
#:nocov:
|
29
|
-
end
|
30
|
-
|
31
|
-
def decrypt(_key, _ciphertext)
|
32
|
-
#:nocov:
|
33
|
-
fail NotImplementedError, "'decrypt' method has not been implemented"
|
34
|
-
#:nocov:
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
require 'active_support/message_encryptor'
|
2
|
-
require 'active_support/message_verifier'
|
3
|
-
|
4
|
-
require 'cryptor/cipher'
|
5
|
-
|
6
|
-
module Cryptor
|
7
|
-
module Ciphers
|
8
|
-
# MessageEncryptor is a bespoke authenticated encryption scheme invented
|
9
|
-
# by rails-core. It uses AES-256-CBC and HMAC-SHA1 in an encrypt-then-MAC
|
10
|
-
# scheme with a wacky and wild semiconstant-time MAC comparison.
|
11
|
-
|
12
|
-
# Cryptor enforces the usage of independent keys for AES encryption and HMAC
|
13
|
-
# by mandating a 64-byte key (using 32-bytes for AES and 32-bytes for HMAC).
|
14
|
-
#
|
15
|
-
# This scheme is probably safe to use, but less interoperable and more
|
16
|
-
# poorly designed than xsalsa20poly1305 from RbNaCl. It does, however,
|
17
|
-
# work using only ActiveSupport and the Ruby OpenSSL extension as
|
18
|
-
# dependencies, and should be available anywhere.
|
19
|
-
#
|
20
|
-
# For the time being, this scheme is only supported for ActiveSupport 4.0+
|
21
|
-
# although support for earlier versions of ActiveSupport should be
|
22
|
-
# possible.
|
23
|
-
class MessageEncryptor < Cipher
|
24
|
-
SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
|
25
|
-
KEY_BYTES = 64
|
26
|
-
|
27
|
-
register :message_encryptor, key_bytes: KEY_BYTES
|
28
|
-
|
29
|
-
def encrypt(key, plaintext)
|
30
|
-
encryptor(key).encrypt_and_sign(plaintext)
|
31
|
-
end
|
32
|
-
|
33
|
-
def decrypt(key, ciphertext)
|
34
|
-
encryptor(key).decrypt_and_verify(ciphertext)
|
35
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature => ex
|
36
|
-
raise CorruptedMessageError, ex.to_s
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
def encryptor(key)
|
42
|
-
fail ArgumentError, "wrong key size: #{key.bytesize}" unless key.bytesize == KEY_BYTES
|
43
|
-
encryption_key, hmac_key = key[0, 32], key[32, 32]
|
44
|
-
ActiveSupport::MessageEncryptor.new(encryption_key, hmac_key, serializer: SERIALIZER)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
@@ -1,28 +0,0 @@
|
|
1
|
-
require 'rbnacl/libsodium'
|
2
|
-
|
3
|
-
require 'cryptor/cipher'
|
4
|
-
|
5
|
-
module Cryptor
|
6
|
-
module Ciphers
|
7
|
-
# XSalsa20+Poly1305 authenticated stream cipher
|
8
|
-
class XSalsa20Poly1305 < Cipher
|
9
|
-
register :xsalsa20poly1305, key_bytes: RbNaCl::SecretBoxes::XSalsa20Poly1305.key_bytes
|
10
|
-
|
11
|
-
def encrypt(key, plaintext)
|
12
|
-
box(key).encrypt(plaintext)
|
13
|
-
end
|
14
|
-
|
15
|
-
def decrypt(key, ciphertext)
|
16
|
-
box(key).decrypt(ciphertext)
|
17
|
-
rescue RbNaCl::CryptoError => ex
|
18
|
-
raise CorruptedMessageError, ex.to_s
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def box(key)
|
24
|
-
RbNaCl::SimpleBox.new(RbNaCl::SecretBoxes::XSalsa20Poly1305.new(key))
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/lib/cryptor/secret_key.rb
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
require 'base64'
|
2
|
-
require 'digest/sha2'
|
3
|
-
|
4
|
-
module Cryptor
|
5
|
-
# Secret key used to encrypt plaintexts
|
6
|
-
class SecretKey
|
7
|
-
attr_reader :cipher
|
8
|
-
|
9
|
-
# Generate a random secret key
|
10
|
-
#
|
11
|
-
# @param [Cryptor::Cipher, Symbol] Cryptor::Cipher or algorithm name as a symbol
|
12
|
-
#
|
13
|
-
# @return [Cryptor::SecretKey] new secret key object
|
14
|
-
def self.random_key(cipher)
|
15
|
-
cipher = Cryptor::Cipher[cipher] if cipher.is_a? Symbol
|
16
|
-
fail ArgumentError, "invalid cipher: #{cipher.inspect}" unless cipher.is_a? Cryptor::Cipher
|
17
|
-
bytes = RbNaCl::Random.random_bytes(cipher.key_bytes)
|
18
|
-
base64 = Cryptor::Encoding.encode(bytes)
|
19
|
-
|
20
|
-
new "secret.key:///#{cipher.algorithm};#{base64}"
|
21
|
-
end
|
22
|
-
|
23
|
-
# Create a new SecretKey object from a URI
|
24
|
-
#
|
25
|
-
# @param [#to_s] uri representing a secret key
|
26
|
-
#
|
27
|
-
# @raise [ArgumentError] on invalid URIs
|
28
|
-
#
|
29
|
-
# @return [Cryptor::SecretKey] new secret key object
|
30
|
-
def initialize(uri_string)
|
31
|
-
uri = URI.parse(uri_string.to_s)
|
32
|
-
fail ArgumentError, "invalid scheme: #{uri.scheme}" unless uri.scheme == 'secret.key'
|
33
|
-
|
34
|
-
components = uri.path.match(/^\/([^;]+);(.+)$/)
|
35
|
-
fail ArgumentError, "couldn't parse cipher name from secret URI" unless components
|
36
|
-
|
37
|
-
@cipher = Cryptor::Cipher[components[1]]
|
38
|
-
@secret_key = Cryptor::Encoding.decode(components[2])
|
39
|
-
end
|
40
|
-
|
41
|
-
# Serialize SecretKey object to a URI
|
42
|
-
#
|
43
|
-
# @return [String] serialized URI representing the key
|
44
|
-
def to_secret_uri
|
45
|
-
"secret.key:///#{@cipher.algorithm};#{Cryptor::Encoding.encode(@secret_key)}"
|
46
|
-
end
|
47
|
-
|
48
|
-
# Fingerprint of this key's secret URI
|
49
|
-
#
|
50
|
-
# @return [String] fingerprint as a ni:// URL
|
51
|
-
def fingerprint
|
52
|
-
digest = Digest::SHA256.digest(to_secret_uri)
|
53
|
-
"ni:///sha-256;#{Cryptor::Encoding.encode(digest)}"
|
54
|
-
end
|
55
|
-
|
56
|
-
# Encrypt a plaintext under this key
|
57
|
-
#
|
58
|
-
# @param [String] plaintext string to be encrypted
|
59
|
-
#
|
60
|
-
# @return [String] ciphertext encrypted under this key
|
61
|
-
def encrypt(plaintext)
|
62
|
-
@cipher.encrypt(@secret_key, plaintext)
|
63
|
-
end
|
64
|
-
|
65
|
-
# Decrypt ciphertext using this key
|
66
|
-
#
|
67
|
-
# @param [String] ciphertext string to be decrypted
|
68
|
-
#
|
69
|
-
# @return [String] plaintext decrypted from the given ciphertext
|
70
|
-
def decrypt(ciphertext)
|
71
|
-
@cipher.decrypt(@secret_key, ciphertext)
|
72
|
-
end
|
73
|
-
|
74
|
-
# Inspect this key
|
75
|
-
#
|
76
|
-
# @return [String] a string representing this key
|
77
|
-
def inspect
|
78
|
-
"#<#{self.class}:0x#{object_id.to_s(16)} " \
|
79
|
-
"cipher=#{cipher.algorithm} " \
|
80
|
-
"fingerprint=#{fingerprint}>"
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|