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 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