Skip to content

Commit 082a9e7

Browse files
authored
fix: if a version < 4.3.0 is specified create an old-style TDF (#234)
If a version less than 4.3.0 is specified: * encode hashes using a layer of hex-encoding * leave version information out of the manifest
1 parent f9eeb0d commit 082a9e7

File tree

7 files changed

+201
-11
lines changed

7 files changed

+201
-11
lines changed

cmdline/src/main/java/io/opentdf/platform/Command.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ void encrypt(
162162
@Option(names = {
163163
"--encap-key-type" }, defaultValue = Option.NULL_VALUE, description = "Preferred key access key wrap algorithm, one of ${COMPLETION-CANDIDATES}") Optional<KeyType> encapKeyType,
164164
@Option(names = { "--mime-type" }, defaultValue = Option.NULL_VALUE) Optional<String> mimeType,
165-
@Option(names = { "--with-assertions" }, defaultValue = Option.NULL_VALUE) Optional<String> assertion)
165+
@Option(names = { "--with-assertions" }, defaultValue = Option.NULL_VALUE) Optional<String> assertion,
166+
@Option(names = { "--with-target-mode" }, defaultValue = Option.NULL_VALUE) Optional<String> targetMode)
166167

167168
throws IOException, JOSEException, AutoConfigureException, InterruptedException, ExecutionException, DecoderException {
168169

@@ -214,9 +215,8 @@ void encrypt(
214215
configs.add(Config.withAssertionConfig(assertionConfigs));
215216
}
216217

217-
if (attributes.isPresent()) {
218-
configs.add(Config.withDataAttributes(attributes.get().split(",")));
219-
}
218+
attributes.ifPresent(s -> configs.add(Config.withDataAttributes(s.split(","))));
219+
targetMode.map(Config::withTargetMode).ifPresent(configs::add);
220220
var tdfConfig = Config.newTDFConfig(configs.toArray(Consumer[]::new));
221221
try (var in = file.isEmpty() ? new BufferedInputStream(System.in) : new FileInputStream(file.get())) {
222222
try (var out = new BufferedOutputStream(System.out)) {

sdk/src/main/java/io/opentdf/platform/sdk/Config.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ public static class TDFConfig {
140140
public String mimeType;
141141
public List<Autoconfigure.KeySplitStep> splitPlan;
142142
public KeyType wrappingKeyType;
143+
public boolean hexEncodeRootAndSegmentHashes;
144+
public boolean renderVersionInfoInManifest;
143145

144146
public TDFConfig() {
145147
this.autoconfigure = true;
@@ -154,6 +156,8 @@ public TDFConfig() {
154156
this.mimeType = DEFAULT_MIME_TYPE;
155157
this.splitPlan = new ArrayList<>();
156158
this.wrappingKeyType = KeyType.RSA2048Key;
159+
this.hexEncodeRootAndSegmentHashes = false;
160+
this.renderVersionInfoInManifest = true;
157161
}
158162
}
159163

@@ -251,6 +255,18 @@ public static Consumer<TDFConfig> withAutoconfigure(boolean enable) {
251255
};
252256
}
253257

258+
// specify TDF version for TDF creation to target. Versions less than 4.3.0 will add a
259+
// layer of hex encoding to their segment hashes and will not include version information
260+
// in their manifests.
261+
public static Consumer<TDFConfig> withTargetMode(String targetVersion) {
262+
Version version = new Version(targetVersion == null ? "0.0.0" : targetVersion);
263+
return (TDFConfig config) -> {
264+
var legacyTDF = version.compareTo(new Version("4.3.0")) < 0;
265+
config.renderVersionInfoInManifest = !legacyTDF;
266+
config.hexEncodeRootAndSegmentHashes = legacyTDF;
267+
};
268+
}
269+
254270
public static Consumer<TDFConfig> WithWrappingKeyAlg(KeyType keyType) {
255271
return (TDFConfig config) -> config.wrappingKeyType = keyType;
256272
}
@@ -393,4 +409,4 @@ public synchronized void updateHeaderInfo(HeaderInfo headerInfo) {
393409
this.notifyAll();
394410
}
395411
}
396-
}
412+
}

