Skip to content
Merged
8 changes: 4 additions & 4 deletions cmdline/src/main/java/io/opentdf/platform/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ void encrypt(
@Option(names = {
"--encap-key-type" }, defaultValue = Option.NULL_VALUE, description = "Preferred key access key wrap algorithm, one of ${COMPLETION-CANDIDATES}") Optional<KeyType> encapKeyType,
@Option(names = { "--mime-type" }, defaultValue = Option.NULL_VALUE) Optional<String> mimeType,
@Option(names = { "--with-assertions" }, defaultValue = Option.NULL_VALUE) Optional<String> assertion)
@Option(names = { "--with-assertions" }, defaultValue = Option.NULL_VALUE) Optional<String> assertion,
@Option(names = { "--with-target-mode" }, defaultValue = Option.NULL_VALUE) Optional<String> targetMode)

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

Expand Down Expand Up @@ -214,9 +215,8 @@ void encrypt(
configs.add(Config.withAssertionConfig(assertionConfigs));
}

if (attributes.isPresent()) {
configs.add(Config.withDataAttributes(attributes.get().split(",")));
}
attributes.ifPresent(s -> configs.add(Config.withDataAttributes(s.split(","))));
targetMode.map(Config::withTargetMode).ifPresent(configs::add);
var tdfConfig = Config.newTDFConfig(configs.toArray(Consumer[]::new));
try (var in = file.isEmpty() ? new BufferedInputStream(System.in) : new FileInputStream(file.get())) {
try (var out = new BufferedOutputStream(System.out)) {
Expand Down
18 changes: 17 additions & 1 deletion sdk/src/main/java/io/opentdf/platform/sdk/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ public static class TDFConfig {
public String mimeType;
public List<Autoconfigure.KeySplitStep> splitPlan;
public KeyType wrappingKeyType;
public boolean hexEncodeRootAndSegmentHashes;
public boolean renderVersionInfoInManifest;

public TDFConfig() {
this.autoconfigure = true;
Expand All @@ -154,6 +156,8 @@ public TDFConfig() {
this.mimeType = DEFAULT_MIME_TYPE;
this.splitPlan = new ArrayList<>();
this.wrappingKeyType = KeyType.RSA2048Key;
this.hexEncodeRootAndSegmentHashes = false;
this.renderVersionInfoInManifest = true;
}
}

Expand Down Expand Up @@ -251,6 +255,18 @@ public static Consumer<TDFConfig> withAutoconfigure(boolean enable) {
};
}

// specify TDF version for TDF creation to target. Versions less than 4.3.0 will add a
// layer of hex encoding to their segment hashes and will not include version information
// in their manifests.
public static Consumer<TDFConfig> withTargetMode(String targetVersion) {
Version version = new Version(targetVersion == null ? "0.0.0" : targetVersion);
return (TDFConfig config) -> {
var legacyTDF = version.compareTo(new Version("4.3.0")) < 0;
config.renderVersionInfoInManifest = !legacyTDF;
config.hexEncodeRootAndSegmentHashes = legacyTDF;
};
}

public static Consumer<TDFConfig> WithWrappingKeyAlg(KeyType keyType) {
return (TDFConfig config) -> config.wrappingKeyType = keyType;
}
Expand Down Expand Up @@ -393,4 +409,4 @@ public synchronized void updateHeaderInfo(HeaderInfo headerInfo) {
this.notifyAll();
}
}
}
}
14 changes: 9 additions & 5 deletions sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -222,7 +221,7 @@ PolicyObject createPolicyObject(List<Autoconfigure.AttributeValueFQN> attributes
private static final Base64.Encoder encoder = Base64.getEncoder();

private void prepareManifest(Config.TDFConfig tdfConfig, SDK.KAS kas) {
manifest.tdfVersion = TDF_VERSION;
manifest.tdfVersion = tdfConfig.renderVersionInfoInManifest ? TDF_VERSION : null;
manifest.encryptionInformation.keyAccessType = kSplitKeyType;
manifest.encryptionInformation.keyAccessObj = new ArrayList<>();

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

segmentSig = calculateSignature(cipherData, tdfObject.payloadKey, tdfConfig.segmentIntegrityAlgorithm);
if (tdfConfig.hexEncodeRootAndSegmentHashes) {
segmentSig = Hex.encodeHexString(segmentSig).getBytes(StandardCharsets.UTF_8);
}
segmentInfo.hash = Base64.getEncoder().encodeToString(segmentSig);

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

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

byte[] rootSig = calculateSignature(aggregateHash.toByteArray(),
tdfObject.payloadKey, tdfConfig.integrityAlgorithm);
rootSignature.signature = Base64.getEncoder().encodeToString(rootSig);
byte[] rootSig = calculateSignature(aggregateHash.toByteArray(), tdfObject.payloadKey, tdfConfig.integrityAlgorithm);
byte[] encodedRootSig = tdfConfig.hexEncodeRootAndSegmentHashes
? Hex.encodeHexString(rootSig).getBytes(StandardCharsets.UTF_8)
: rootSig;
rootSignature.signature = Base64.getEncoder().encodeToString(encodedRootSig);

String alg = kGmacIntegrityAlgorithm;
if (tdfConfig.integrityAlgorithm == Config.IntegrityAlgorithm.HS256) {
Expand Down
75 changes: 75 additions & 0 deletions sdk/src/main/java/io/opentdf/platform/sdk/Version.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.opentdf.platform.sdk;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;

class Version implements Comparable<Version> {
private final int major;
private final int minor;
private final int patch;
private final String prereleaseAndMetadata;
private static final Logger log = LoggerFactory.getLogger(Version.class);

Pattern SEMVER_PATTERN = Pattern.compile("^(?<major>0|[1-9]\\d*)\\.(?<minor>0|[1-9]\\d*)\\.(?<patch>0|[1-9]\\d*)(?<prereleaseAndMetadata>\\D.*)?$");

@Override
public String toString() {
return "Version{" +
"major=" + major +
", minor=" + minor +
", patch=" + patch +
", prereleaseAndMetadata='" + prereleaseAndMetadata + '\'' +
'}';
}

public Version(String semver) {
var matcher = SEMVER_PATTERN.matcher(semver);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid version format: " + semver);
}
this.major = Integer.parseInt(matcher.group("major"));
this.minor = Optional.ofNullable(matcher.group("minor")).map(Integer::parseInt).orElse(0);
this.patch = Optional.ofNullable(matcher.group("patch")).map(Integer::parseInt).orElse(0);
this.prereleaseAndMetadata = matcher.group("prereleaseAndMetadata");
}

public Version(int major, int minor, int patch, @Nullable String prereleaseAndMetadata) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.prereleaseAndMetadata = prereleaseAndMetadata;
}

@Override
public int compareTo(@Nonnull Version o) {
if (this.major != o.major) {
return Integer.compare(this.major, o.major);
}
if (this.minor != o.minor) {
return Integer.compare(this.minor, o.minor);
}
if (this.patch != o.patch) {
return Integer.compare(this.patch, o.patch);
}
log.debug("ignoring prerelease and buildmetadata during comparision this = {} o = {}", this, o);
return 0;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
Version version = (Version) o;
return major == version.major && minor == version.minor && patch == version.patch;
}

@Override
public int hashCode() {
return Objects.hash(major, minor, patch);
}
}
16 changes: 16 additions & 0 deletions sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
Expand All @@ -18,6 +20,8 @@ void newTDFConfig_shouldCreateDefaultConfig() {
assertEquals(Config.IntegrityAlgorithm.GMAC, config.segmentIntegrityAlgorithm);
assertTrue(config.attributes.isEmpty());
assertTrue(config.kasInfoList.isEmpty());
assertTrue(config.renderVersionInfoInManifest);
assertFalse(config.hexEncodeRootAndSegmentHashes);
}

@Test
Expand Down Expand Up @@ -61,6 +65,18 @@ void withSegmentSize_shouldIgnoreSegmentSize() {
}
}

