@@ -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
114104println("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 */
127109class 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)
392362signature: RS256 Base64URL signature.
393363</code></pre>
394364
0 commit comments