Skip to content

Commit c3981ff

Browse files
committed
Merge branch 'advisory-fix-1'
2 parents 13f503a + 01d5050 commit c3981ff

File tree

5 files changed

+728
-109
lines changed

5 files changed

+728
-109
lines changed

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,28 @@
33
This file contains all of the notable changes from Jervis releases. For the
44
full change log see the commit log.
55

6+
# jervis 2.2
7+
8+
## Bug fixes
9+
10+
- SecurityIO.sha256sum padding bug fixed.
11+
- SecurityIO switching to `AES/GCM/NoPadding` and old AES functions marked as
12+
deprecated to be removed in Jervis 2.3.
13+
- SecurityIO.avoidTimingAttack uses cryptographically secure PRNG.
14+
- SecurityIO.verifyJsonWebToken does additional JWT structure validation.
15+
- SecurityIO.isBase64 now checks for base64 padding. Previously, it was
16+
possible for a String to pass as base64 characters but not be actually valid
17+
base64.
18+
- SecurityIO.sha256Sum hex string padding fixed so resulting checksum is always
19+
valid for 3rd party checksum functions.
20+
21+
## Breaking changes
22+
23+
- `CipherMap` encrypted data will be discarded when upgrading to Jervis 2.2.
24+
Currently, this only affects GitHub app authentication in Jervis and new
25+
tokens will be issued instead of reusing old tokens within their previous
26+
expiration. In general, GitHub app issued tokens expire after one hour.
27+
628
# jervis 2.1
729

830
## Bug fixes

src/main/groovy/net/gleske/jervis/tools/CipherMap.groovy