@Test
void withCompatibilityModeShouldSetFieldsCorrectly() {
Config.TDFConfig oldConfig = Config.newTDFConfig(Config.withTargetMode("1.0.1"));
assertThat(oldConfig.renderVersionInfoInManifest).isFalse();
assertThat(oldConfig.hexEncodeRootAndSegmentHashes).isTrue();

Config.TDFConfig newConfig = Config.newTDFConfig(Config.withTargetMode("100.0.1"));
assertThat(newConfig.renderVersionInfoInManifest).isTrue();
assertThat(newConfig.hexEncodeRootAndSegmentHashes).isFalse();
}


@Test
void withMimeType_shouldSetMimeType() {
final String mimeType = "application/pdf";
Expand Down
53 changes: 52 additions & 1 deletion sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,34 @@
import io.opentdf.platform.sdk.TDF.Reader;
import io.opentdf.platform.sdk.nanotdf.ECKeyPair;
import io.opentdf.platform.sdk.nanotdf.NanoTDFType;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.compress.utils.SeekableInMemoryByteChannel;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import javax.annotation.Nonnull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static io.opentdf.platform.sdk.TDF.GLOBAL_KEY_SALT;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class TDFTest {
Expand Down Expand Up @@ -492,6 +497,48 @@ public void testCreateTDFWithMimeType() throws Exception {
assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType);
}

@Test
public void legacyTDFRoundTrips() throws DecoderException, IOException, ExecutionException, JOSEException, InterruptedException, ParseException, NoSuchAlgorithmException {
final String mimeType = "application/pdf";

Config.TDFConfig config = Config.newTDFConfig(
Config.withAutoconfigure(false),
Config.withKasInformation(getRSAKASInfos()),
Config.withTargetMode("4.2.1"),
Config.withMimeType(mimeType));

byte[] data = new byte[129];
new Random().nextBytes(data);
InputStream plainTextInputStream = new ByteArrayInputStream(data);
ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream();

TDF tdf = new TDF();
tdf.createTDF(plainTextInputStream, tdfOutputStream, config, kas, null);

var dataOutputStream = new ByteArrayOutputStream();

var reader = tdf.loadTDF(new SeekableInMemoryByteChannel(tdfOutputStream.toByteArray()), kas);
var integrityInformation = reader.getManifest().encryptionInformation.integrityInformation;
assertThat(reader.getManifest().tdfVersion).isNull();
var decodedSignature = Base64.getDecoder().decode(integrityInformation.rootSignature.signature);
for (var b: decodedSignature) {
assertThat(isHexChar(b))
.withFailMessage("non-hex byte in signature: " + b)
.isTrue();
}
for (var s: integrityInformation.segments) {
var decodedSegmentSignature = Base64.getDecoder().decode(s.hash);
for (var b: decodedSegmentSignature) {
assertThat(isHexChar(b))
.withFailMessage("non-hex byte in segment signature: " + b)
.isTrue();
}
}
reader.readPayload(dataOutputStream);
assertThat(reader.getManifest().payload.mimeType).isEqualTo(mimeType);
assertArrayEquals(data, dataOutputStream.toByteArray(), "extracted data does not match");
}

@Nonnull
private static Config.KASInfo[] getKASInfos(Predicate<Integer> filter) {
var kasInfos = new ArrayList<Config.KASInfo>();
Expand All @@ -515,4 +562,8 @@ private static Config.KASInfo[] getRSAKASInfos() {
private static Config.KASInfo[] getECKASInfos() {
return getKASInfos(i -> i % 2 != 0);
}

private static boolean isHexChar(byte b) {
return (b >= 'a' && b <= 'f') || (b >= '0' && b <= '9');
}
}
28 changes: 28 additions & 0 deletions sdk/src/test/java/io/opentdf/platform/sdk/VersionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.opentdf.platform.sdk;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class VersionTest {

@Test
public void testParsingVersions() {
assertThat(new Version("1.0.0")).isEqualTo(new Version(1, 0, 0, null));
assertThat(new Version("1.2.1-alpha")).isEqualTo(new Version(1, 2, 1, "alpha a build"));
// ignore anything but the version
assertThat(new Version("1.2.1-alpha+build.123")).isEqualTo(new Version(1, 2, 1, "beta build.1234"));
}

@Test
public void testComparingVersions() {
assertThat(new Version("1.0.0")).isLessThan(new Version("1.0.1"));
assertThat(new Version("1.0.1")).isGreaterThan(new Version("1.0.0"));

assertThat(new Version("500.0.1")).isLessThan(new Version("500.1.1"));
assertThat(new Version("500.1.1")).isGreaterThan(new Version("500.0.1"));

// ignore anything but the version
assertThat(new Version("1.0.1-alpha+thisbuild")).isEqualByComparingTo(new Version("1.0.1-beta+thatbuild"));
}
}
Loading