sdk/src/main/java/io/opentdf/platform/sdk/TDF.java

+9-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
1414
import org.apache.commons.codec.DecoderException;
1515
import org.apache.commons.codec.binary.Hex;
16-
import org.bouncycastle.crypto.digests.SHA256Digest;
1716
import org.bouncycastle.jce.interfaces.ECPublicKey;
1817
import org.slf4j.Logger;
1918
import org.slf4j.LoggerFactory;
@@ -222,7 +221,7 @@ PolicyObject createPolicyObject(List<Autoconfigure.AttributeValueFQN> attributes
222221
private static final Base64.Encoder encoder = Base64.getEncoder();
223222

224223
private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) {
225-
manifest.tdfVersion = TDF_VERSION;
224+
manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_VERSION : null;
226225
manifest.encryptionInformation.keyAccessType = kSplitKeyType;
227226
manifest.encryptionInformation.keyAccessObj = new ArrayList<>();
228227

@@ -541,6 +540,9 @@ public TDFObject createTDF(InputStream payload,
541540
payloadOutput.write(cipherData);
542541

543542
segmentSig = calculateSignature(cipherData, tdfObject.payloadKey, tdfConfig.segmentIntegrityAlgorithm);
543+
if (tdfConfig.hexEncodeRootAndSegmentHashes) {
544+
segmentSig = Hex.encodeHexString(segmentSig).getBytes(StandardCharsets.UTF_8);
545+
}
544546
segmentInfo.hash = Base64.getEncoder().encodeToString(segmentSig);
545547

546548
aggregateHash.write(segmentSig);
@@ -553,9 +555,11 @@ public TDFObject createTDF(InputStream payload,
553555

554556
Manifest.RootSignature rootSignature = new Manifest.RootSignature();
555557

556-
byte[] rootSig = calculateSignature(aggregateHash.toByteArray(),
557-
tdfObject.payloadKey, tdfConfig.integrityAlgorithm);
558-
rootSignature.signature = Base64.getEncoder().encodeToString(rootSig);
558+
byte[] rootSig = calculateSignature(aggregateHash.toByteArray(), tdfObject.payloadKey, tdfConfig.integrityAlgorithm);
559+
byte[] encodedRootSig = tdfConfig.hexEncodeRootAndSegmentHashes
560+
? Hex.encodeHexString(rootSig).getBytes(StandardCharsets.UTF_8)
561+
: rootSig;
562+
rootSignature.signature = Base64.getEncoder().encodeToString(encodedRootSig);
559563

560564
String alg = kGmacIntegrityAlgorithm;
561565
if (tdfConfig.integrityAlgorithm == Config.IntegrityAlgorithm.HS256) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import javax.annotation.Nonnull;
7+
import javax.annotation.Nullable;
8+
import java.util.Objects;
9+
import java.util.Optional;
10+
import java.util.regex.Pattern;
11+
12+
class Version implements Comparable<Version> {
13+
private final int major;
14+
private final int minor;
15+
private final int patch;
16+
private final String prereleaseAndMetadata;
17+
private static final Logger log = LoggerFactory.getLogger(Version.class);
18+
19+
Pattern SEMVER_PATTERN = Pattern.compile("^(?<major>0|[1-9]\\d*)\\.(?<minor>0|[1-9]\\d*)\\.(?<patch>0|[1-9]\\d*)(?<prereleaseAndMetadata>\\D.*)?$");
20+
21+
@Override
22+
public String toString() {
23+
return "Version{" +
24+
"major=" + major +
25+
", minor=" + minor +
26+
", patch=" + patch +
27+
", prereleaseAndMetadata='" + prereleaseAndMetadata + '\'' +
28+
'}';
29+
}
30+
31+
public Version(String semver) {
32+
var matcher = SEMVER_PATTERN.matcher(semver);
33+
if (!matcher.matches()) {
34+
throw new IllegalArgumentException("Invalid version format: " + semver);
35+
}
36+
this.major = Integer.parseInt(matcher.group("major"));
37+
this.minor = Optional.ofNullable(matcher.group("minor")).map(Integer::parseInt).orElse(0);
38+
this.patch = Optional.ofNullable(matcher.group("patch")).map(Integer::parseInt).orElse(0);
39+
this.prereleaseAndMetadata = matcher.group("prereleaseAndMetadata");
40+
}
41+
42+
public Version(int major, int minor, int patch, @Nullable String prereleaseAndMetadata) {
43+
this.major = major;
44+
this.minor = minor;
45+
this.patch = patch;
46+
this.prereleaseAndMetadata = prereleaseAndMetadata;
47+
}
48+
49+
@Override
50+
public int compareTo(@Nonnull Version o) {
51+
if (this.major != o.major) {
52+
return Integer.compare(this.major, o.major);
53+
}
54+
if (this.minor != o.minor) {
55+
return Integer.compare(this.minor, o.minor);
56+
}
57+
if (this.patch != o.patch) {
58+
return Integer.compare(this.patch, o.patch);
59+
}
60+
log.debug("ignoring prerelease and buildmetadata during comparision this = {} o = {}", this, o);
61+
return 0;
62+
}
63+
64+
@Override
65+
public boolean equals(Object o) {
66+
if (o == null || getClass() != o.getClass()) return false;
67+
Version version = (Version) o;
68+
return major == version.major && minor == version.minor && patch == version.patch;
69+
}
70+
71+
@Override
72+
public int hashCode() {
73+
return Objects.hash(major, minor, patch);
74+
}
75+
}

sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import org.junit.jupiter.api.Test;
44

5+
import static org.assertj.core.api.Assertions.assertThat;
6+
import static org.junit.Assert.assertFalse;
57
import static org.junit.jupiter.api.Assertions.assertEquals;
68
import static org.junit.jupiter.api.Assertions.assertTrue;
79
import static org.junit.jupiter.api.Assertions.fail;
@@ -18,6 +20,8 @@ void newTDFConfig_shouldCreateDefaultConfig() {
1820
assertEquals(Config.IntegrityAlgorithm.GMAC, config.segmentIntegrityAlgorithm);
1921
assertTrue(config.attributes.isEmpty());
2022
assertTrue(config.kasInfoList.isEmpty());
23+
assertTrue(config.renderVersionInfoInManifest);
24+
assertFalse(config.hexEncodeRootAndSegmentHashes);
2125
}
2226

2327
@Test
@@ -61,6 +65,18 @@ void withSegmentSize_shouldIgnoreSegmentSize() {
6165
}
6266
}
6367

68+
@Test
69+
void withCompatibilityModeShouldSetFieldsCorrectly() {
70+
Config.TDFConfig oldConfig = Config.newTDFConfig(Config.withTargetMode("1.0.1"));
71+
assertThat(oldConfig.renderVersionInfoInManifest).isFalse();
72+
assertThat(oldConfig.hexEncodeRootAndSegmentHashes).isTrue();
73+
74+
Config.TDFConfig newConfig = Config.newTDFConfig(Config.withTargetMode("100.0.1"));
75+
assertThat(newConfig.renderVersionInfoInManifest).isTrue();
76+
assertThat(newConfig.hexEncodeRootAndSegmentHashes).isFalse();
77+
}
78+
79+
6480
@Test
6581
void withMimeType_shouldSetMimeType() {
6682
final String mimeType = "application/pdf";

sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java

+52-1
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,34 @@
55
import io.opentdf.platform.sdk.TDF.Reader;
66
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
77
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
8+
import org.apache.commons.codec.DecoderException;
89
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
9-
import org.bouncycastle.jce.interfaces.ECPrivateKey;
1010
import org.junit.jupiter.api.BeforeAll;
1111
import org.junit.jupiter.api.Test;
1212

1313
import javax.annotation.Nonnull;
1414
import java.io.ByteArrayInputStream;
1515
import java.io.ByteArrayOutputStream;
16+
import java.io.IOException;
1617
import java.io.InputStream;
1718
import java.io.OutputStream;
1819
import java.nio.charset.StandardCharsets;
1920
import java.security.KeyPair;
21+
import java.security.NoSuchAlgorithmException;
2022
import java.security.SecureRandom;
23+
import java.text.ParseException;
2124
import java.util.ArrayList;
2225
import java.util.Base64;
2326
import java.util.List;
2427
import java.util.Random;
28+
import java.util.concurrent.ExecutionException;
2529
import java.util.concurrent.atomic.AtomicInteger;
2630
import java.util.function.Predicate;
2731
import java.util.stream.Collectors;
2832

2933
import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT;
3034
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
35+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
3136
import static org.junit.jupiter.api.Assertions.assertThrows;
3237

3338
public class TDFTest {
@@ -492,6 +497,48 @@ public void testCreateTDFWithMimeType() throws Exception {
492497
assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType);
493498
}
494499

500+
@Test
501+
public void legacyTDFRoundTrips() throws DecoderException, IOException, ExecutionException, JOSEException, InterruptedException, ParseException, NoSuchAlgorithmException {
502+
final String mimeType = "application/pdf";
503+
504+
Config.TDFConfig config = Config.newTDFConfig(
505+
Config.withAutoconfigure(false),
506+
Config.withKasInformation(getRSAKASInfos()),
507+
Config.withTargetMode("4.2.1"),
508+
Config.withMimeType(mimeType));
509+
510+
byte[] data = new byte[129];
511+
new Random().nextBytes(data);
512+
InputStream plainTextInputStream = new ByteArrayInputStream(data);
513+
ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream();
514+
515+
TDF tdf = new TDF();
516+
tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, null);
517+
518+
var dataOutputStream = new ByteArrayOutputStream();
519+
520+
var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas);
521+
var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation;
522+
assertThat(reader.getManifest().tdfVersion).isNull();
523+
var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature);
524+
for (var b: decodedSignature) {
525+
assertThat(isHexChar(b))
526+
.withFailMessage("non-hex byte in signature: " + b)
527+
.isTrue();
528+
}
529+
for (var s: integrityInformation.segments) {
530+
var decodedSegmentSignature = Base64.getDecoder().decode(s.hash);
531+
for (var b: decodedSegmentSignature) {
532+
assertThat(isHexChar(b))
533+
.withFailMessage("non-hex byte in segment signature: " + b)
534+
.isTrue();
535+
}
536+
}
537+
reader.readPayload(dataOutputStream);
538+
assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType);
539+
assertArrayEquals(data, dataOutputStream.toByteArray(), "extracted data does not match");
540+
}
541+
495542
@Nonnull
496543
private static Config.KASInfo[] getKASInfos(Predicate<Integer> filter) {
497544
var kasInfos = new ArrayList<Config.KASInfo>();
@@ -515,4 +562,8 @@ private static Config.KASInfo[] getRSAKASInfos() {
515562
private static Config.KASInfo[] getECKASInfos() {
516563
return getKASInfos(i -> i % 2 != 0);
517564
}
565+
566+
private static boolean isHexChar(byte b) {
567+
return (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9');
568+
}
518569
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.opentdf.platform.sdk;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
7+
class VersionTest {
8+
9+
@Test
10+
public void testParsingVersions() {
11+
assertThat(new Version("1.0.0")).isEqualTo(new Version(1, 0, 0, null));
12+
assertThat(new Version("1.2.1-alpha")).isEqualTo(new Version(1, 2, 1, "alpha a build"));
13+
// ignore anything but the version
14+
assertThat(new Version("1.2.1-alpha+build.123")).isEqualTo(new Version(1, 2, 1, "beta build.1234"));
15+
}
16+
17+
@Test
18+
public void testComparingVersions() {
19+
assertThat(new Version("1.0.0")).isLessThan(new Version("1.0.1"));
20+
assertThat(new Version("1.0.1")).isGreaterThan(new Version("1.0.0"));
21+
22+
assertThat(new Version("500.0.1")).isLessThan(new Version("500.1.1"));
23+
assertThat(new Version("500.1.1")).isGreaterThan(new Version("500.0.1"));
24+
25+
// ignore anything but the version
26+
assertThat(new Version("1.0.1-alpha+thisbuild")).isEqualByComparingTo(new Version("1.0.1-beta+thatbuild"));
27+
}
28+
}

0 commit comments

Comments
 (0)