|
1 | 1 | package org.cryptomator.siv; |
2 | 2 |
|
3 | 3 | import org.junit.jupiter.api.Assertions; |
| 4 | +import org.junit.jupiter.api.Assumptions; |
| 5 | +import org.junit.jupiter.api.BeforeAll; |
| 6 | +import org.junit.jupiter.api.DisplayName; |
4 | 7 | import org.junit.jupiter.api.DynamicContainer; |
5 | 8 | import org.junit.jupiter.api.DynamicTest; |
6 | 9 | import org.junit.jupiter.api.Nested; |
7 | 10 | import org.junit.jupiter.api.Test; |
8 | 11 | import org.junit.jupiter.api.TestFactory; |
| 12 | +import org.junit.jupiter.api.TestInstance; |
9 | 13 | import org.junit.jupiter.params.ParameterizedTest; |
| 14 | +import org.junit.jupiter.params.provider.FieldSource; |
10 | 15 | import org.junit.jupiter.params.provider.ValueSource; |
11 | 16 |
|
12 | 17 | import javax.crypto.AEADBadTagException; |
|
16 | 21 | import java.io.InputStream; |
17 | 22 | import java.io.InputStreamReader; |
18 | 23 | import java.io.Reader; |
19 | | -import java.io.UncheckedIOException; |
20 | 24 | import java.nio.charset.StandardCharsets; |
21 | 25 | import java.util.Arrays; |
| 26 | +import java.util.List; |
| 27 | +import java.util.stream.Collectors; |
22 | 28 | import java.util.stream.Stream; |
23 | 29 |
|
24 | 30 | /** |
@@ -293,115 +299,154 @@ public void testSivDecrypt() throws AEADBadTagException, IllegalBlockSizeExcepti |
293 | 299 |
|
294 | 300 | } |
295 | 301 |
|
296 | | - @TestFactory |
297 | | - public Stream<DynamicContainer> testGeneratedTestCases() { |
298 | | - InputStream in = EncryptionTestCase.class.getResourceAsStream("/testcases.txt"); |
299 | | - Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII); |
300 | | - BufferedReader bufferedReader = new BufferedReader(reader); |
301 | | - Stream<String> lines = bufferedReader.lines().onClose(() -> { |
302 | | - try { |
303 | | - bufferedReader.close(); |
304 | | - } catch (IOException e) { |
305 | | - throw new UncheckedIOException(e); |
| 302 | + @Nested |
| 303 | + @DisplayName("Generated Test Cases") |
| 304 | + @TestInstance(TestInstance.Lifecycle.PER_CLASS) |
| 305 | + public class GeneratedTestCases { |
| 306 | + |
| 307 | + private List<EncryptionTestCase> testCases; |
| 308 | + |
| 309 | + @BeforeAll |
| 310 | + public void loadTestCases() throws IOException { |
| 311 | + try (InputStream in = EncryptionTestCase.class.getResourceAsStream("/testcases.txt"); |
| 312 | + Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII); |
| 313 | + BufferedReader bufferedReader = new BufferedReader(reader)) { |
| 314 | + this.testCases = bufferedReader.lines().map(EncryptionTestCase::fromLine).collect(Collectors.toList()); |
306 | 315 | } |
307 | | - }); |
308 | | - return lines.map(EncryptionTestCase::fromLine).map(testCase -> { |
309 | | - int testIdx = testCase.getTestCaseNumber(); |
| 316 | + } |
| 317 | + |
| 318 | + @DisplayName("decrypt") |
| 319 | + @ParameterizedTest(name = "{0}") |
| 320 | + @FieldSource("testCases") |
| 321 | + public void testDecrypt(EncryptionTestCase testCase) throws AEADBadTagException, IllegalBlockSizeException { |
| 322 | + SivEngine siv = new SivEngine(testCase.getKey()); |
| 323 | + byte[] actualPlaintext = siv.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); |
| 324 | + Assertions.assertArrayEquals(testCase.getPlaintext(), actualPlaintext); |
| 325 | + } |
| 326 | + |
| 327 | + @DisplayName("encrypt") |
| 328 | + @ParameterizedTest(name = "{0}") |
| 329 | + @FieldSource("testCases") |
| 330 | + public void testEncrypt(EncryptionTestCase testCase) { |
| 331 | + SivEngine siv = new SivEngine(testCase.getKey()); |
| 332 | + byte[] actualCiphertext = siv.encrypt(testCase.getPlaintext(), testCase.getAssociatedData()); |
| 333 | + Assertions.assertArrayEquals(testCase.getCiphertext(), actualCiphertext); |
| 334 | + } |
| 335 | + |
| 336 | + @DisplayName("decrypt fails due to tampered mac key") |
| 337 | + @ParameterizedTest(name = "{0}") |
| 338 | + @FieldSource("testCases") |
| 339 | + public void testDecryptFailsDueToTamperedMacKey(EncryptionTestCase testCase) { |
| 340 | + byte[] key = testCase.getKey(); |
| 341 | + |
| 342 | + // Pick some arbitrary byte from first half of key (i.e. the MAC key) to tamper with |
| 343 | + int halfKeyLen = key.length / 2; |
| 344 | + int tamperedByteIndex = testCase.getTestCaseNumber() % halfKeyLen; |
| 345 | + |
| 346 | + // Flip a single bit |
| 347 | + key[tamperedByteIndex] ^= 0x10; |
| 348 | + |
| 349 | + SivEngine sivWithTamperedKey = new SivEngine(key); |
| 350 | + |
| 351 | + Assertions.assertThrows(AEADBadTagException.class, () -> { |
| 352 | + sivWithTamperedKey.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); |
| 353 | + }); |
| 354 | + } |
| 355 | + |
| 356 | + @DisplayName("decrypt fails due to tampered ciphertext") |
| 357 | + @ParameterizedTest(name = "{0}") |
| 358 | + @FieldSource("testCases") |
| 359 | + public void testDecryptFailsDueToTamperedCiphertext(EncryptionTestCase testCase) { |
| 360 | + SivEngine siv = new SivEngine(testCase.getKey()); |
| 361 | + byte[] ciphertext = testCase.getCiphertext(); |
| 362 | + |
| 363 | + // Pick some arbitrary key byte to tamper with |
| 364 | + int tamperedByteIndex = testCase.getTestCaseNumber() % ciphertext.length; |
| 365 | + |
| 366 | + // Flip a single bit |
| 367 | + ciphertext[tamperedByteIndex] ^= 0x10; |
| 368 | + |
| 369 | + Assertions.assertThrows(AEADBadTagException.class, () -> { |
| 370 | + siv.decrypt(ciphertext, testCase.getAssociatedData()); |
| 371 | + }); |
| 372 | + } |
| 373 | + |
| 374 | + @DisplayName("decrypt fails due to tampered associated data") |
| 375 | + @ParameterizedTest(name = "{0}") |
| 376 | + @FieldSource("testCases") |
| 377 | + public void testDecryptFailsDueToTamperedAAD(EncryptionTestCase testCase) { |
| 378 | + // Skip if there is no AD |
| 379 | + if (testCase.getAssociatedData().length == 0) { |
| 380 | + return; |
| 381 | + } |
| 382 | + |
310 | 383 | SivEngine siv = new SivEngine(testCase.getKey()); |
311 | | - return DynamicContainer.dynamicContainer("test case " + testIdx, Arrays.asList( |
312 | | - DynamicTest.dynamicTest("decrypt", () -> { |
313 | | - byte[] actualPlaintext = siv.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); |
314 | | - Assertions.assertArrayEquals(testCase.getPlaintext(), actualPlaintext); |
315 | | - }), |
316 | | - DynamicTest.dynamicTest("encrypt", () -> { |
317 | | - byte[] actualCiphertext = siv.encrypt(testCase.getPlaintext(), testCase.getAssociatedData()); |
318 | | - Assertions.assertArrayEquals(testCase.getCiphertext(), actualCiphertext); |
319 | | - }), |
320 | | - DynamicTest.dynamicTest("decrypt fails due to tampered mac key", () -> { |
321 | | - byte[] key = testCase.getKey(); |
322 | | - |
323 | | - // Pick some arbitrary byte from first half of key (i.e. the MAC key) to tamper with |
324 | | - int halfKeyLen = key.length / 2; |
325 | | - int tamperedByteIndex = testIdx % halfKeyLen; |
326 | | - |
327 | | - // Flip a single bit |
328 | | - key[tamperedByteIndex] ^= 0x10; |
329 | | - |
330 | | - SivEngine sivWithTamperedKey = new SivEngine(key); |
331 | | - |
332 | | - Assertions.assertThrows(AEADBadTagException.class, () -> { |
333 | | - sivWithTamperedKey.decrypt(testCase.getCiphertext(), testCase.getAssociatedData()); |
334 | | - }); |
335 | | - }), |
336 | | - DynamicTest.dynamicTest("decrypt fails due to tampered ciphertext", () -> { |
337 | | - byte[] ciphertext = testCase.getCiphertext(); |
338 | | - |
339 | | - // Pick some arbitrary key byte to tamper with |
340 | | - int tamperedByteIndex = testIdx % ciphertext.length; |
341 | | - |
342 | | - // Flip a single bit |
343 | | - ciphertext[tamperedByteIndex] ^= 0x10; |
344 | | - |
345 | | - Assertions.assertThrows(AEADBadTagException.class, () -> { |
346 | | - siv.decrypt(ciphertext, testCase.getAssociatedData()); |
347 | | - }); |
348 | | - }), |
349 | | - DynamicTest.dynamicTest("decrypt fails due to tampered associated data", () -> { |
350 | | - byte[][] ad = testCase.getAssociatedData(); |
351 | | - |
352 | | - // Try flipping bits in the associated data elements |
353 | | - for (int adIdx = 0; adIdx < ad.length; adIdx++) { |
354 | | - // Skip if this ad element is empty |
355 | | - if (ad[adIdx].length == 0) { |
356 | | - continue; |
357 | | - } |
358 | | - |
359 | | - // Pick some arbitrary byte to tamper with |
360 | | - int tamperedByteIndex = testIdx % ad[adIdx].length; |
361 | | - |
362 | | - // Flip a single bit |
363 | | - ad[adIdx][tamperedByteIndex] ^= 0x04; |
364 | | - |
365 | | - Assertions.assertThrows(AEADBadTagException.class, () -> { |
366 | | - siv.decrypt(testCase.getCiphertext(), ad); |
367 | | - }); |
368 | | - |
369 | | - // Restore ad to original value |
370 | | - ad[adIdx][tamperedByteIndex] ^= 0x04; |
371 | | - } |
372 | | - }), |
373 | | - DynamicTest.dynamicTest("decrypt fails due to prepended associated data", () -> { |
374 | | - // Skip if there is no more room for additional AD |
375 | | - if (testCase.getAssociatedData().length > 125) { |
376 | | - return; |
377 | | - } |
378 | | - |
379 | | - byte[][] ad = testCase.getAssociatedData(); |
380 | | - byte[][] prependedAd = new byte[ad.length + 1][]; |
381 | | - prependedAd[0] = new byte[testIdx % 16]; |
382 | | - System.arraycopy(ad, 0, prependedAd, 1, ad.length); |
383 | | - |
384 | | - Assertions.assertThrows(AEADBadTagException.class, () -> { |
385 | | - siv.decrypt(testCase.getCiphertext(), prependedAd); |
386 | | - }); |
387 | | - }), |
388 | | - DynamicTest.dynamicTest("decrypt fails due to appended associated data", () -> { |
389 | | - // Skip if there is no more room for additional AD |
390 | | - if (testCase.getAssociatedData().length > 125) { |
391 | | - return; |
392 | | - } |
393 | | - |
394 | | - byte[][] ad = testCase.getAssociatedData(); |
395 | | - byte[][] appendedAd = new byte[ad.length + 1][]; |
396 | | - appendedAd[ad.length] = new byte[testIdx % 16]; |
397 | | - System.arraycopy(ad, 0, appendedAd, 0, ad.length); |
398 | | - |
399 | | - Assertions.assertThrows(AEADBadTagException.class, () -> { |
400 | | - siv.decrypt(testCase.getCiphertext(), appendedAd); |
401 | | - }); |
402 | | - }) |
403 | | - )); |
404 | | - }); |
| 384 | + byte[][] ad = testCase.getAssociatedData(); |
| 385 | + |
| 386 | + // Try flipping bits in the associated data elements |
| 387 | + for (int adIdx = 0; adIdx < ad.length; adIdx++) { |
| 388 | + // Skip if this ad element is empty |
| 389 | + if (ad[adIdx].length == 0) { |
| 390 | + continue; |
| 391 | + } |
| 392 | + |
| 393 | + // Pick some arbitrary byte to tamper with |
| 394 | + int tamperedByteIndex = testCase.getTestCaseNumber() % ad[adIdx].length; |
| 395 | + |
| 396 | + // Flip a single bit |
| 397 | + ad[adIdx][tamperedByteIndex] ^= 0x04; |
| 398 | + |
| 399 | + Assertions.assertThrows(AEADBadTagException.class, () -> { |
| 400 | + siv.decrypt(testCase.getCiphertext(), ad); |
| 401 | + }); |
| 402 | + |
| 403 | + // Restore ad to original value |
| 404 | + ad[adIdx][tamperedByteIndex] ^= 0x04; |
| 405 | + } |
| 406 | + } |
| 407 | + |
| 408 | + @DisplayName("decrypt fails due to prepended associated data") |
| 409 | + @ParameterizedTest(name = "{0}") |
| 410 | + @FieldSource("testCases") |
| 411 | + public void testDecryptFailsDueToPrependedAAD(EncryptionTestCase testCase) { |
| 412 | + // Skip if there is no more room for additional AD |
| 413 | + if (testCase.getAssociatedData().length > 125) { |
| 414 | + return; |
| 415 | + } |
| 416 | + |
| 417 | + SivEngine siv = new SivEngine(testCase.getKey()); |
| 418 | + |
| 419 | + byte[][] ad = testCase.getAssociatedData(); |
| 420 | + byte[][] prependedAd = new byte[ad.length + 1][]; |
| 421 | + prependedAd[0] = new byte[testCase.getTestCaseNumber() % 16]; |
| 422 | + System.arraycopy(ad, 0, prependedAd, 1, ad.length); |
| 423 | + |
| 424 | + Assertions.assertThrows(AEADBadTagException.class, () -> { |
| 425 | + siv.decrypt(testCase.getCiphertext(), prependedAd); |
| 426 | + }); |
| 427 | + } |
| 428 | + |
| 429 | + @DisplayName("decrypt fails due to appended associated data") |
| 430 | + @ParameterizedTest(name = "{0}") |
| 431 | + @FieldSource("testCases") |
| 432 | + public void testDecryptFailsDueToAppendedAAD(EncryptionTestCase testCase) { |
| 433 | + // Skip if there is no more room for additional AD |
| 434 | + if (testCase.getAssociatedData().length > 125) { |
| 435 | + return; |
| 436 | + } |
| 437 | + |
| 438 | + SivEngine siv = new SivEngine(testCase.getKey()); |
| 439 | + |
| 440 | + byte[][] ad = testCase.getAssociatedData(); |
| 441 | + byte[][] appendedAd = new byte[ad.length + 1][]; |
| 442 | + appendedAd[ad.length] = new byte[testCase.getTestCaseNumber() % 16]; |
| 443 | + System.arraycopy(ad, 0, appendedAd, 0, ad.length); |
| 444 | + |
| 445 | + Assertions.assertThrows(AEADBadTagException.class, () -> { |
| 446 | + siv.decrypt(testCase.getCiphertext(), appendedAd); |
| 447 | + }); |
| 448 | + } |
| 449 | + |
405 | 450 | } |
406 | 451 |
|
407 | 452 | } |
0 commit comments