|
1 | 1 | package org.bouncycastle.openpgp.operator.jcajce; |
2 | 2 |
|
| 3 | +import java.io.EOFException; |
3 | 4 | import java.io.IOException; |
4 | 5 | import java.io.InputStream; |
5 | 6 | import java.io.OutputStream; |
@@ -292,6 +293,7 @@ static class PGPAeadInputStream |
292 | 293 | private int dataOff; |
293 | 294 | private long chunkIndex = 0; |
294 | 295 | private long totalBytes = 0; |
| 296 | + private boolean aeadComplete = false; // set once the trailing message tag has been verified |
295 | 297 | private boolean v5StyleAEAD; |
296 | 298 |
|
297 | 299 | /** |
@@ -329,7 +331,7 @@ public PGPAeadInputStream(boolean isV5AEAD, InputStream in, |
329 | 331 | this.aaData = aaData; |
330 | 332 |
|
331 | 333 | // prime with 2 * tag len bytes. |
332 | | - Streams.readFully(in, buf, 0, aeadTagLength + aeadTagLength); |
| 334 | + readAeadFully(buf, 0, aeadTagLength + aeadTagLength); |
333 | 335 |
|
334 | 336 | // load the first block |
335 | 337 | this.data = readBlock(); |
@@ -409,9 +411,20 @@ private byte[] readBlock() |
409 | 411 | { |
410 | 412 | // we initialise with the first 16 bytes as there is an additional 16 bytes following |
411 | 413 | // the last chunk (which may not be the exact chunklength). |
412 | | - int dataLen = Streams.readFully(in, buf, aeadTagLength + aeadTagLength, chunkLength); |
| 414 | + int dataLen = readAeadFully(buf, aeadTagLength + aeadTagLength, chunkLength); |
413 | 415 | if (dataLen == 0) |
414 | 416 | { |
| 417 | + if (!aeadComplete) |
| 418 | + { |
| 419 | + // A chunk-aligned message (plaintext an exact multiple of chunkLength) ends after a |
| 420 | + // full-size final data chunk, so the trailing message tag was pre-read into the |
| 421 | + // look-ahead (buf[0..aeadTagLength)) but the full-size-chunk path below never verified it. |
| 422 | + // Verify it now, before signalling EOF, so that truncation at a chunk boundary |
| 423 | + // (dropping trailing chunks plus the final tag) is rejected rather than accepted as |
| 424 | + // authentic - RFC 9580 5.13.2 relies on the final tag to authenticate the total length. |
| 425 | + verifyFinalTag(); |
| 426 | + aeadComplete = true; |
| 427 | + } |
415 | 428 | return null; |
416 | 429 | } |
417 | 430 |
|
@@ -441,31 +454,55 @@ private byte[] readBlock() |
441 | 454 |
|
442 | 455 | if (dataLen != chunkLength) // it's our last block |
443 | 456 | { |
444 | | - adata = PGPAeadOutputStream.getAdata(v5StyleAEAD, aaData, chunkIndex, totalBytes); |
445 | | - try |
446 | | - { |
447 | | - if (v5StyleAEAD) |
448 | | - { |
449 | | - JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.DECRYPT_MODE, getNonce(iv, chunkIndex), 128, Arrays.concatenate(adata, Pack.longToBigEndian(totalBytes))); |
450 | | - } |
451 | | - else |
452 | | - { |
453 | | - JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.DECRYPT_MODE, getNonce(iv, chunkIndex), 128, adata); |
454 | | - } |
| 457 | + verifyFinalTag(); |
| 458 | + aeadComplete = true; |
| 459 | + } |
| 460 | + else |
| 461 | + { |
| 462 | + readAeadFully(buf, aeadTagLength, aeadTagLength); // read the next tag bytes |
| 463 | + } |
| 464 | + |
| 465 | + return decData; |
| 466 | + } |
| 467 | + |
| 468 | + // Verify the trailing message tag, which is held in buf[0..aeadTagLength). Called both for a short |
| 469 | + // final data chunk and for a chunk-aligned message whose final tag would otherwise be skipped. |
| 470 | + private void verifyFinalTag() |
| 471 | + throws IOException |
| 472 | + { |
| 473 | + byte[] adata = PGPAeadOutputStream.getAdata(v5StyleAEAD, aaData, chunkIndex, totalBytes); |
455 | 474 |
|
456 | | - c.doFinal(buf, 0, aeadTagLength); // check final tag |
| 475 | + try |
| 476 | + { |
| 477 | + if (v5StyleAEAD) |
| 478 | + { |
| 479 | + JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.DECRYPT_MODE, getNonce(iv, chunkIndex), 128, Arrays.concatenate(adata, Pack.longToBigEndian(totalBytes))); |
457 | 480 | } |
458 | | - catch (Exception e) |
| 481 | + else |
459 | 482 | { |
460 | | - throw new IOException("exception processing final tag: " + e.getMessage()); |
| 483 | + JceAEADCipherUtil.setUpAeadCipher(c, secretKey, Cipher.DECRYPT_MODE, getNonce(iv, chunkIndex), 128, adata); |
461 | 484 | } |
| 485 | + |
| 486 | + c.doFinal(buf, 0, aeadTagLength); // check final tag |
462 | 487 | } |
463 | | - else |
| 488 | + catch (Exception e) |
464 | 489 | { |
465 | | - Streams.readFully(in, buf, aeadTagLength, aeadTagLength); // read the next tag bytes |
| 490 | + throw new IOException("exception processing final tag: " + e.getMessage()); |
466 | 491 | } |
| 492 | + } |
467 | 493 |
|
468 | | - return decData; |
| 494 | + // truncation must not escape as an EOFException - nextPacketTag() reads one as a clean end of message |
| 495 | + private int readAeadFully(byte[] b, int off, int len) |
| 496 | + throws IOException |
| 497 | + { |
| 498 | + try |
| 499 | + { |
| 500 | + return Streams.readFully(in, b, off, len); |
| 501 | + } |
| 502 | + catch (EOFException e) |
| 503 | + { |
| 504 | + throw new IOException("truncated AEAD data: " + e.getMessage()); |
| 505 | + } |
469 | 506 | } |
470 | 507 | } |
471 | 508 |
|
|
0 commit comments