Lines changed: 50 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -42,29 +42,20 @@ import javax.crypto.BadPaddingException
4242
4343
<ul>
4444
<li>
45-
Upon instantiation the AES cipher secret and initialization vector (IV) are
46-
randomly generated bytes. The random secret is 32 bytes and the random IV
47-
is 16 bytes.
45+
Upon instantiation the AES cipher secret is randomly generated (32 bytes).
46+
A random 12-byte nonce is generated for each encryption operation.
4847
</li>
4948
<li>
50-
The cipher secret and IV are asymmetrically encrypted with RSA. The
51-
stronger the RSA key provided the more secure the encryption at rest.
49+
The cipher secret is asymmetrically encrypted with RSA using OAEP padding.
50+
OAEP padding prevents Bleichenbacher padding oracle attacks. The stronger
51+
the RSA key provided the more secure the encryption at rest.
5252
Keys below 2048-bits will throw an exception for being too weak.
5353
Recommended RSA private key size is 4096-bit.
5454
</li>
5555
<li>
56-
The data is encrypted with AES-256 CBC with PKCS5 padding. The cipher
57-
secret and IV are the inputs for encryption and decryption.
58-
</li>
59-
<li>
60-
The random IV is hashed with 5000 iterations of SHA-256 (let's call this
61-
initialization hash) and the resulting hash is passed into PBKDF2 with Hmac
62-
SHA-256 (PBKDF for short). The PBKDF has variable iterations based on the
63-
provided initialization hash. The iterations for PBKDF range from 100100
64-
to 960000 iterations. Since this is configurable via
65-
<tt>{@link #hash_iterations}</tt> it's possible to fully disable this
66-
behavior (setting <tt>hash_iterations</tt> to zero) and only use the
67-
provided secret and IV unmodified.
56+
The data is encrypted with AES-256-GCM authenticated encryption.
57+
GCM mode provides both confidentiality and integrity protection,
58+
preventing padding oracle attacks and detecting tampering.
6859
</li>
6960
<li>
7061
After encryption, the encrypted value is signed with an RS256 signature
@@ -73,10 +64,9 @@ import javax.crypto.BadPaddingException
7364
An empty map is the default if the signature is invalid.
7465
</li>
7566
<li>
76-
The cipher secret and IV infrequently change to protect against RSA attack
77-
utilizing Chinese Remainder Theorem. The cipher secret and IV are
78-
automatically rotated if the secrets (secret/IV) are older than 30 days
79-
when data is encrypted.
67+
The cipher secret infrequently changes to protect against RSA attack
68+
utilizing Chinese Remainder Theorem. The cipher secret is automatically
69+
rotated if the secret is older than 30 days when data is encrypted.
8070
</li>
8171
</ul>
8272
@@ -113,15 +103,7 @@ timing = time {
113103
114104
println("Time to load from String and decrypt: ${timing} second(s)")
115105
116-
// re-encrypt with stronger security
117-
def cmap3 = new CipherMap(new File('src/test/resources/rsa_keys/good_id_rsa_4096').text)
118-
cmap3.hash_iterations = 100100
119-
120-
timing = time {
121-
cmap3.plainMap = cmap1.plainMap
122-
}
123-
println("Time migrating to stronger encryption with 100100 hash iterations: ${timing} second(s)")
124-
println(['\n', '='*80, 'Encrypted contents with CipherMap toString()'.with { ' '*(40 - it.size()/2) + it }, '='*80, "\n${cmap3}"].join('\n'))
106+
println(['\n', '='*80, 'Encrypted contents with CipherMap toString()'.with { ' '*(40 - it.size()/2) + it }, '='*80, "\n${cmap1}"].join('\n'))
125107
</code></pre>
126108
*/
127109
class CipherMap implements Serializable {
@@ -136,16 +118,17 @@ class CipherMap implements Serializable {
136118
private transient Map hidden
137119

138120
/**
139-
Customize the number of SHA-256 hash iterations performed during AES
140-
encryption operations.
121+
Deprecated: This field is no longer used. AES-256-GCM mode uses random
122+
nonces generated for each encryption operation instead of derived IVs.
141123
142-
@see net.gleske.jervis.tools.SecurityIO#DEFAULT_AES_ITERATIONS
124+
@deprecated No longer used with AES-256-GCM authenticated encryption.
143125
*/
126+
@Deprecated
144127
Integer hash_iterations
145128

146129
/**
147-
The time limit in seconds before AES secret and IV need to be rotated.
148-
Once it reaches this age then new secrets will be generated. Default:
130+
The time limit in seconds before AES secret needs to be rotated.
131+
Once it reaches this age then a new secret will be generated. Default:
149132
<tt>2592000</tt> seconds (number of seconds in 30 days).
150133
151134
@see #setPlainMap(java.util.Map)
@@ -159,17 +142,18 @@ class CipherMap implements Serializable {
159142
@param privateKey A PKCS1 or PKCS8 private key PEM.
160143
*/
161144
CipherMap(String privateKey) {
162-
this(privateKey, SecurityIO.DEFAULT_AES_ITERATIONS)
145+
this.security = new SecurityIO(privateKey)
163146
}
164147

165148
/**
166149
Instantiates a new CipherMap object with the given private key. This is
167150
used for asymmetric encryption wrapping symmetric encryption.
168151
169-
@see #hash_iterations
152+
@deprecated The hash_iterations parameter is no longer used with AES-256-GCM.
170153
@param privateKey A PKCS1 or PKCS8 private key PEM.
171-
@param hash_iterations Customize the hash iterations on instantiation.
154+
@param hash_iterations Deprecated, ignored. AES-GCM uses random nonces.
172155
*/
156+
@Deprecated
173157
CipherMap(String privateKey, Integer hash_iterations) {
174158
this.hash_iterations = hash_iterations
175159
this.security = new SecurityIO(privateKey)
@@ -189,52 +173,47 @@ class CipherMap implements Serializable {
189173
Instantiates a new CipherMap object with the given private key. This is
190174
used for asymmetric encryption wrapping symmetric encryption.
191175
192-
@see #hash_iterations
176+
@deprecated The hash_iterations parameter is no longer used with AES-256-GCM.
193177
@param privateKey A PKCS1 or PKCS8 private key.
194-
@param hash_iterations Customize the hash iterations on instantiation.
178+
@param hash_iterations Deprecated, ignored. AES-GCM uses random nonces.
195179
*/
180+
@Deprecated
196181
CipherMap(File privateKey, Integer hash_iterations) {
197182
this(privateKey.text, hash_iterations)
198183
}
199184

200185
/**
201-
Encrypts the data with AES.
186+
Encrypts the data with AES-256-GCM authenticated encryption.
202187
203188
@param data To be encrypted.
204189
@return Returns encrypted String.
205190
*/
206191
private String encrypt(String data) {
207-
this.hidden.cipher.with { secret ->
208-
return security.encryptWithAES256Base64(
209-
security.encodeBase64(security.rsaDecryptBytes(security.decodeBase64Bytes(secret[0]))),
210-
security.encodeBase64(security.rsaDecryptBytes(security.decodeBase64Bytes(secret[1]))),
211-
data,
212-
this.hash_iterations
213-
)
214-
}
192+
String encryptedSecret = this.hidden.cipher
193+
// Decrypt the RSA-wrapped AES secret using OAEP padding
194+
byte[] aesSecret = security.rsaDecryptBytesOaep(security.decodeBase64Bytes(encryptedSecret))
195+
// Encrypt data with AES-256-GCM (nonce is generated automatically and prepended)
196+
SecurityIO.encryptWithAES256GCMBase64(security.encodeBase64(aesSecret), data)
215197
}
216198

217199
/**
218-
Decrypts the data with AES.
200+
Decrypts the data with AES-256-GCM authenticated decryption.
219201
@param data To be decrypted.
220202
@return Returns the plaintext data.
221203
*/
222204
private String decrypt(String data) {
223-
this.hidden.cipher.with { secret ->
224-
return security.decryptWithAES256Base64(
225-
security.encodeBase64(security.rsaDecryptBytes(security.decodeBase64Bytes(secret[0]))),
226-
security.encodeBase64(security.rsaDecryptBytes(security.decodeBase64Bytes(secret[1]))),
227-
data,
228-
this.hash_iterations
229-
)
230-
}
205+
String encryptedSecret = this.hidden.cipher
206+
// Decrypt the RSA-wrapped AES secret using OAEP padding
207+
byte[] aesSecret = security.rsaDecryptBytesOaep(security.decodeBase64Bytes(encryptedSecret))
208+
// Decrypt data with AES-256-GCM
209+
SecurityIO.decryptWithAES256GCMBase64(security.encodeBase64(aesSecret), data)
231210
}
232211

233212
/**
234213
Returns a string meant for signing and verifying signed data.
235214
*/
236215
private String signedData(Map obj) {
237-
([obj.age] + obj.cipher + [obj.data]).join('\n')
216+
[obj.age, obj.cipher, obj.data].join('\n')
238217
}
239218

240219
private Boolean verifyCipherObj(def obj) {
@@ -245,13 +224,10 @@ class CipherMap implements Serializable {
245224
if(!(['age', 'cipher', 'data', 'signature'] == obj.keySet().toList())) {
246225
return false
247226
}
248-
if(!((obj.cipher in List) && obj.cipher.size() == 2)) {
249-
return false
250-
}
227+
// cipher is now a single String (RSA-OAEP encrypted AES secret)
251228
Boolean stringCheck = [
252229
obj.age,
253-
obj.cipher[0],
254-
obj.cipher[1],
230+
obj.cipher,
255231
obj.data,
256232
obj.signature
257233
].every { it in String }
@@ -268,23 +244,19 @@ class CipherMap implements Serializable {
268244
}
269245

270246
/**
271-
Creates a new cipher either for the first time or as part of rotation.
272-
@return Returns a new random cipher secret and IV.
247+
Creates a new cipher secret using RSA-OAEP encryption.
248+
@return Returns a new RSA-OAEP encrypted random AES-256 secret.
273249
*/
274-
private List newCipher() {
275-
// Get the max size an RSA key can encrypt. Sometimes this is less
276-
// than 256 bytes which means padding will be required.
277-
Integer maxSize = [((security.getRsa_keysize() / 8) - 11), 256].min()
278-
[
279-
security.encodeBase64(security.rsaEncryptBytes(security.randomBytes(maxSize))),
280-
security.encodeBase64(security.rsaEncryptBytes(security.randomBytes(16)))
281-
]
250+
private String newCipher() {
251+
// Generate a 32-byte (256-bit) random AES secret
252+
// Use OAEP padding for RSA encryption to prevent Bleichenbacher attacks
253+
security.encodeBase64(security.rsaEncryptBytesOaep(SecurityIO.randomBytes(32)))
282254
}
283255

284256
private void initialize() {
285257
this.hidden = [
286258
age: '',
287-
cipher: ['', ''],
259+
cipher: '',
288260
data: '',
289261
signature: ''
290262
]
@@ -384,11 +356,9 @@ class CipherMap implements Serializable {
384356
Returns an encrypted object as text meant for storing at rest.
385357
386358
<pre><code>
387-
age: AES encrypted timestamp
388-
cipher:
389-
- asymmetrically encrypted AES secret
390-
- asymmetrically encrypted AES IV
391-
data: AES encrypted data
359+
age: AES-GCM encrypted timestamp
360+
cipher: RSA-OAEP encrypted AES-256 secret
361+
data: AES-GCM encrypted data (with nonce prepended)
392362
signature: RS256 Base64URL signature.
393363
</code></pre>
394364

0 commit comments

Comments
 (0)