@@ -411,7 +411,7 @@ public Manifest getManifest() {
411411 this .unencryptedMetadata = unencryptedMetadata ;
412412 }
413413
414- public void readPayload (OutputStream outputStream ) throws SDK .SegmentSignatureMismatch , IOException {
414+ public void readPayload (OutputStream outputStream ) throws SDK .TamperException , IOException {
415415
416416 MessageDigest digest = null ;
417417 try {
@@ -438,13 +438,10 @@ public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMi
438438 var isLegacyTdf = manifest .tdfVersion == null || manifest .tdfVersion .isEmpty ();
439439
440440 if (manifest .payload .isEncrypted ) {
441- String segHashAlg = manifest .encryptionInformation .integrityInformation .segmentHashAlg ;
442- Config .IntegrityAlgorithm sigAlg = Config .IntegrityAlgorithm .HS256 ;
443- if (segHashAlg .compareToIgnoreCase (kGmacIntegrityAlgorithm ) == 0 ) {
444- sigAlg = Config .IntegrityAlgorithm .GMAC ;
445- }
441+ var sigAlg = segmentIntegrityAlgorithmFromManifest (
442+ manifest .encryptionInformation .integrityInformation .segmentHashAlg );
446443
447- var payloadSig = calculateSignature (readBuf , payloadKey , sigAlg );
444+ var payloadSig = segmentIntegrity (readBuf , payloadKey , sigAlg );
448445 if (isLegacyTdf ) {
449446 payloadSig = Hex .encodeHexString (payloadSig ).getBytes (StandardCharsets .UTF_8 );
450447 }
@@ -470,22 +467,164 @@ public void readPayload(OutputStream outputStream) throws SDK.SegmentSignatureMi
470467 public PolicyObject readPolicyObject () {
471468 return tdfReader .readPolicyObject ();
472469 }
473- }
474470
475- private static byte [] calculateSignature (byte [] data , byte [] secret , Config .IntegrityAlgorithm algorithm ) {
476- if (algorithm == Config .IntegrityAlgorithm .HS256 ) {
477- return CryptoUtils .CalculateSHA256Hmac (secret , data );
471+ /**
472+ * Resolves {@code segmentHashAlg} as read from the manifest. Both algorithms are
473+ * allowed: a GMAC segment hash proves nothing by itself, but unlike the root it is
474+ * bracketed by keyed checks that do (see {@link TDF#aeadTag}). An unrecognized name
475+ * is still refused rather than defaulted. Contrast
476+ * {@link TDF#rootIntegrityAlgorithmFromManifest}, where only HS256 is meaningful.
477+ */
478+ private static Config .IntegrityAlgorithm segmentIntegrityAlgorithmFromManifest (String declared ) {
479+ if (declared != null ) {
480+ String name = declared .trim ();
481+ if (kGmacIntegrityAlgorithm .equalsIgnoreCase (name )) {
482+ return Config .IntegrityAlgorithm .GMAC ;
483+ }
484+ if (kHmacIntegrityAlgorithm .equalsIgnoreCase (name )) {
485+ return Config .IntegrityAlgorithm .HS256 ;
486+ }
487+ }
488+ // Not a SegmentSignatureMismatch: no signature was compared. Still a
489+ // TamperException, because segmentHashAlg is not covered by the root
490+ // signature and so is something an attacker can freely rewrite.
491+ throw new SDK .TamperException ("unsupported segment integrity algorithm: " + declared );
478492 }
493+ }
479494
480- if (kGMACPayloadLength > data .length ) {
495+ /**
496+ * Recovers the trailing AES-GCM authentication tag from a segment's ciphertext.
497+ * <p>
498+ * Recovering a tag is not verifying one. These are bytes whoever supplied the input
499+ * already holds, so comparing them against a manifest value is keyless and on its own
500+ * proves nothing — an attacker can re-chunk a payload and write each chunk's own
501+ * trailing sixteen bytes into its {@code segment.hash}. What makes a GMAC segment hash
502+ * trustworthy is the keyed checks around it: {@code loadTDF} has already validated the
503+ * whole list of segment hashes against the HS256 root signature, and {@code readPayload}
504+ * follows the comparison with a real AES-GCM tag check under the payload key.
505+ * <p>
506+ * The root signature has neither backstop — it is the outermost check, so a "GMAC root"
507+ * is a keyless comparison with nothing behind it. The asymmetry is therefore structural,
508+ * not a property of the bytes, and it is why {@link #rootIntegrity} does not offer this
509+ * algorithm.
510+ */
511+ private static byte [] aeadTag (byte [] ciphertext ) {
512+ if (kGMACPayloadLength > ciphertext .length ) {
481513 throw new IllegalArgumentException ("tried to calculate GMAC on too small a payload. payload is "
482- + data .length + "bytes while GMAC is " + kGMACPayloadLength + " bytes" );
514+ + ciphertext .length + " bytes while GMAC is " + kGMACPayloadLength + " bytes" );
515+ }
516+
517+ return Arrays .copyOfRange (ciphertext , ciphertext .length - kGMACPayloadLength , ciphertext .length );
518+ }
519+
520+ /**
521+ * The integrity value recorded in a segment's {@code hash}.
522+ *
523+ * @param ciphertext the AES-GCM output for this segment, whole and unmodified
524+ * @param key the payload key
525+ * @param algorithm {@code GMAC} to reuse the segment's own AEAD tag, or
526+ * {@code HS256} to HMAC the segment ciphertext
527+ * @throws IllegalArgumentException if {@code algorithm} is null or unsupported
528+ */
529+ static byte [] segmentIntegrity (byte [] ciphertext , byte [] key , Config .IntegrityAlgorithm algorithm ) {
530+ requireSupportedSegmentIntegrityAlgorithm (algorithm );
531+ switch (algorithm ) {
532+ case HS256 :
533+ return CryptoUtils .CalculateSHA256Hmac (key , ciphertext );
534+ case GMAC :
535+ return aeadTag (ciphertext );
536+ default :
537+ throw new IllegalArgumentException ("unsupported segment integrity algorithm: " + algorithm );
538+ }
539+ }
540+
541+ /**
542+ * The integrity value recorded in {@code rootSignature.sig}, over the concatenated
543+ * segment hashes.
544+ * <p>
545+ * HS256 only. The aggregate hash never passes through the AEAD, so there is no tag
546+ * to recover from it; a "GMAC" root signature is just a copy of the last segment
547+ * hash, which is attacker-controlled manifest data. Accepting one would let anyone
548+ * truncate, reorder, duplicate or drop segments without holding a key, since nothing
549+ * else binds a segment to its index or to the segment count.
550+ *
551+ * @throws IllegalArgumentException if {@code algorithm} is anything but HS256
552+ */
553+ static byte [] rootIntegrity (byte [] aggregateHash , byte [] key , Config .IntegrityAlgorithm algorithm ) {
554+ requireSupportedRootIntegrityAlgorithm (algorithm );
555+ return CryptoUtils .CalculateSHA256Hmac (key , aggregateHash );
556+ }
557+
558+ /**
559+ * The segment counterpart to {@link #requireSupportedRootIntegrityAlgorithm}. Both
560+ * algorithms are legal in this position, so this exists to reject {@code null} and any
561+ * future enum value in {@code createTDF} rather than partway through the payload:
562+ * TDFConfig's fields are public, so a field left unset arrives here as {@code null} and
563+ * would otherwise surface as a {@link NullPointerException} at the switch below, after
564+ * segments had already been written to the output stream.
565+ *
566+ * @throws IllegalArgumentException if {@code algorithm} cannot hash a segment
567+ */
568+ static void requireSupportedSegmentIntegrityAlgorithm (Config .IntegrityAlgorithm algorithm ) {
569+ if (algorithm != Config .IntegrityAlgorithm .HS256 && algorithm != Config .IntegrityAlgorithm .GMAC ) {
570+ throw new IllegalArgumentException ("unsupported segment integrity algorithm: " + algorithm );
571+ }
572+ }
573+
574+ /**
575+ * The write-path gate, and a second checkpoint inside {@link #rootIntegrity}.
576+ * <p>
577+ * An {@link IllegalArgumentException} rather than a {@link SDK.TamperException}, on
578+ * purpose. Reading, this is unreachable: {@link #rootIntegrityAlgorithmFromManifest}
579+ * has already narrowed the manifest's declaration to HS256 or thrown
580+ * {@link SDK.RootSignatureValidationException} trying. So if it ever does fire on a
581+ * read, the cause is a bug in this class rather than a hostile file, and it should
582+ * escape {@code loadTDF} uncaught instead of being reported to callers as tamper —
583+ * fail loud, and do not let a defect hide inside an exception type that callers
584+ * routinely handle.
585+ *
586+ * @throws IllegalArgumentException if {@code algorithm} cannot authenticate a root
587+ * signature
588+ */
589+ static void requireSupportedRootIntegrityAlgorithm (Config .IntegrityAlgorithm algorithm ) {
590+ if (algorithm != Config .IntegrityAlgorithm .HS256 ) {
591+ throw new IllegalArgumentException ("unsupported root integrity algorithm: " + algorithm
592+ + "; the root signature must be " + kHmacIntegrityAlgorithm );
483593 }
594+ }
484595
485- return Arrays .copyOfRange (data , data .length - kGMACPayloadLength , data .length );
596+ /**
597+ * Resolves {@code rootSignature.alg} as read from the (unauthenticated) manifest.
598+ * <p>
599+ * An allowlist, deliberately: anything other than HS256 — GMAC, an unknown name, an
600+ * empty string — is refused rather than being defaulted to HS256. Defaulting would
601+ * validate a downgraded manifest against an algorithm it does not declare.
602+ */
603+ private static Config .IntegrityAlgorithm rootIntegrityAlgorithmFromManifest (String declared ) {
604+ if (declared != null && kHmacIntegrityAlgorithm .equalsIgnoreCase (declared .trim ())) {
605+ return Config .IntegrityAlgorithm .HS256 ;
606+ }
607+ throw new SDK .RootSignatureValidationException ("unsupported root integrity algorithm: " + declared
608+ + "; the root signature must be " + kHmacIntegrityAlgorithm );
486609 }
487610
611+ /**
612+ * @throws IllegalArgumentException if {@code tdfConfig} selects an integrity algorithm
613+ * that cannot be written. Unchecked and not an
614+ * {@link SDKException}, matching how the config layer
615+ * already reports out-of-range values (see
616+ * {@link Config#withSegmentSize}): this is a caller
617+ * mistake to fix in code, not a condition to handle
618+ * alongside I/O and tamper failures.
619+ */
488620 TDFObject createTDF (InputStream payload , OutputStream outputStream , Config .TDFConfig tdfConfig ) throws SDKException , IOException {
621+ // Checked before anything is written so an unusable algorithm cannot produce a
622+ // partial TDF. There are no setters for these -- the config defaults to an HS256
623+ // root and GMAC segments -- but TDFConfig's fields are public, so re-check what
624+ // was actually set.
625+ requireSupportedRootIntegrityAlgorithm (tdfConfig .integrityAlgorithm );
626+ requireSupportedSegmentIntegrityAlgorithm (tdfConfig .segmentIntegrityAlgorithm );
627+
489628 Planner planner = new Planner (tdfConfig , services , Autoconfigure ::createGranter );
490629 Map <String , List <KASInfo >> splits = planner .getSplits ();
491630
@@ -526,7 +665,7 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo
526665 readBuf , 0 , readThisLoop );
527666 payloadOutput .write (cipherData );
528667
529- segmentSig = calculateSignature (cipherData , tdfObject .payloadKey , tdfConfig .segmentIntegrityAlgorithm );
668+ segmentSig = segmentIntegrity (cipherData , tdfObject .payloadKey , tdfConfig .segmentIntegrityAlgorithm );
530669 if (tdfConfig .hexEncodeRootAndSegmentHashes ) {
531670 segmentSig = Hex .encodeHexString (segmentSig ).getBytes (StandardCharsets .UTF_8 );
532671 }
@@ -542,18 +681,17 @@ TDFObject createTDF(InputStream payload, OutputStream outputStream, Config.TDFCo
542681
543682 Manifest .RootSignature rootSignature = new Manifest .RootSignature ();
544683
545- byte [] rootSig = calculateSignature (aggregateHash .toByteArray (), tdfObject .payloadKey ,
684+ byte [] rootSig = rootIntegrity (aggregateHash .toByteArray (), tdfObject .payloadKey ,
546685 tdfConfig .integrityAlgorithm );
547686 byte [] encodedRootSig = tdfConfig .hexEncodeRootAndSegmentHashes
548687 ? Hex .encodeHexString (rootSig ).getBytes (StandardCharsets .UTF_8 )
549688 : rootSig ;
550689 rootSignature .signature = Base64 .getEncoder ().encodeToString (encodedRootSig );
551690
552- String alg = kGmacIntegrityAlgorithm ;
553- if (tdfConfig .integrityAlgorithm == Config .IntegrityAlgorithm .HS256 ) {
554- alg = kHmacIntegrityAlgorithm ;
555- }
556- rootSignature .algorithm = alg ;
691+ // Unconditional. createTDF refuses any other root algorithm before a byte is
692+ // written and rootIntegrity would refuse it again; selecting on tdfConfig here
693+ // would leave a path that emits alg="GMAC" should either check ever be relaxed.
694+ rootSignature .algorithm = kHmacIntegrityAlgorithm ;
557695
558696 tdfObject .manifest .encryptionInformation .integrityInformation .rootSignature = rootSignature ;
559697 tdfObject .manifest .encryptionInformation .integrityInformation .segmentSizeDefault = tdfConfig .defaultSegmentSize ;
@@ -757,17 +895,21 @@ Reader loadTDF(SeekableByteChannel tdf, Config.TDFReaderConfig tdfReaderConfig)
757895 String rootSigValue ;
758896 boolean isLegacyTdf = manifest .tdfVersion == null || manifest .tdfVersion .isEmpty ();
759897 if (manifest .payload .isEncrypted ) {
760- Config .IntegrityAlgorithm sigAlg = Config .IntegrityAlgorithm .HS256 ;
761- if (rootAlgorithm .compareToIgnoreCase (kGmacIntegrityAlgorithm ) == 0 ) {
762- sigAlg = Config .IntegrityAlgorithm .GMAC ;
763- }
898+ var sigAlg = rootIntegrityAlgorithmFromManifest (rootAlgorithm );
764899
765- var sig = calculateSignature (aggregateHash .toByteArray (), payloadKey , sigAlg );
900+ var sig = rootIntegrity (aggregateHash .toByteArray (), payloadKey , sigAlg );
766901 if (isLegacyTdf ) {
767902 sig = Hex .encodeHexString (sig ).getBytes ();
768903 }
769904 rootSigValue = Base64 .getEncoder ().encodeToString (sig );
770905 } else {
906+ // KNOWN GAP, untouched by this change and tracked separately: this branch is a
907+ // bare SHA-256, so it authenticates nothing. `payload.isEncrypted` is itself
908+ // unauthenticated manifest data, so flipping it to false selects a keyless
909+ // verification path that anyone can satisfy -- and readPayload then emits the
910+ // segment bytes without decrypting them. Same downgrade shape as a GMAC root.
911+ // Left alone here because closing it means deciding whether unencrypted TDFs
912+ // are supported at all, which is a wider question than this fix.
771913 MessageDigest digest ;
772914 try {
773915 digest = MessageDigest .getInstance ("SHA-256" );
0 commit comments