miscreant 0.1.0 → 0.2.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/.rubocop.yml +15 -0
- data/README.md +22 -15
- data/lib/miscreant.rb +8 -0
- data/lib/miscreant/aead.rb +93 -0
- data/lib/miscreant/aes/cmac.rb +63 -0
- data/lib/miscreant/aes/pmac.rb +128 -0
- data/lib/miscreant/aes/siv.rb +123 -0
- data/lib/miscreant/internals.rb +4 -3
- data/lib/miscreant/internals/aes/block_cipher.rb +48 -0
- data/lib/miscreant/internals/aes/ctr.rb +40 -0
- data/lib/miscreant/internals/block.rb +103 -0
- data/lib/miscreant/internals/util.rb +51 -55
- data/lib/miscreant/version.rb +2 -1
- metadata +9 -5
- data/lib/miscreant/internals/aes.rb +0 -15
- data/lib/miscreant/internals/aes/cmac.rb +0 -79
- data/lib/miscreant/internals/aes/siv.rb +0 -120
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96a35e90aee3d3f35dcbe9a273e57d9685b7d537
|
4
|
+
data.tar.gz: dc9ab108d1d8e69daccc528bc6f06f009ab584ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8463583471437d1be6ef3885bbcee2ef6d78c58db500d2893a2ab3c91e6864eb5669fae96377d59d6a45e4711b6a2b7bbc57bb8d41f1a8538b0475a10ceeeb7e
|
7
|
+
data.tar.gz: f9d0be51f4255fed217290c9d07e90dea204e3009836bf5088eb861881d3b5f84b0d05804004c48d6b50e21f6367a0b8927177dd7cda138173fd1ef82d4a7039
|
data/.rubocop.yml
CHANGED
@@ -17,6 +17,9 @@ Metrics/CyclomaticComplexity:
|
|
17
17
|
Metrics/PerceivedComplexity:
|
18
18
|
Enabled: false
|
19
19
|
|
20
|
+
Metrics/BlockLength:
|
21
|
+
Max: 100
|
22
|
+
|
20
23
|
Metrics/ClassLength:
|
21
24
|
Max: 100
|
22
25
|
|
@@ -30,5 +33,17 @@ Metrics/MethodLength:
|
|
30
33
|
# Style
|
31
34
|
#
|
32
35
|
|
36
|
+
Style/AsciiComments:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Style/ClassAndModuleChildren:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Style/ConditionalAssignment:
|
43
|
+
Enabled: false
|
44
|
+
|
45
|
+
Style/NumericPredicate:
|
46
|
+
Enabled: false
|
47
|
+
|
33
48
|
Style/StringLiterals:
|
34
49
|
EnforcedStyle: double_quotes
|
data/README.md
CHANGED
@@ -81,27 +81,24 @@ Or install it yourself as:
|
|
81
81
|
|
82
82
|
## API
|
83
83
|
|
84
|
-
### Miscreant::
|
84
|
+
### Miscreant::AEAD
|
85
85
|
|
86
|
-
The `Miscreant::
|
86
|
+
The `Miscreant::AEAD` class provides the main interface to the **AES-SIV**
|
87
87
|
misuse resistant authenticated encryption function.
|
88
88
|
|
89
|
-
To make a new instance, pass in a 32-byte or 64-byte key.
|
90
|
-
options are twice the size of what you might be expecting
|
91
|
-
AES keys).
|
92
|
-
|
93
|
-
You can generate a random key using the `generate_key` method (default 32 bytes):
|
89
|
+
To make a new instance, pass in a binary-encoded 32-byte or 64-byte key.
|
90
|
+
Note that these options are twice the size of what you might be expecting
|
91
|
+
(AES-SIV uses two AES keys).
|
94
92
|
|
95
93
|
```ruby
|
96
|
-
|
97
|
-
|
98
|
-
# => #<Miscreant::AES::SIV:0x007fe0109e85e8>
|
94
|
+
secret_key = Miscreant::AEAD.generate_key
|
95
|
+
encryptor = Miscreant::AEAD.new("AES-SIV", secret_key)
|
99
96
|
```
|
100
97
|
|
101
98
|
#### Encryption (#seal)
|
102
99
|
|
103
|
-
The `Miscreant::
|
104
|
-
*associated data* message headers.
|
100
|
+
The `Miscreant::AEAD#seal` method encrypts a binary-encoded message along with
|
101
|
+
a set of *associated data* message headers.
|
105
102
|
|
106
103
|
It's recommended to include a unique "nonce" value with each message. This
|
107
104
|
prevents those who may be observing your ciphertexts from being able to tell
|
@@ -114,23 +111,33 @@ Example:
|
|
114
111
|
|
115
112
|
```ruby
|
116
113
|
message = "Hello, world!"
|
117
|
-
nonce =
|
114
|
+
nonce = Miscreant::AEAD.generate_nonce
|
118
115
|
ciphertext = key.seal(message, nonce)
|
119
116
|
```
|
120
117
|
|
121
118
|
#### Decryption (#open)
|
122
119
|
|
123
|
-
The `Miscreant::
|
120
|
+
The `Miscreant::AEAD#open` method decrypts a binary-encoded ciphertext with the
|
121
|
+
given key.
|
124
122
|
|
125
123
|
Example:
|
126
124
|
|
127
125
|
```ruby
|
128
126
|
message = "Hello, world!"
|
129
|
-
nonce =
|
127
|
+
nonce = Miscreant::AEAD.generate_nonce
|
130
128
|
ciphertext = key.seal(message, nonce)
|
131
129
|
plaintext = key.open(ciphertext, nonce)
|
132
130
|
```
|
133
131
|
|
132
|
+
## Code of Conduct
|
133
|
+
|
134
|
+
We abide by the [Contributor Covenant][cc] and ask that you do as well.
|
135
|
+
|
136
|
+
For more information, please see [CODE_OF_CONDUCT.md].
|
137
|
+
|
138
|
+
[cc]: https://contributor-covenant.org
|
139
|
+
[CODE_OF_CONDUCT.md]: https://github.com/miscreant/miscreant/blob/master/CODE_OF_CONDUCT.md
|
140
|
+
|
134
141
|
## Contributing
|
135
142
|
|
136
143
|
Bug reports and pull requests are welcome on GitHub at https://github.com/miscreant/miscreant
|
data/lib/miscreant.rb
CHANGED
@@ -1,9 +1,14 @@
|
|
1
|
+
# encoding: binary
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require "openssl"
|
4
5
|
require "securerandom"
|
5
6
|
|
6
7
|
require "miscreant/version"
|
8
|
+
require "miscreant/aead"
|
9
|
+
require "miscreant/aes/cmac"
|
10
|
+
require "miscreant/aes/pmac"
|
11
|
+
require "miscreant/aes/siv"
|
7
12
|
require "miscreant/internals"
|
8
13
|
|
9
14
|
# Miscreant: A misuse-resistant symmetric encryption library
|
@@ -13,4 +18,7 @@ module Miscreant
|
|
13
18
|
|
14
19
|
# Ciphertext failed to verify as authentic
|
15
20
|
IntegrityError = Class.new(CryptoError)
|
21
|
+
|
22
|
+
# Hide internals from the outside world
|
23
|
+
private_constant :Internals
|
16
24
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
# The AEAD class provides Authenticated Encryption with Associated Data
|
6
|
+
#
|
7
|
+
# If you're looking for the API to encrypt something, congratulations!
|
8
|
+
# This is the one you probably want to use. This class provides a high-level
|
9
|
+
# interface to Miscreant's misuse-resistant encryption.
|
10
|
+
class AEAD
|
11
|
+
# Generate a new random AES-SIV key of the given size
|
12
|
+
#
|
13
|
+
# @param size [Integer] size of key in bytes (32 or 64)
|
14
|
+
#
|
15
|
+
# @return [String] newly generated AES-SIV key
|
16
|
+
def self.generate_key(size = 32)
|
17
|
+
raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
|
18
|
+
AES::SIV.generate_key(size)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Generate a random "nonce" (i.e. number used once) value
|
22
|
+
#
|
23
|
+
# @param size [Integer] size of nonce in bytes (default 16)
|
24
|
+
#
|
25
|
+
# @return [String] newly generated nonce value
|
26
|
+
def self.generate_nonce(size = 16)
|
27
|
+
SecureRandom.random_bytes(size)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create a new AEAD encryptor instance.
|
31
|
+
#
|
32
|
+
# You will need to select an algorithm to use, passed as a string:
|
33
|
+
#
|
34
|
+
# * "AES-SIV" (RFC 5297): the original AES-SIV function, based on CMAC
|
35
|
+
# * "AES-PMAC-SIV": a parallelizable AES-SIV alternative
|
36
|
+
#
|
37
|
+
# Choose AES-PMAC-SIV if you'd like better performance.
|
38
|
+
# Choose AES-SIV if you'd like wider compatibility: AES-PMAC-SIV is
|
39
|
+
# presently implemented in the Miscreant libraries.
|
40
|
+
#
|
41
|
+
# @param alg ["AES-SIV", "AES-PMAC-SIV"] cryptographic algorithm to use
|
42
|
+
# @param key [String] 32-byte or 64-byte random Encoding::BINARY secret key
|
43
|
+
def initialize(alg, key)
|
44
|
+
Internals::Util.validate_bytestring("key", key, length: [32, 64])
|
45
|
+
|
46
|
+
case alg
|
47
|
+
when "AES-SIV", "AES-CMAC-SIV"
|
48
|
+
mac = :CMAC
|
49
|
+
when "AES-PMAC-SIV"
|
50
|
+
mac = :PMAC
|
51
|
+
else raise ArgumentError, "unsupported algorithm: #{alg.inspect}"
|
52
|
+
end
|
53
|
+
|
54
|
+
@siv = AES::SIV.new(key, mac)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Inspect this AES-SIV instance
|
58
|
+
#
|
59
|
+
# @return [String] description of this instance
|
60
|
+
def inspect
|
61
|
+
to_s
|
62
|
+
end
|
63
|
+
|
64
|
+
# Encrypt a message, authenticating it along with the associated data
|
65
|
+
#
|
66
|
+
# @param plaintext [String] an Encoding::BINARY string to encrypt
|
67
|
+
# @param nonce [String] a unique-per-message value
|
68
|
+
# @param ad [String] optional data to authenticate along with the message
|
69
|
+
#
|
70
|
+
# @return [String] encrypted ciphertext
|
71
|
+
def seal(plaintext, nonce:, ad: "")
|
72
|
+
raise TypeError, "expected String, got #{nonce.class}" unless nonce.is_a?(String)
|
73
|
+
raise TypeError, "expected String, got #{ad.class}" unless ad.is_a?(String)
|
74
|
+
|
75
|
+
@siv.seal(plaintext, [ad, nonce])
|
76
|
+
end
|
77
|
+
|
78
|
+
# Verify and decrypt a ciphertext, authenticating it along with the associated data
|
79
|
+
#
|
80
|
+
# @param ciphertext [String] an Encoding::BINARY string to decrypt
|
81
|
+
# @param nonce [String] a unique-per-message value
|
82
|
+
# @param associated_data [String] optional data to authenticate along with the message
|
83
|
+
#
|
84
|
+
# @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
|
85
|
+
# @return [String] decrypted plaintext
|
86
|
+
def open(ciphertext, nonce:, ad: "")
|
87
|
+
raise TypeError, "expected nonce to be String, got #{nonce.class}" unless nonce.is_a?(String)
|
88
|
+
raise TypeError, "expected ad to be String, got #{ad.class}" unless ad.is_a?(String)
|
89
|
+
|
90
|
+
@siv.open(ciphertext, [ad, nonce])
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
# The Advanced Encryption Standard
|
6
|
+
module AES
|
7
|
+
# The AES-CMAC message authentication code
|
8
|
+
class CMAC
|
9
|
+
# Create a new AES-CMAC instance
|
10
|
+
#
|
11
|
+
# @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
|
12
|
+
def initialize(key)
|
13
|
+
@cipher = Internals::AES::BlockCipher.new(key)
|
14
|
+
|
15
|
+
@subkey1 = Internals::Block.new
|
16
|
+
@subkey1.encrypt(@cipher)
|
17
|
+
@subkey1.dbl
|
18
|
+
|
19
|
+
@subkey2 = @subkey1.dup
|
20
|
+
@subkey2.dbl
|
21
|
+
end
|
22
|
+
|
23
|
+
# Inspect this AES-CMAC instance
|
24
|
+
#
|
25
|
+
# @return [String] description of this instance
|
26
|
+
def inspect
|
27
|
+
to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
# Compute the AES-CMAC of the given input message in a single shot,
|
31
|
+
# outputting the MAC tag.
|
32
|
+
#
|
33
|
+
# Unlike other AES-CMAC implementations, this one does not support
|
34
|
+
# incremental processing/IUF operation. (Though that would enable
|
35
|
+
# slightly more efficient decryption for AES-SIV)
|
36
|
+
#
|
37
|
+
# @param message [String] an Encoding::BINARY string to authenticate
|
38
|
+
#
|
39
|
+
# @return [String] CMAC tag
|
40
|
+
def digest(message)
|
41
|
+
Internals::Util.validate_bytestring("message", message)
|
42
|
+
|
43
|
+
if message.empty? || message.length % Internals::Block::SIZE != 0
|
44
|
+
message = Internals::Util.pad(message, Internals::Block::SIZE)
|
45
|
+
subkey = @subkey2
|
46
|
+
else
|
47
|
+
subkey = @subkey1
|
48
|
+
end
|
49
|
+
|
50
|
+
count = message.length / Internals::Block::SIZE
|
51
|
+
digest = Internals::Block.new
|
52
|
+
|
53
|
+
count.times do |i|
|
54
|
+
digest.xor_in_place(message[Internals::Block::SIZE * i, Internals::Block::SIZE])
|
55
|
+
digest.xor_in_place(subkey) if i == count - 1
|
56
|
+
digest.encrypt(@cipher)
|
57
|
+
end
|
58
|
+
|
59
|
+
digest.data
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
# The Advanced Encryption Standard
|
6
|
+
module AES
|
7
|
+
# The Parallel Message Authentication Code
|
8
|
+
class PMAC
|
9
|
+
# Number of L blocks to precompute (i.e. µ in the PMAC paper)
|
10
|
+
# TODO: dynamically compute these as needed
|
11
|
+
PRECOMPUTED_BLOCKS = 31
|
12
|
+
|
13
|
+
# Create a new PMAC instance
|
14
|
+
#
|
15
|
+
# @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
|
16
|
+
def initialize(key)
|
17
|
+
@cipher = Internals::AES::BlockCipher.new(key)
|
18
|
+
|
19
|
+
# L is defined as follows (quoted from the PMAC paper):
|
20
|
+
#
|
21
|
+
# Equation 1:
|
22
|
+
#
|
23
|
+
# a · x =
|
24
|
+
# a<<1 if firstbit(a)=0
|
25
|
+
# (a<<1) ⊕ 0¹²⁰10000111 if firstbit(a)=1
|
26
|
+
#
|
27
|
+
# Equation 2:
|
28
|
+
#
|
29
|
+
# a · x⁻¹ =
|
30
|
+
# a>>1 if lastbit(a)=0
|
31
|
+
# (a>>1) ⊕ 10¹²⁰1000011 if lastbit(a)=1
|
32
|
+
#
|
33
|
+
# Let L(0) ← L. For i ∈ [1..µ], compute L(i) ← L(i − 1) · x by
|
34
|
+
# Equation (1) using a shift and a conditional xor.
|
35
|
+
#
|
36
|
+
# Compute L(−1) ← L · x⁻¹ by Equation (2), using a shift and a
|
37
|
+
# conditional xor.
|
38
|
+
#
|
39
|
+
# Save the values L(−1), L(0), L(1), L(2), ..., L(µ) in a table.
|
40
|
+
# (Alternatively, [ed: as we have done in this codebase] defer computing
|
41
|
+
# some or all of these L(i) values until the value is actually needed.)
|
42
|
+
@l = []
|
43
|
+
tmp = Internals::Block.new
|
44
|
+
tmp.encrypt(@cipher)
|
45
|
+
|
46
|
+
PRECOMPUTED_BLOCKS.times.each do
|
47
|
+
block = Internals::Block.new(tmp.data.dup)
|
48
|
+
block.data.freeze
|
49
|
+
block.freeze
|
50
|
+
|
51
|
+
@l << block
|
52
|
+
tmp.dbl
|
53
|
+
end
|
54
|
+
|
55
|
+
@l.freeze
|
56
|
+
|
57
|
+
# Compute L(−1) ← L · x⁻¹:
|
58
|
+
#
|
59
|
+
# a>>1 if lastbit(a)=0
|
60
|
+
# (a>>1) ⊕ 10¹²⁰1000011 if lastbit(a)=1
|
61
|
+
#
|
62
|
+
@l_inv = Internals::Block.new(@l[0].data.dup)
|
63
|
+
last_bit = @l_inv[Internals::Block::SIZE - 1] & 0x01
|
64
|
+
|
65
|
+
(Internals::Block::SIZE - 1).downto(1) do |i|
|
66
|
+
carry = Internals::Util.ct_select(@l_inv[i - 1] & 1, 0x80, 0)
|
67
|
+
@l_inv[i] = (@l_inv[i] >> 1) | carry
|
68
|
+
end
|
69
|
+
|
70
|
+
@l_inv[0] >>= 1
|
71
|
+
@l_inv[0] ^= Internals::Util.ct_select(last_bit, 0x80, 0)
|
72
|
+
@l_inv[Internals::Block::SIZE - 1] ^= Internals::Util.ct_select(last_bit, Internals::Block::R >> 1, 0)
|
73
|
+
@l_inv.freeze
|
74
|
+
@l_inv.data.freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
# Inspect this PMAC instance
|
78
|
+
#
|
79
|
+
# @return [String] description of this instance
|
80
|
+
def inspect
|
81
|
+
to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
# Compute the PMAC of the given input message in a single shot,
|
85
|
+
# outputting the MAC tag.
|
86
|
+
#
|
87
|
+
# Unlike other PMAC implementations, this one does not support
|
88
|
+
# incremental processing/IUF operation. (Though that would enable
|
89
|
+
# slightly more efficient decryption for AES-SIV)
|
90
|
+
#
|
91
|
+
# @param message [String] an Encoding::BINARY string to authenticate
|
92
|
+
#
|
93
|
+
# @return [String] PMAC tag
|
94
|
+
def digest(message)
|
95
|
+
Internals::Util.validate_bytestring("message", message)
|
96
|
+
|
97
|
+
offset = Internals::Block.new
|
98
|
+
tmp = Internals::Block.new
|
99
|
+
tag = Internals::Block.new
|
100
|
+
ctr = 0
|
101
|
+
remaining = message.bytesize
|
102
|
+
|
103
|
+
while remaining > Internals::Block::SIZE
|
104
|
+
offset.xor_in_place(@l.fetch(Internals::Util.ctz(ctr + 1)))
|
105
|
+
|
106
|
+
tmp.copy(offset)
|
107
|
+
tmp.xor_in_place(message[ctr * Internals::Block::SIZE, Internals::Block::SIZE])
|
108
|
+
tmp.encrypt(@cipher)
|
109
|
+
tag.xor_in_place(tmp)
|
110
|
+
|
111
|
+
ctr += 1
|
112
|
+
remaining -= Internals::Block::SIZE
|
113
|
+
end
|
114
|
+
|
115
|
+
if remaining == Internals::Block::SIZE
|
116
|
+
tag.xor_in_place(message[(ctr * Internals::Block::SIZE)..-1])
|
117
|
+
tag.xor_in_place(@l_inv)
|
118
|
+
else
|
119
|
+
remaining.times { |i| tag[i] ^= message.getbyte((ctr * Internals::Block::SIZE) + i) }
|
120
|
+
tag[remaining] ^= 0x80
|
121
|
+
end
|
122
|
+
|
123
|
+
tag.encrypt(@cipher)
|
124
|
+
tag.data
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
# The Advanced Encryption Standard
|
6
|
+
module AES
|
7
|
+
# The SIV misuse resistant authenticated encryption mode
|
8
|
+
#
|
9
|
+
# This class is intended for power users. If you're uncertain if you
|
10
|
+
# should be using it, you probably want the `Miscreant::AEAD` API.
|
11
|
+
class SIV
|
12
|
+
# Generate a new random AES-SIV key of the given size
|
13
|
+
#
|
14
|
+
# @param size [Integer] size of key in bytes (32 or 64)
|
15
|
+
#
|
16
|
+
# @return [String] newly generated AES-SIV key
|
17
|
+
def self.generate_key(size = 32)
|
18
|
+
raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
|
19
|
+
SecureRandom.random_bytes(size)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a new AES-SIV instance
|
23
|
+
#
|
24
|
+
# @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
|
25
|
+
# @param mac [:CMAC, :PMAC] (optional) MAC function to use (default CMAC)
|
26
|
+
def initialize(key, mac_class = :CMAC)
|
27
|
+
Internals::Util.validate_bytestring("key", key, length: [32, 64])
|
28
|
+
length = key.length / 2
|
29
|
+
|
30
|
+
case mac_class
|
31
|
+
when :CMAC
|
32
|
+
@mac = CMAC.new(key[0, length])
|
33
|
+
when :PMAC
|
34
|
+
@mac = PMAC.new(key[0, length])
|
35
|
+
else raise ArgumentError, "bad MAC class: #{mac_class} (expected :CMAC or :PMAC)"
|
36
|
+
end
|
37
|
+
|
38
|
+
@ctr = Internals::AES::CTR.new(key[length..-1])
|
39
|
+
end
|
40
|
+
|
41
|
+
# Inspect this AES-SIV instance
|
42
|
+
#
|
43
|
+
# @return [String] description of this instance
|
44
|
+
def inspect
|
45
|
+
to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
# Encrypt a message using AES-SIV, authenticating it along with the associated data
|
49
|
+
#
|
50
|
+
# @param plaintext [String] an Encoding::BINARY string to encrypt
|
51
|
+
# @param associated_data [Array<String>] optional array of message headers to authenticate
|
52
|
+
#
|
53
|
+
# @return [String] encrypted ciphertext
|
54
|
+
def seal(plaintext, associated_data = [])
|
55
|
+
raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
|
56
|
+
v = _s2v(associated_data, plaintext)
|
57
|
+
ciphertext = @ctr.encrypt(_zero_iv_bits(v), plaintext)
|
58
|
+
v + ciphertext
|
59
|
+
end
|
60
|
+
|
61
|
+
# Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
|
62
|
+
#
|
63
|
+
# @param ciphertext [String] an Encoding::BINARY string to decrypt
|
64
|
+
# @param associated_data [Array<String>] optional array of message headers to authenticate
|
65
|
+
#
|
66
|
+
# @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
|
67
|
+
# @return [String] decrypted plaintext
|
68
|
+
def open(ciphertext, associated_data = [])
|
69
|
+
raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
|
70
|
+
v = ciphertext[0, Internals::Block::SIZE]
|
71
|
+
plaintext = @ctr.encrypt(_zero_iv_bits(v), ciphertext[Internals::Block::SIZE..-1])
|
72
|
+
t = _s2v(associated_data, plaintext)
|
73
|
+
raise IntegrityError, "ciphertext verification failure!" unless Internals::Util.ct_equal(t, v)
|
74
|
+
|
75
|
+
plaintext
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# The S2V operation consists of the doubling and XORing of the outputs
|
81
|
+
# of the pseudo-random function CMAC.
|
82
|
+
#
|
83
|
+
# See Section 2.4 of RFC 5297 for more information
|
84
|
+
def _s2v(associated_data, plaintext)
|
85
|
+
# Note: the standalone S2V returns CMAC(1) if the number of passed
|
86
|
+
# vectors is zero, however in SIV construction this case is never
|
87
|
+
# triggered, since we always pass plaintext as the last vector (even
|
88
|
+
# if it's zero-length), so we omit this case.
|
89
|
+
d = Internals::Block.new
|
90
|
+
d.xor_in_place(@mac.digest(d.data))
|
91
|
+
|
92
|
+
associated_data.each do |ad|
|
93
|
+
d.dbl
|
94
|
+
d.xor_in_place(@mac.digest(ad))
|
95
|
+
end
|
96
|
+
|
97
|
+
if plaintext.bytesize >= Internals::Block::SIZE
|
98
|
+
# TODO: implement this more efficiently by adding IUF support to CMAC
|
99
|
+
difference = plaintext.length - Internals::Block::SIZE
|
100
|
+
beginning = plaintext[0, difference]
|
101
|
+
d.xor_in_place(plaintext[difference..-1])
|
102
|
+
msg = beginning + d.data
|
103
|
+
else
|
104
|
+
d.dbl
|
105
|
+
d.xor_in_place(Internals::Util.pad(plaintext, Internals::Block::SIZE))
|
106
|
+
msg = d.data
|
107
|
+
end
|
108
|
+
|
109
|
+
@mac.digest(msg)
|
110
|
+
end
|
111
|
+
|
112
|
+
# "We zero-out the top bit in each of the last two 32-bit words
|
113
|
+
# of the IV before assigning it to Ctr"
|
114
|
+
# -- http://web.cs.ucdavis.edu/~rogaway/papers/siv.pdf
|
115
|
+
def _zero_iv_bits(iv)
|
116
|
+
iv = iv.dup
|
117
|
+
iv.setbyte(8, iv.getbyte(8) & 0x7f)
|
118
|
+
iv.setbyte(12, iv.getbyte(12) & 0x7f)
|
119
|
+
iv
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/miscreant/internals.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
# encoding: binary
|
1
2
|
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require "miscreant/internals/
|
4
|
-
require "miscreant/internals/aes/siv"
|
5
|
-
require "miscreant/internals/aes/cmac"
|
4
|
+
require "miscreant/internals/block"
|
6
5
|
require "miscreant/internals/util"
|
6
|
+
require "miscreant/internals/aes/block_cipher"
|
7
|
+
require "miscreant/internals/aes/ctr"
|
7
8
|
|
8
9
|
module Miscreant
|
9
10
|
# Internal functionality not intended for direct consumption
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
module Internals
|
6
|
+
module AES # :nodoc:
|
7
|
+
# The AES cipher in a raw block mode (a.k.a. ECB mode)
|
8
|
+
#
|
9
|
+
# NOTE: The only valid use of ECB mode is constructing higher-level
|
10
|
+
# cryptographic primitives. This library uses this class to implement
|
11
|
+
# the CMAC and PMAC message authentication codes.
|
12
|
+
class BlockCipher # :nodoc:
|
13
|
+
# Create a new block cipher instance
|
14
|
+
#
|
15
|
+
# @param key [String] a random 16-byte or 32-byte Encoding::BINARY encryption key
|
16
|
+
#
|
17
|
+
# @raise [TypeError] the key was not a String
|
18
|
+
# @raise [ArgumentError] the key was the wrong length or encoding
|
19
|
+
def initialize(key)
|
20
|
+
Util.validate_bytestring("key", key, length: [16, 32])
|
21
|
+
|
22
|
+
@cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
|
23
|
+
@cipher.encrypt
|
24
|
+
@cipher.padding = 0
|
25
|
+
@cipher.key = key
|
26
|
+
end
|
27
|
+
|
28
|
+
# Inspect this AES block cipher instance
|
29
|
+
#
|
30
|
+
# @return [String] description of this instance
|
31
|
+
def inspect
|
32
|
+
to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
# Encrypt the given AES block-sized message
|
36
|
+
#
|
37
|
+
# @param message [String] a 16-byte Encoding::BINARY message to encrypt
|
38
|
+
#
|
39
|
+
# @raise [TypeError] the message was not a String
|
40
|
+
# @raise [ArgumentError] the message was the wrong length
|
41
|
+
def encrypt(message)
|
42
|
+
Util.validate_bytestring("message", message, length: Block::SIZE)
|
43
|
+
@cipher.update(message) + @cipher.final
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
module Internals
|
6
|
+
module AES # :nodoc:
|
7
|
+
# The AES-CTR unauthenticated stream cipher
|
8
|
+
class CTR # :nodoc:
|
9
|
+
# Create a new AES-CTR instance
|
10
|
+
#
|
11
|
+
# @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
|
12
|
+
def initialize(key)
|
13
|
+
Util.validate_bytestring("key", key, length: [16, 32])
|
14
|
+
@cipher = OpenSSL::Cipher::AES.new(key.bytesize * 8, :CTR)
|
15
|
+
@cipher.encrypt
|
16
|
+
@cipher.key = key
|
17
|
+
end
|
18
|
+
|
19
|
+
# Inspect this AES-CTR instance
|
20
|
+
#
|
21
|
+
# @return [String] description of this instance
|
22
|
+
def inspect
|
23
|
+
to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
# Encrypt the given message using the given counter (i.e. IV)
|
27
|
+
#
|
28
|
+
# @param iv [String] initial counter value as a 16-byte Encoding::BINARY string
|
29
|
+
# @param message [String] message to be encrypted
|
30
|
+
def encrypt(iv, message)
|
31
|
+
Util.validate_bytestring("IV", iv, length: Block::SIZE)
|
32
|
+
return "".b if message.empty?
|
33
|
+
|
34
|
+
@cipher.iv = iv
|
35
|
+
@cipher.update(message) + @cipher.final
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Miscreant
|
5
|
+
module Internals
|
6
|
+
# A 128-bit block (i.e. for AES)
|
7
|
+
class Block # :nodoc:
|
8
|
+
# Size of an AES block in bytes
|
9
|
+
SIZE = 16
|
10
|
+
|
11
|
+
# Minimal irreducible polynomial for a 128-bit block size
|
12
|
+
R = 0x87
|
13
|
+
|
14
|
+
attr_reader :data
|
15
|
+
|
16
|
+
# Create a new Block, optionally from the given data
|
17
|
+
def initialize(data = nil)
|
18
|
+
if data
|
19
|
+
@data = Util.validate_bytestring("block data", data, length: SIZE)
|
20
|
+
else
|
21
|
+
@data = "\0".b * SIZE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Inspect the contents of the block in hex
|
26
|
+
def inspect
|
27
|
+
"#<#{self.class} data:\"#{@data.unpack('H*').first}\">"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Retrieve the value of the byte at the given index as an integer
|
31
|
+
def [](n)
|
32
|
+
raise IndexError, "n must be zero or greater (got #{n})" if n < 0
|
33
|
+
raise IndexError, "n must be less than #{SIZE} (got #{n})" unless n < SIZE
|
34
|
+
|
35
|
+
@data.getbyte(n)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set the value of the byte at the given index as an integer
|
39
|
+
def []=(n, byte)
|
40
|
+
@data.setbyte(n, byte)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Reset the value of this block to all zeroes
|
44
|
+
def clear
|
45
|
+
SIZE.times { |n| @data[n] = 0 }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Copy the contents of another block into this block
|
49
|
+
def copy(other_block)
|
50
|
+
SIZE.times { |n| @data[n] = other_block.data[n] }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Double a value over GF(2^128):
|
54
|
+
#
|
55
|
+
# a<<1 if firstbit(a)=0
|
56
|
+
# (a<<1) ⊕ 0¹²⁰10000111 if firstbit(a)=1
|
57
|
+
#
|
58
|
+
def dbl
|
59
|
+
overflow = 0
|
60
|
+
words = @data.unpack("N4").reverse
|
61
|
+
|
62
|
+
words.map! do |word|
|
63
|
+
new_word = (word << 1) & 0xFFFFFFFF
|
64
|
+
new_word |= overflow
|
65
|
+
overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
|
66
|
+
new_word
|
67
|
+
end
|
68
|
+
|
69
|
+
@data = words.reverse.pack("N4")
|
70
|
+
@data[-1] = (@data[-1].ord ^ Util.ct_select(overflow, R, 0)).chr
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
# Encrypt this block in-place, replacing its current contents with
|
75
|
+
# their ciphertext under the given block cipher
|
76
|
+
#
|
77
|
+
# @param cipher [Miscreant::Internals::AES::BlockCipher] block cipher to encrypt with
|
78
|
+
def encrypt(cipher)
|
79
|
+
raise TypeError, "invalid cipher: #{cipher.class}" unless cipher.is_a?(AES::BlockCipher)
|
80
|
+
|
81
|
+
# TODO: more efficient in-place encryption
|
82
|
+
@data = cipher.encrypt(@data)
|
83
|
+
end
|
84
|
+
|
85
|
+
# XOR the given data into the current block in-place
|
86
|
+
#
|
87
|
+
# @param value [AES::Block, String] a block or String to XOR into this one
|
88
|
+
def xor_in_place(value)
|
89
|
+
case value
|
90
|
+
when Block
|
91
|
+
value = value.data
|
92
|
+
when String
|
93
|
+
Util.validate_bytestring("value", value, length: SIZE)
|
94
|
+
else raise TypeError, "invalid XOR input: #{value.class}"
|
95
|
+
end
|
96
|
+
|
97
|
+
SIZE.times do |i|
|
98
|
+
self[i] ^= value.getbyte(i)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -7,76 +7,72 @@ module Miscreant
|
|
7
7
|
module Util # :nodoc:
|
8
8
|
module_function
|
9
9
|
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
# :nodoc: Lookup table for the number of trailing zeroes in a byte
|
11
|
+
CTZ_TABLE = [
|
12
|
+
8, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
13
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
14
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
15
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
16
|
+
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
17
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
18
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
19
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
20
|
+
7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
21
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
22
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
23
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
24
|
+
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
25
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
26
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
27
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
|
28
|
+
].freeze
|
14
29
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
|
19
|
-
new_word
|
20
|
-
end
|
30
|
+
# Perform a constant time-ish comparison of two bytestrings
|
31
|
+
def ct_equal(a, b)
|
32
|
+
return false unless a.bytesize == b.bytesize
|
21
33
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
34
|
+
l = a.unpack("C*")
|
35
|
+
r = 0
|
36
|
+
i = -1
|
26
37
|
|
27
|
-
|
28
|
-
|
29
|
-
padded_length = message.length + length - (message.length % length)
|
30
|
-
message += "\x80"
|
31
|
-
message.ljust(padded_length, "\0")
|
38
|
+
b.each_byte { |v| r |= v ^ l[i += 1] }
|
39
|
+
r.zero?
|
32
40
|
end
|
33
41
|
|
34
42
|
# Perform a constant time(-ish) branch operation
|
35
|
-
def
|
43
|
+
def ct_select(subject, result_if_one, result_if_zero)
|
36
44
|
(~(subject - 1) & result_if_one) | ((subject - 1) & result_if_zero)
|
37
45
|
end
|
38
46
|
|
39
|
-
#
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
output[i] = (a[i].ord ^ b[i].ord).chr
|
45
|
-
end
|
46
|
-
output
|
47
|
-
end
|
48
|
-
|
49
|
-
# XOR the second value into the end of the first
|
50
|
-
def xorend(a, b)
|
51
|
-
difference = a.length - b.length
|
52
|
-
|
53
|
-
left = a.slice(0, difference)
|
54
|
-
right = a.slice(difference..-1)
|
55
|
-
|
56
|
-
left + xor(right, b)
|
47
|
+
# Count the number of zeros in a given 8-bit integer
|
48
|
+
#
|
49
|
+
# @param value [Integer] an integer in the 8-bit unsigned range (0-255)
|
50
|
+
def ctz(value)
|
51
|
+
CTZ_TABLE.fetch(value)
|
57
52
|
end
|
58
53
|
|
59
|
-
#
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
iv = iv.dup
|
65
|
-
iv[8] = (iv[8].ord & 0x7f).chr
|
66
|
-
iv[12] = (iv[12].ord & 0x7f).chr
|
67
|
-
iv
|
54
|
+
# Pad value with a 0x80 value and zeroes up to the given length
|
55
|
+
def pad(message, length)
|
56
|
+
padded_length = message.length + length - (message.length % length)
|
57
|
+
message += "\x80"
|
58
|
+
message.ljust(padded_length, "\0")
|
68
59
|
end
|
69
60
|
|
70
|
-
#
|
71
|
-
def
|
72
|
-
|
61
|
+
# Ensure a string is a valid bytestring (potentially of a given length)
|
62
|
+
def validate_bytestring(name, string, length: nil)
|
63
|
+
raise TypeError, "expected String for #{name}, got #{string.class}" unless string.is_a?(String)
|
64
|
+
raise ArgumentError, "#{name} must be Encoding::BINARY" unless string.encoding == Encoding::BINARY
|
73
65
|
|
74
|
-
|
75
|
-
|
76
|
-
|
66
|
+
case length
|
67
|
+
when Array
|
68
|
+
raise ArgumentError, "#{name} must be #{length.join(' or ')} bytes long" unless length.include?(string.bytesize)
|
69
|
+
when Integer
|
70
|
+
raise ArgumentError, "#{name} must be #{length}-bytes long" unless string.bytesize == length
|
71
|
+
else
|
72
|
+
raise TypeError, "bad length parameter: #{length.inspect} (#{length.class}" unless length.nil?
|
73
|
+
end
|
77
74
|
|
78
|
-
|
79
|
-
r.zero?
|
75
|
+
string
|
80
76
|
end
|
81
77
|
end
|
82
78
|
end
|
data/lib/miscreant/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: miscreant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Arcieri
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -39,10 +39,14 @@ files:
|
|
39
39
|
- README.md
|
40
40
|
- Rakefile
|
41
41
|
- lib/miscreant.rb
|
42
|
+
- lib/miscreant/aead.rb
|
43
|
+
- lib/miscreant/aes/cmac.rb
|
44
|
+
- lib/miscreant/aes/pmac.rb
|
45
|
+
- lib/miscreant/aes/siv.rb
|
42
46
|
- lib/miscreant/internals.rb
|
43
|
-
- lib/miscreant/internals/aes.rb
|
44
|
-
- lib/miscreant/internals/aes/
|
45
|
-
- lib/miscreant/internals/
|
47
|
+
- lib/miscreant/internals/aes/block_cipher.rb
|
48
|
+
- lib/miscreant/internals/aes/ctr.rb
|
49
|
+
- lib/miscreant/internals/block.rb
|
46
50
|
- lib/miscreant/internals/util.rb
|
47
51
|
- lib/miscreant/version.rb
|
48
52
|
- miscreant.gemspec
|
@@ -1,15 +0,0 @@
|
|
1
|
-
# encoding: binary
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
module Miscreant
|
5
|
-
module Internals
|
6
|
-
# The Advanced Encryption Standard Block Cipher
|
7
|
-
module AES # :nodoc:
|
8
|
-
# Size of an AES block (i.e. input/output from the AES function)
|
9
|
-
BLOCK_SIZE = 16
|
10
|
-
|
11
|
-
# A bytestring of all zeroes, the same length as an AES block
|
12
|
-
ZERO_BLOCK = ("\0" * BLOCK_SIZE).freeze
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,79 +0,0 @@
|
|
1
|
-
# encoding: binary
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
module Miscreant
|
5
|
-
module Internals
|
6
|
-
module AES
|
7
|
-
# The AES-CMAC message authentication code
|
8
|
-
class CMAC # :nodoc:
|
9
|
-
# Create a new AES-CMAC instance
|
10
|
-
#
|
11
|
-
# @param key [String] 16-byte or 32-byte Encoding::BINARY cryptographic key
|
12
|
-
#
|
13
|
-
# @return [Miscreant::AES::CMAC] new AES-CMAC instance
|
14
|
-
def initialize(key)
|
15
|
-
raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
|
16
|
-
raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
|
17
|
-
raise ArgumentError, "key must be 32 or 64 bytes" unless [16, 32].include?(key.length)
|
18
|
-
|
19
|
-
# The only valid use of ECB mode: constructing higher-level cryptographic primitives
|
20
|
-
@cipher = OpenSSL::Cipher.new("AES-#{key.length * 8}-ECB")
|
21
|
-
@cipher.encrypt
|
22
|
-
@cipher.padding = 0
|
23
|
-
@cipher.key = key
|
24
|
-
@key1, @key2 = _generate_subkeys
|
25
|
-
end
|
26
|
-
|
27
|
-
# Inspect this AES-CMAC instance
|
28
|
-
#
|
29
|
-
# @return [String] description of this instance
|
30
|
-
def inspect
|
31
|
-
to_s
|
32
|
-
end
|
33
|
-
|
34
|
-
# Compute the AES-CMAC of the given input message in a single shot,
|
35
|
-
# outputting the MAC tag.
|
36
|
-
#
|
37
|
-
# Unlike other AES-CMAC implementations, this one does not support
|
38
|
-
# incremental processing/IUF operation. (Though that would enable
|
39
|
-
# slightly more efficient decryption for AES-SIV)
|
40
|
-
#
|
41
|
-
# @param message [String] an Encoding::BINARY string to authenticate
|
42
|
-
#
|
43
|
-
# @return [String] CMAC tag
|
44
|
-
def digest(message)
|
45
|
-
raise TypeError, "expected String, got #{message.class}" unless message.is_a?(String)
|
46
|
-
raise ArgumentError, "message must be Encoding::BINARY" unless message.encoding == Encoding::BINARY
|
47
|
-
|
48
|
-
if message.empty? || message.length % AES::BLOCK_SIZE != 0
|
49
|
-
message = Util.pad(message, AES::BLOCK_SIZE)
|
50
|
-
final_block = @key2
|
51
|
-
else
|
52
|
-
final_block = @key1
|
53
|
-
end
|
54
|
-
|
55
|
-
count = message.length / AES::BLOCK_SIZE
|
56
|
-
result = AES::ZERO_BLOCK
|
57
|
-
|
58
|
-
count.times do |i|
|
59
|
-
block = message.slice(AES::BLOCK_SIZE * i, AES::BLOCK_SIZE)
|
60
|
-
block = Util.xor(final_block, block) if i == count - 1
|
61
|
-
block = Util.xor(block, result)
|
62
|
-
result = @cipher.update(block) + @cipher.final
|
63
|
-
end
|
64
|
-
|
65
|
-
result
|
66
|
-
end
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
def _generate_subkeys
|
71
|
-
key0 = @cipher.update(AES::ZERO_BLOCK) + @cipher.final
|
72
|
-
key1 = Util.dbl(key0)
|
73
|
-
key2 = Util.dbl(key1)
|
74
|
-
[key1, key2]
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
@@ -1,120 +0,0 @@
|
|
1
|
-
# encoding: binary
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
module Miscreant
|
5
|
-
module Internals
|
6
|
-
module AES
|
7
|
-
# The AES-SIV misuse resistant authenticated encryption cipher
|
8
|
-
class SIV # :nodoc:
|
9
|
-
# Generate a new random AES-SIV key of the given size
|
10
|
-
#
|
11
|
-
# @param size [Integer] size of key in bytes (32 or 64)
|
12
|
-
#
|
13
|
-
# @return [String] newly generated AES-SIV key
|
14
|
-
def self.generate_key(size = 32)
|
15
|
-
raise ArgumentError, "key size must be 32 or 64 bytes" unless [32, 64].include?(size)
|
16
|
-
SecureRandom.random_bytes(size)
|
17
|
-
end
|
18
|
-
|
19
|
-
# Create a new AES-SIV instance
|
20
|
-
#
|
21
|
-
# @param key [String] 32-byte or 64-byte Encoding::BINARY cryptographic key
|
22
|
-
#
|
23
|
-
# @return [Miscreant::AES::SIV] new AES-SIV instance
|
24
|
-
def initialize(key)
|
25
|
-
raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
|
26
|
-
raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
|
27
|
-
raise ArgumentError, "key must be 32 or 64 bytes" unless [32, 64].include?(key.length)
|
28
|
-
|
29
|
-
length = key.length / 2
|
30
|
-
|
31
|
-
@mac_key = key.slice(0, length)
|
32
|
-
@enc_key = key.slice(length..-1)
|
33
|
-
end
|
34
|
-
|
35
|
-
# Inspect this AES-SIV instance
|
36
|
-
#
|
37
|
-
# @return [String] description of this instance
|
38
|
-
def inspect
|
39
|
-
to_s
|
40
|
-
end
|
41
|
-
|
42
|
-
# Encrypt a message using AES-SIV, authenticating it along with the associated data
|
43
|
-
#
|
44
|
-
# @param plaintext [String] an Encoding::BINARY string to encrypt
|
45
|
-
# @param associated_data [Array<String>] optional array of message headers to authenticate
|
46
|
-
#
|
47
|
-
# @return [String] encrypted ciphertext
|
48
|
-
def seal(plaintext, associated_data = [])
|
49
|
-
raise TypeError, "expected String, got #{plaintext.class}" unless plaintext.is_a?(String)
|
50
|
-
raise ArgumentError, "plaintext must be Encoding::BINARY" unless plaintext.encoding == Encoding::BINARY
|
51
|
-
|
52
|
-
v = _s2v(associated_data, plaintext)
|
53
|
-
ciphertext = _transform(v, plaintext)
|
54
|
-
v + ciphertext
|
55
|
-
end
|
56
|
-
|
57
|
-
# Verify and decrypt an AES-SIV ciphertext, authenticating it along with the associated data
|
58
|
-
#
|
59
|
-
# @param ciphertext [String] an Encoding::BINARY string to decrypt
|
60
|
-
# @param associated_data [Array<String>] optional array of message headers to authenticate
|
61
|
-
#
|
62
|
-
# @raise [Miscreant::IntegrityError] ciphertext and/or associated data are corrupt or tampered with
|
63
|
-
# @return [String] decrypted plaintext
|
64
|
-
def open(ciphertext, associated_data = [])
|
65
|
-
raise TypeError, "expected String, got #{ciphertext.class}" unless ciphertext.is_a?(String)
|
66
|
-
raise ArgumentError, "ciphertext must be Encoding::BINARY" unless ciphertext.encoding == Encoding::BINARY
|
67
|
-
|
68
|
-
v = ciphertext.slice(0, AES::BLOCK_SIZE)
|
69
|
-
ciphertext = ciphertext.slice(AES::BLOCK_SIZE..-1)
|
70
|
-
plaintext = _transform(v, ciphertext)
|
71
|
-
|
72
|
-
t = _s2v(associated_data, plaintext)
|
73
|
-
raise IntegrityError, "ciphertext verification failure!" unless Util.ct_equal(t, v)
|
74
|
-
|
75
|
-
plaintext
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
# Performs raw unauthenticted encryption or decryption of the message
|
81
|
-
def _transform(v, data)
|
82
|
-
return "".b if data.empty?
|
83
|
-
|
84
|
-
cipher = OpenSSL::Cipher::AES.new(@mac_key.length * 8, :CTR)
|
85
|
-
cipher.encrypt
|
86
|
-
cipher.iv = Util.zero_iv_bits(v)
|
87
|
-
cipher.key = @enc_key
|
88
|
-
cipher.update(data) + cipher.final
|
89
|
-
end
|
90
|
-
|
91
|
-
# The S2V operation consists of the doubling and XORing of the outputs
|
92
|
-
# of the pseudo-random function CMAC.
|
93
|
-
#
|
94
|
-
# See Section 2.4 of RFC 5297 for more information
|
95
|
-
def _s2v(associated_data, plaintext)
|
96
|
-
# Note: the standalone S2V returns CMAC(1) if the number of passed
|
97
|
-
# vectors is zero, however in SIV construction this case is never
|
98
|
-
# triggered, since we always pass plaintext as the last vector (even
|
99
|
-
# if it's zero-length), so we omit this case.
|
100
|
-
cmac = CMAC.new(@mac_key)
|
101
|
-
d = cmac.digest(AES::ZERO_BLOCK)
|
102
|
-
|
103
|
-
associated_data.each do |ad|
|
104
|
-
d = Util.dbl(d)
|
105
|
-
d = Util.xor(d, cmac.digest(ad))
|
106
|
-
end
|
107
|
-
|
108
|
-
if plaintext.bytesize >= AES::BLOCK_SIZE
|
109
|
-
d = Util.xorend(plaintext, d)
|
110
|
-
else
|
111
|
-
d = Util.dbl(d)
|
112
|
-
d = Util.xor(d, Util.pad(plaintext, AES::BLOCK_SIZE))
|
113
|
-
end
|
114
|
-
|
115
|
-
cmac.digest(d)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|