cryptor 0.0.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7e231e81835f4ec4b64c603b597b38266d505cd8
4
- data.tar.gz: 0843bf2d906c77f5824d75c187d30d2d28bf246c
3
+ metadata.gz: f5fd951f1826bc584bac34d75f61ed7a75af3875
4
+ data.tar.gz: e89c7a58275dac153b47b2201fecee4194f0ad72
5
5
  SHA512:
6
- metadata.gz: 46ecd69eebc0ab266b699f978bc934f03fd7211414eb55a8147e9cdc14a1a999424fd266c66d2db211b4ce9a0734cb6799d7e45beba2927167cd40579d835c36
7
- data.tar.gz: 3da93938f00f68a5e7b3d968e7628a09d788b3838142a48945ae14437150c5849bae15da751002ba205b70c5c90bf399ca6f80793315ce6e4a90a4f02ed48e21
6
+ metadata.gz: fde5ba8749bc1c6fa79ff27a5e8e3fcc733abfd8d164afbdd7cca3984371ce18f1a6e6d372f7c4080dd1e9945366d0d69f960fd835e8e87f1d03f34f6a85f69d
7
+ data.tar.gz: 26c0c7f57afb309c8d993121e7d2d680f0edfd5b7c4485daed7024d7445186741637e85d7958334bfa97ddab2b5e70c7982049899f10ae8107bcd2e4e88459d8
data/CHANGES.md CHANGED
@@ -1,3 +1,8 @@
1
+ 1.0.0 (2014-06-14)
2
+ ------------------
3
+ * Key rotation support
4
+ * Factor all classes under Cryptor::SymmetricEncryption
5
+
1
6
  0.0.2 (2014-06-07)
2
7
  ------------------
3
8
  * Raise Cryptor::CorruptedMessageError on MAC verification failure
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
- "secret.key:///xsalsa20poly1305;0saB1tfgKWDh_bX0oAquLWgAq-6yjG1u04mP-CtQG-4"
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(key)
14
- @key = key
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 = @key.encrypt(plaintext)
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' => @key.cipher.algorithm,
35
+ 'Cipher' => @active_key.cipher.algorithm,
24
36
  'Content-Length' => base64.bytesize,
25
37
  'Content-Transfer-Encoding' => 'base64',
26
- 'Key-Fingerprint' => @key.fingerprint
38
+ 'Key-Fingerprint' => @active_key.fingerprint
27
39
  ).to_string
28
40
  end
29
41
 
30
42
  def decrypt(ciphertext)
31
- begin
32
- message = ORDO::Message.parse(ciphertext)
33
- rescue ORDO::ParseError => ex
34
- raise InvalidMessageError, ex.to_s
35
- end
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 ArgumentError, "no key configured for: #{fingerprint}" if @key.fingerprint != fingerprint
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
- @key.decrypt message.body
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
@@ -1,4 +1,4 @@
1
1
  # An easy-to-use library for real-world Ruby cryptography
2
2
  module Cryptor
3
- VERSION = '0.0.2'
3
+ VERSION = '1.0.0'
4
4
  end
@@ -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.2
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-07 00:00:00.000000000 Z
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
@@ -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
@@ -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