From d67daa262a6c3c8a40c1bbab9b86b31460bf6474 Mon Sep 17 00:00:00 2001 From: Mikhail Ustiantsev <39340048+mustyantsev@users.noreply.github.com> Date: Tue, 28 May 2024 20:32:22 +0300 Subject: [PATCH] feat: SDK Encrypt (with mocked rewrap) (#45) Co-authored-by: sujan kota Co-authored-by: Morgan Kleene --- sdk/pom.xml | 22 + .../java/io/opentdf/platform/sdk/AesGcm.java | 17 +- .../java/io/opentdf/platform/sdk/Config.java | 82 ++++ .../io/opentdf/platform/sdk/CryptoUtils.java | 18 + .../io/opentdf/platform/sdk/Manifest.java | 65 +++ .../io/opentdf/platform/sdk/PolicyObject.java | 22 + .../java/io/opentdf/platform/sdk/TDF.java | 444 ++++++++++++++++++ .../io/opentdf/platform/sdk/TDFReader.java | 25 + .../io/opentdf/platform/sdk/TDFWriter.java | 28 ++ .../io/opentdf/platform/sdk/ZipWriter.java | 13 +- .../io/opentdf/platform/sdk/ConfigTest.java | 51 ++ .../io/opentdf/platform/sdk/ManifestTest.java | 94 ++++ .../java/io/opentdf/platform/sdk/TDFTest.java | 54 +++ .../opentdf/platform/sdk/TDFWriterTest.java | 67 +++ 14 files changed, 997 insertions(+), 5 deletions(-) create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/Config.java create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/TDF.java create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java create mode 100644 sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java create mode 100644 sdk/src/test/java/io/opentdf/platform/sdk/TDFWriterTest.java diff --git a/sdk/pom.xml b/sdk/pom.xml index d69b0294..a2e5edd5 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -4,6 +4,18 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 sdk + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + sdk sdk-pom @@ -83,6 +95,16 @@ 4.13.1 test + + com.google.code.gson + gson + 2.10.1 + + + commons-codec + commons-codec + 1.17.0 + org.apache.commons commons-compress diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java index 57a44454..8fe916e9 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java @@ -11,7 +11,7 @@ import java.util.Base64; public class AesGcm { - private static final int GCM_NONCE_LENGTH = 12; // in bytes + public static final int GCM_NONCE_LENGTH = 12; // in bytes private static final int GCM_TAG_LENGTH = 16; // in bytes private static final String CIPHER_TRANSFORM = "AES/GCM/NoPadding"; @@ -37,13 +37,26 @@ public AesGcm(byte[] key) { */ public byte[] encrypt(byte[] plaintext) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + return encrypt(plaintext, 0, plaintext.length); + } + + /** + *

encrypt.

+ * + * @param plaintext the plaintext byte array to encrypt + * @param offset where the input start + * @param len input length + * @return the encrypted text + */ + public byte[] encrypt(byte[] plaintext, int offset, int len) throws NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { Cipher cipher = Cipher.getInstance(CIPHER_TRANSFORM); byte[] nonce = new byte[GCM_NONCE_LENGTH]; SecureRandom.getInstanceStrong().nextBytes(nonce); GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce); cipher.init(Cipher.ENCRYPT_MODE, key, spec); - byte[] cipherText = cipher.doFinal(plaintext); + byte[] cipherText = cipher.doFinal(plaintext, offset, len); byte[] cipherTextWithNonce = new byte[nonce.length + cipherText.length]; System.arraycopy(nonce, 0, cipherTextWithNonce, 0, nonce.length); System.arraycopy(cipherText, 0, cipherTextWithNonce, nonce.length, cipherText.length); diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Config.java b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java new file mode 100644 index 00000000..844ef589 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Config.java @@ -0,0 +1,82 @@ +package io.opentdf.platform.sdk; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +public class Config { + + public static final int TDF3_KEY_SIZE = 2048; + public static final int DEFAULT_SEGMENT_SIZE = 2 * 1024 * 1024; // 2mb + public static final String KAS_PUBLIC_KEY_PATH = "/kas_public_key"; + + public enum TDFFormat { + JSONFormat, + XMLFormat + } + + public enum IntegrityAlgorithm { + HS256, + GMAC + } + + public static final int K_HTTP_OK = 200; + + public static class KASInfo { + public String URL; + public String PublicKey; + } + + public static class TDFConfig { + public int defaultSegmentSize; + public boolean enableEncryption; + public TDFFormat tdfFormat; + public String tdfPublicKey; + public String tdfPrivateKey; + public String metaData; + public IntegrityAlgorithm integrityAlgorithm; + public IntegrityAlgorithm segmentIntegrityAlgorithm; + public List attributes; + public List kasInfoList; + + public TDFConfig() { + this.defaultSegmentSize = DEFAULT_SEGMENT_SIZE; + this.enableEncryption = true; + this.tdfFormat = TDFFormat.JSONFormat; + this.integrityAlgorithm = IntegrityAlgorithm.HS256; + this.segmentIntegrityAlgorithm = IntegrityAlgorithm.GMAC; + this.attributes = new ArrayList<>(); + this.kasInfoList = new ArrayList<>(); + } + } + + @SafeVarargs + public static TDFConfig newTDFConfig(Consumer... options) { + TDFConfig config = new TDFConfig(); + for (Consumer option : options) { + option.accept(config); + } + return config; + } + + public static Consumer withDataAttributes(String... attributes) { + return (TDFConfig config) -> { + Collections.addAll(config.attributes, attributes); + }; + } + + public static Consumer withKasInformation(KASInfo... kasInfoList) { + return (TDFConfig config) -> { + Collections.addAll(config.kasInfoList, kasInfoList); + }; + } + + public static Consumer withMetaData(String metaData) { + return (TDFConfig config) -> config.metaData = metaData; + } + + public static Consumer withSegmentSize(int size) { + return (TDFConfig config) -> config.defaultSegmentSize = size; + } +} \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java b/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java new file mode 100644 index 00000000..259c0a49 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java @@ -0,0 +1,18 @@ +package io.opentdf.platform.sdk; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.io.UnsupportedEncodingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +public class CryptoUtils { + public static byte[] CalculateSHA256Hmac(byte[] key, byte[] data) throws NoSuchAlgorithmException, + InvalidKeyException { + Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + SecretKeySpec secret_key = new SecretKeySpec(key, "HmacSHA256"); + sha256_HMAC.init(secret_key); + + return sha256_HMAC.doFinal(data); + } +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java new file mode 100644 index 00000000..b7692ec9 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java @@ -0,0 +1,65 @@ +package io.opentdf.platform.sdk; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class Manifest { + static public class Segment { + public String hash; + public long segmentSize; + public long encryptedSegmentSize; + } + + static public class RootSignature { + @SerializedName(value = "alg") + public String algorithm; + @SerializedName(value = "sig") + public String signature; + } + + static public class IntegrityInformation { + public RootSignature rootSignature; + public String segmentHashAlg; + public int segmentSizeDefault; + public int encryptedSegmentSizeDefault; + public List segments; + } + + static public class KeyAccess { + @SerializedName(value = "type") + public String keyType; + public String url; + public String protocol; + public String wrappedKey; + public String policyBinding; + public String encryptedMetadata; + } + + static public class Method { + public String algorithm; + public String iv; + public Boolean IsStreamable; + } + + static public class EncryptionInformation { + @SerializedName(value = "type") + public String keyAccessType; + public String policy; + + @SerializedName(value = "keyAccess") + public List keyAccessObj; + public Method method; + public IntegrityInformation integrityInformation; + } + + static public class Payload { + public String type; + public String url; + public String protocol; + public String mimeType; + public Boolean isEncrypted; + } + public EncryptionInformation encryptionInformation; + public Payload payload; +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java b/sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java new file mode 100644 index 00000000..8b1bb33f --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java @@ -0,0 +1,22 @@ +package io.opentdf.platform.sdk; + +import java.util.List; +import java.util.UUID; + +public class PolicyObject { + static public class AttributeObject { + public String attribute; + public String displayName; + public boolean isDefault; + public String pubKey; + public String kasURL; + } + + static public class Body { + public List dataAttributes; + public List dissem; + } + + public String uuid; + public Body body; +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java new file mode 100644 index 00000000..97c6546a --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDF.java @@ -0,0 +1,444 @@ +package io.opentdf.platform.sdk; + + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.codec.binary.Hex; + + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.UUID; +import java.util.List; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Random; + +public class TDF { + + private static final long MAX_TDF_INPUT_SIZE = 68719476736L; + private static final int GCM_KEY_SIZE = 32; + private static final String kSplitKeyType = "split"; + private static final String kWrapped = "wrapped"; + private static final String kKasProtocol = "kas"; + private static final int kGcmIvSize = 12; + private static final int kAesBlockSize = 16; + private static final String kGCMCipherAlgorithm = "AES-256-GCM"; + private static final int kGMACPayloadLength = 16; + private static final String kGmacIntegrityAlgorithm = "GMAC"; + + private static final String kHmacIntegrityAlgorithm = "HS256"; + private static final String kDefaultMimeType = "application/octet-stream"; + private static final String kTDFAsZip = "zip"; + private static final String kTDFZipReference = "reference"; + + public static class DataSizeNotSupported extends Exception { + public DataSizeNotSupported(String errorMessage) { + super(errorMessage); + } + } + + public static class KasInfoMissing extends Exception { + public KasInfoMissing(String errorMessage) { + super(errorMessage); + } + } + + public static class KasPublicKeyMissing extends Exception { + public KasPublicKeyMissing(String errorMessage) { + super(errorMessage); + } + } + + public static class InputStreamReadFailed extends Exception { + public InputStreamReadFailed(String errorMessage) { + super(errorMessage); + } + } + + public static class FailedToCreateGMAC extends Exception { + public FailedToCreateGMAC(String errorMessage) { + super(errorMessage); + } + } + + public static class NotValidateRootSignature extends Exception { + public NotValidateRootSignature(String errorMessage) { + super(errorMessage); + } + } + + public static class SegmentSizeMismatch extends Exception { + public SegmentSizeMismatch(String errorMessage) { + super(errorMessage); + } + } + + public static class SegmentSignatureMismatch extends Exception { + public SegmentSignatureMismatch(String errorMessage) { + super(errorMessage); + } + } + + public static class TDFReadFailed extends Exception { + public TDFReadFailed(String errorMessage) { + super(errorMessage); + } + } + + + public static class EncryptedMetadata { + private String ciphertext; + private String iv; + } + + public static class TDFObject { + private Manifest manifest; + private long size; + private AesGcm aesGcm; + private final byte[] payloadKey = new byte[GCM_KEY_SIZE]; + + public TDFObject() { + this.manifest = new Manifest(); + this.manifest.encryptionInformation = new Manifest.EncryptionInformation(); + this.manifest.encryptionInformation.integrityInformation = new Manifest.IntegrityInformation(); + this.manifest.encryptionInformation.method = new Manifest.Method(); + this.size = 0; + } + + PolicyObject createPolicyObject(List attributes) { + UUID uuid = UUID.randomUUID(); + + PolicyObject policyObject = new PolicyObject(); + policyObject.body = new PolicyObject.Body(); + policyObject.uuid = UUID.randomUUID().toString(); + policyObject.body.dataAttributes = new ArrayList<>(); + policyObject.body.dissem = new ArrayList<>(); + + for (String attribute: attributes) { + PolicyObject.AttributeObject attributeObject = new PolicyObject.AttributeObject(); + attributeObject.attribute = attribute; + policyObject.body.dataAttributes.add(attributeObject); + } + return policyObject; + } + + private void prepareManifest(Config.TDFConfig tdfConfig) throws Exception { + Gson gson = new GsonBuilder().create(); + + manifest.encryptionInformation.keyAccessType = kSplitKeyType; + manifest.encryptionInformation.keyAccessObj = new ArrayList<>(); + + PolicyObject policyObject = createPolicyObject(tdfConfig.attributes); + String base64PolicyObject = Base64.getEncoder().encodeToString(gson.toJson(policyObject).getBytes(StandardCharsets.UTF_8)); + List symKeys = new ArrayList<>(); + + for (Config.KASInfo kasInfo: tdfConfig.kasInfoList) { + if (kasInfo.PublicKey.isEmpty()) { + throw new KasPublicKeyMissing("Kas public key is missing in kas information list"); + } + + // Symmetric key + Random rd = new Random(); + byte[] symKey = new byte[GCM_KEY_SIZE]; + //rd.nextBytes(symKey); TODO: Remove this - disabled for testing. + + Manifest.KeyAccess keyAccess = new Manifest.KeyAccess(); + keyAccess.keyType = kWrapped; + keyAccess.url = kasInfo.URL; + keyAccess.protocol = kKasProtocol; + + // Add policyBinding + keyAccess.policyBinding = Hex.encodeHexString(CryptoUtils.CalculateSHA256Hmac(symKey, + base64PolicyObject.getBytes(StandardCharsets.UTF_8))); + + // Wrap the key with kas public key + AsymEncryption asymmetricEncrypt = new AsymEncryption(kasInfo.PublicKey); + byte[] wrappedKey = asymmetricEncrypt.encrypt(symKey); + + keyAccess.wrappedKey = Base64.getEncoder().encodeToString(wrappedKey); + + // Add meta data + if(tdfConfig.metaData != null && !tdfConfig.metaData.trim().isEmpty()) { + AesGcm aesGcm = new AesGcm(symKey); + byte[] ciphertext = aesGcm.encrypt(tdfConfig.metaData.getBytes(StandardCharsets.UTF_8)); + + + byte[] iv = new byte[AesGcm.GCM_NONCE_LENGTH]; + System.arraycopy(ciphertext, 0, iv, 0, iv.length); + + EncryptedMetadata encryptedMetadata = new EncryptedMetadata(); + encryptedMetadata.ciphertext = new String(ciphertext); + encryptedMetadata.iv = new String(iv); + + keyAccess.encryptedMetadata = gson.toJson(encryptedMetadata); + } + + symKeys.add(symKey); + manifest.encryptionInformation.keyAccessObj.add(keyAccess); + } + + manifest.encryptionInformation.policy = base64PolicyObject; + manifest.encryptionInformation.method.algorithm = kGCMCipherAlgorithm; + + // Create the payload key by XOR all the keys in key access object. + for (byte[] symKey: symKeys) { + for (int index = 0; index < symKey.length; index++) { + this.payloadKey[index] ^= symKey[index]; + } + } + + this.aesGcm = new AesGcm(this.payloadKey); + } + } + + private static class Reader { + private TDFReader tdfReader; + private Manifest manifest; + private String unencryptedMetadata; + private final byte[] payloadKey = new byte[GCM_KEY_SIZE]; + private long payloadSize; + private AesGcm aesGcm; + + private void doPayloadKeyUnwrap() throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, FailedToCreateGMAC, NotValidateRootSignature, SegmentSizeMismatch { + for (Manifest.KeyAccess keyAccess: this.manifest.encryptionInformation.keyAccessObj) { + // Create KAS client + + // Perform rewrap + byte[] wrappedKey = new byte[GCM_KEY_SIZE]; // Replace with kas client rewrap call + + for (int index = 0; index < wrappedKey.length; index++) { + this.payloadKey[index] ^= wrappedKey[index]; + } + + if (!keyAccess.encryptedMetadata.isEmpty()) { + AesGcm aesGcm = new AesGcm(wrappedKey); + + String decodedMetadata = new String(Base64.getDecoder().decode(keyAccess.encryptedMetadata), "UTF-8"); + + Gson gson = new GsonBuilder().create(); + EncryptedMetadata encryptedMetadata = gson.fromJson(decodedMetadata, EncryptedMetadata.class); + + String encodedCipherText = encryptedMetadata.ciphertext; + byte[] cipherText = Base64.getDecoder().decode(encodedCipherText); + this.unencryptedMetadata = new String(aesGcm.decrypt(cipherText), "UTF-8"); + } + } + + // Validate root signature + String rootAlgorithm = this.manifest.encryptionInformation.integrityInformation.rootSignature.algorithm; + String rootSignature = this.manifest.encryptionInformation.integrityInformation.rootSignature.signature; + + ByteArrayOutputStream aggregateHash = new ByteArrayOutputStream(); + for (Manifest.Segment segment: this.manifest.encryptionInformation.integrityInformation.segments) { + byte[] decodedHash = Base64.getDecoder().decode(segment.hash); + aggregateHash.write(decodedHash); + } + + Config.IntegrityAlgorithm sigAlg = Config.IntegrityAlgorithm.HS256; + if (rootAlgorithm.compareToIgnoreCase(kGmacIntegrityAlgorithm) == 0) { + sigAlg = Config.IntegrityAlgorithm.GMAC; + } + + String sig = TDF.calculateSignature(aggregateHash.toByteArray(), this.payloadKey, sigAlg); + String rootSigValue = Base64.getEncoder().encodeToString(sig.getBytes(StandardCharsets.UTF_8)); + if (rootSignature.compareTo(rootSigValue) != 0) { + throw new NotValidateRootSignature("root signature validation failed"); + } + + int segmentSize = this.manifest.encryptionInformation.integrityInformation.segmentSizeDefault; + int encryptedSegSize = this.manifest.encryptionInformation.integrityInformation.encryptedSegmentSizeDefault; + + if (segmentSize != encryptedSegSize - (kGcmIvSize + kAesBlockSize)) { + throw new SegmentSizeMismatch("mismatch encrypted segment size in manifest"); + } + + long payloadSize = 0; + for (Manifest.Segment segment: this.manifest.encryptionInformation.integrityInformation.segments) { + payloadSize += segment.segmentSize; + } + + this.payloadSize = payloadSize; + this.aesGcm = new AesGcm(this.payloadKey); + } + } + + + private static String calculateSignature(byte[] data, byte[] secret, Config.IntegrityAlgorithm algorithm) + throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException, FailedToCreateGMAC { + if (algorithm == Config.IntegrityAlgorithm.HS256) { + byte[] hmac = CryptoUtils.CalculateSHA256Hmac(secret, data); + return Hex.encodeHexString(hmac); + } + + if (kGMACPayloadLength > data.length) { + throw new FailedToCreateGMAC("Dail to create gmac signature"); + } + + byte[] gmacPayload = Arrays.copyOfRange(data, data.length - kGMACPayloadLength, data.length); + return Hex.encodeHexString(gmacPayload); + } + + public TDFObject createTDF(InputStream inputStream, + long inputSize, + OutputStream outputStream, + Config.TDFConfig tdfConfig) throws Exception { + if (inputSize > MAX_TDF_INPUT_SIZE) { + throw new DataSizeNotSupported("can't create tdf larger than 64gb"); + } + + if (tdfConfig.kasInfoList.isEmpty()) { + throw new KasInfoMissing("kas information is missing"); + } + + + // Fetch the kas public keys + // FetchKasPubKeys(); + + TDFObject tdfObject = new TDFObject(); + tdfObject.prepareManifest(tdfConfig); + + int segmentSize = tdfConfig.defaultSegmentSize; + long totalSegments = inputSize / segmentSize; + if (inputSize % segmentSize != 0) { + totalSegments += 1; + } + + // Empty payload we still want to create a payload + if (totalSegments == 0) { + totalSegments = 1; + } + + long encryptedSegmentSize = segmentSize + kGcmIvSize + kAesBlockSize; + long payloadSize = inputSize + (totalSegments * (kGcmIvSize + kAesBlockSize)); + TDFWriter tdfWriter = new TDFWriter(outputStream); + + long readPos = 0; + StringBuilder aggregateHash = new StringBuilder(); + byte[] readBuf = new byte[tdfConfig.defaultSegmentSize]; + + tdfObject.manifest.encryptionInformation.integrityInformation.segments = new ArrayList<>(); + while (totalSegments != 0) { + long readSize = segmentSize; + if ((inputSize - readPos) < segmentSize) { + readSize = inputSize - readPos; + } + + long n = inputStream.read(readBuf, 0, (int) readSize); + if (n != readSize) { + throw new InputStreamReadFailed("Input stream read miss match"); + } + + byte[] cipherData = tdfObject.aesGcm.encrypt(readBuf, 0, (int) readSize); + tdfWriter.appendPayload(cipherData); + + String segmentSig = calculateSignature(cipherData, tdfObject.payloadKey, tdfConfig.segmentIntegrityAlgorithm); + + aggregateHash.append(segmentSig); + Manifest.Segment segmentInfo = new Manifest.Segment(); + segmentInfo.hash = Base64.getEncoder().encodeToString(segmentSig.getBytes(StandardCharsets.UTF_8)); + segmentInfo.segmentSize = readSize; + segmentInfo.encryptedSegmentSize = cipherData.length; + + tdfObject.manifest.encryptionInformation.integrityInformation.segments.add(segmentInfo); + + totalSegments -= 1; + readPos += readSize; + } + + Manifest.RootSignature rootSignature = new Manifest.RootSignature(); + String rootSig = calculateSignature(aggregateHash.toString().getBytes(), + tdfObject.payloadKey, tdfConfig.integrityAlgorithm); + rootSignature.signature = Base64.getEncoder().encodeToString(rootSig.getBytes(StandardCharsets.UTF_8)); + + String alg = kGmacIntegrityAlgorithm; + if (tdfConfig.integrityAlgorithm == Config.IntegrityAlgorithm.HS256) { + alg = kHmacIntegrityAlgorithm; + } + rootSignature.algorithm = alg; + tdfObject.manifest.encryptionInformation.integrityInformation.rootSignature = rootSignature; + + tdfObject.manifest.encryptionInformation.integrityInformation.segmentSizeDefault = segmentSize; + tdfObject.manifest.encryptionInformation.integrityInformation.encryptedSegmentSizeDefault = (int)encryptedSegmentSize; + + tdfObject.manifest.encryptionInformation.integrityInformation.segmentHashAlg = kGmacIntegrityAlgorithm; + if (tdfConfig.segmentIntegrityAlgorithm == Config.IntegrityAlgorithm.HS256) { + tdfObject.manifest.encryptionInformation.integrityInformation.segmentHashAlg = kHmacIntegrityAlgorithm; + } + + tdfObject.manifest.encryptionInformation.method.IsStreamable = true; + + // Add payload info + tdfObject.manifest.payload = new Manifest.Payload(); + tdfObject.manifest.payload.mimeType = kDefaultMimeType; + tdfObject.manifest.payload.protocol = kTDFAsZip; + tdfObject.manifest.payload.type = kTDFZipReference; + tdfObject.manifest.payload.url = TDFWriter.TDF_PAYLOAD_FILE_NAME; + tdfObject.manifest.payload.isEncrypted = true; + + Gson gson = new GsonBuilder().create(); + String manifestAsStr = gson.toJson(tdfObject.manifest); + + tdfWriter.appendManifest(manifestAsStr); + tdfObject.size = tdfWriter.finish(); + + return tdfObject; + } + + public void loadTDF(InputStream inputStream, OutputStream outputStream) throws InvalidAlgorithmParameterException, + NotValidateRootSignature, SegmentSizeMismatch, NoSuchPaddingException, + IllegalBlockSizeException, IOException, NoSuchAlgorithmException, + BadPaddingException, InvalidKeyException, FailedToCreateGMAC, TDFReadFailed, SegmentSignatureMismatch { + Reader reader = new Reader(); + reader.tdfReader = new TDFReader(inputStream); + String manifest = reader.tdfReader.manifest(); + + + Gson gson = new GsonBuilder().create(); + reader.manifest = gson.fromJson(manifest, Manifest.class); + + reader.doPayloadKeyUnwrap(); + + long totalBytes = 0; + long payloadOffset = 0; + + for (Manifest.Segment segment: reader.manifest.encryptionInformation.integrityInformation.segments) { + byte[] readBuf = reader.tdfReader.readPayload(payloadOffset, segment.encryptedSegmentSize); + + if (readBuf.length != segment.encryptedSegmentSize) { + throw new TDFReadFailed("faile to read payload"); + } + + String segHashAlg = reader.manifest.encryptionInformation.integrityInformation.segmentHashAlg; + Config.IntegrityAlgorithm sigAlg = Config.IntegrityAlgorithm.HS256; + if (segHashAlg.compareToIgnoreCase(kGmacIntegrityAlgorithm) == 0) { + sigAlg = Config.IntegrityAlgorithm.GMAC; + } + + String payloadSig = calculateSignature(readBuf, reader.payloadKey, sigAlg); + byte[] payloadSigAsBytes = payloadSig.getBytes(StandardCharsets.UTF_8); + if (segment.hash.compareTo(Base64.getEncoder().encodeToString(payloadSigAsBytes)) != 0) { + throw new SegmentSignatureMismatch("segment signature miss match"); + } + + byte[] writeBuf = reader.aesGcm.decrypt(readBuf); + outputStream.write(writeBuf); + + payloadOffset += segment.encryptedSegmentSize; + totalBytes += readBuf.length; + } + } + + +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java new file mode 100644 index 00000000..accf0837 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java @@ -0,0 +1,25 @@ +package io.opentdf.platform.sdk; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class TDFReader { + + public TDFReader(InputStream inputStream) { + } + + public String manifest() { + return "Not Implemented"; + } + + public byte[] readPayload(long index, long length) { + byte[] data = new byte[0]; + return data; + } + + public long PayloadSize() { + return 0; + } +} diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java b/sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java new file mode 100644 index 00000000..65ab6787 --- /dev/null +++ b/sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java @@ -0,0 +1,28 @@ +package io.opentdf.platform.sdk; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +public class TDFWriter { + private final OutputStream outStream; + private ZipWriter archiveWriter; + public static final String TDF_PAYLOAD_FILE_NAME = "0.payload"; + public static final String TDF_MANIFEST_FILE_NAME = "0.manifest.json"; + + public TDFWriter(OutputStream outStream) throws FileNotFoundException { + this.archiveWriter = new ZipWriter(); + this.outStream = outStream; + } + + public void appendManifest(String manifest) throws IOException { + this.archiveWriter.file(TDF_MANIFEST_FILE_NAME, manifest.getBytes(StandardCharsets.UTF_8)); + } + + public void appendPayload(byte[] data) throws IOException { + this.archiveWriter.file(TDF_PAYLOAD_FILE_NAME, data); + } + + public long finish() throws IOException { + return this.archiveWriter.build(outStream); + } +} \ No newline at end of file diff --git a/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java b/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java index 70af96f4..89a58c73 100644 --- a/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java +++ b/sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java @@ -25,7 +25,6 @@ public class ZipWriter { private static final int BASE_YEAR = 1980; private static final int DEFAULT_SECOND_VALUE = 29; private static final int MONTH_SHIFT = 5; - private static class FileBytes { public FileBytes(String name, byte[] data) { this.name = name; @@ -59,7 +58,14 @@ public ZipWriter file(String name, byte[] content) { return this; } - public void build(OutputStream sink) throws IOException { + /** + * Writes the zip file to a stream and returns the number of + * bytes written to the stream + * @param sink + * @return + * @throws IOException + */ + public long build(OutputStream sink) throws IOException { var out = new CountingOutputStream(sink); ArrayList fileInfos = new ArrayList<>(); @@ -80,6 +86,8 @@ public void build(OutputStream sink) throws IOException { final var sizeOfCentralDirectory = out.position - startOfCentralDirectory; writeEndOfCentralDirectory(!streamFiles.isEmpty(), fileInfos.size(), startOfCentralDirectory, sizeOfCentralDirectory, out); + + return out.position; } public byte[] build() throws IOException { @@ -219,7 +227,6 @@ private FileInfo writeByteArray(String name, byte[] data, CountingOutputStream o return fileInfo; } - private void writeEndOfCentralDirectory(boolean hasZip64Entry, long numEntries, long startOfCentralDirectory, long sizeOfCentralDirectory, CountingOutputStream out) throws IOException { var isZip64 = hasZip64Entry || (numEntries & ~0xFF) != 0 diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java new file mode 100644 index 00000000..2b651b06 --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ConfigTest.java @@ -0,0 +1,51 @@ +package io.opentdf.platform.sdk; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ConfigTest { + + @Test + void newTDFConfig_shouldCreateDefaultConfig() { + Config.TDFConfig config = Config.newTDFConfig(); + assertEquals(Config.DEFAULT_SEGMENT_SIZE, config.defaultSegmentSize); + assertTrue(config.enableEncryption); + assertEquals(Config.TDFFormat.JSONFormat, config.tdfFormat); + assertEquals(Config.IntegrityAlgorithm.HS256, config.integrityAlgorithm); + assertEquals(Config.IntegrityAlgorithm.GMAC, config.segmentIntegrityAlgorithm); + assertTrue(config.attributes.isEmpty()); + assertTrue(config.kasInfoList.isEmpty()); + } + + @Test + void withDataAttributes_shouldAddAttributes() { + Config.TDFConfig config = Config.newTDFConfig(Config.withDataAttributes("attr1", "attr2")); + assertEquals(2, config.attributes.size()); + assertTrue(config.attributes.contains("attr1")); + assertTrue(config.attributes.contains("attr2")); + } + + @Test + void withKasInformation_shouldAddKasInfo() { + Config.KASInfo kasInfo = new Config.KASInfo(); + kasInfo.URL = "http://example.com"; + kasInfo.PublicKey = "publicKey"; + Config.TDFConfig config = Config.newTDFConfig(Config.withKasInformation(kasInfo)); + assertEquals(1, config.kasInfoList.size()); + assertEquals(kasInfo, config.kasInfoList.get(0)); + } + + @Test + void withMetaData_shouldSetMetaData() { + Config.TDFConfig config = Config.newTDFConfig(Config.withMetaData("metaData")); + assertEquals("metaData", config.metaData); + } + + @Test + void withSegmentSize_shouldSetSegmentSize() { + Config.TDFConfig config = Config.newTDFConfig(Config.withSegmentSize(1024)); + assertEquals(1024, config.defaultSegmentSize); + } +} \ No newline at end of file diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java new file mode 100644 index 00000000..558cdacc --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/ManifestTest.java @@ -0,0 +1,94 @@ +package io.opentdf.platform.sdk; + +import org.junit.jupiter.api.Test; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ManifestTest { + @Test + void testManifestMarshalAndUnMarshal() { + String kManifestJsonFromTDF = "{\n" + + " \"encryptionInformation\": {\n" + + " \"integrityInformation\": {\n" + + " \"encryptedSegmentSizeDefault\": 1048604,\n" + + " \"rootSignature\": {\n" + + " \"alg\": \"HS256\",\n" + + " \"sig\": \"N2Y1ZjJlYWE4N2EzNjc2Nzc3NzgxNGU2ZGE1NmI4NDNhZTI5ZWY5NDc2OGI1ZTMzYTIyMTU4MDBlZTY3NzQzNA==\"\n" + + " },\n" + + " \"segmentHashAlg\": \"GMAC\",\n" + + " \"segmentSizeDefault\": 1048576,\n" + + " \"segments\": [\n" + + " {\n" + + " \"encryptedSegmentSize\": 41,\n" + + " \"hash\": \"ZWEyZTkwYjZiZThmYWZhNzg5ZmNjOWIyZTA2Njg5OTQ=\",\n" + + " \"segmentSize\": 1048576\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"keyAccess\": [\n" + + " {\n" + + " \"policyBinding\": \"YTgzNThhNzc5NWRhMjdjYThlYjk4ZmNmODliNzc2Y2E5ZmZiZDExZDQ3OTM5ODFjZTRjNmE3MmVjOTUzZTFlMA==\",\n" + + " \"protocol\": \"kas\",\n" + + " \"type\": \"wrapped\",\n" + + " \"url\": \"http://localhost:65432/kas\",\n" + + " \"wrappedKey\": \"dJ3PdscXWvLv/juSkL7EMhl4lgLSBfI9EeoG2ct6NeSwPkPm/ieMF6ryDQjGeqZttoLlx2qBCVpik/BooGd/FtpYMIF/7a5RFTJ3G+o4Lww/zG6zIgV2APEPO+Gp7ORlFyMNJfn6Tj8ChTweKBqfXEXLihTV6sTZFtsWjdV96Z4KXbLe8tGpkXBpUAsSlmjcDJ920vrqnp3dvt2GwfmAiRWYCMXxnqUECqN5kVXMJywcvHatv2ZJSA/ixjDOrix+MocDJ69K/yFA17DXgfjf5X4SLyS0XgaZcXsdACBb+ogBlPw6vAbBrAyqI0Vi1msMRYNDS+FTl1yWEXl1HpyyCw==\"\n" + + " }\n" + + " ],\n" + + " \"method\": {\n" + + " \"algorithm\": \"AES-256-GCM\",\n" + + " \"isStreamable\": true,\n" + + " \"iv\": \"tozen81HLtZktNOP\"\n" + + " },\n" + + " \"policy\": \"eyJib2R5Ijp7ImRhdGFBdHRyaWJ1dGVzIjpbXSwiZGlzc2VtIjpbXX0sInV1aWQiOiJiNTM3MDllMy03NmE3LTRmYzctOGEwZi1mZDBhNjcyNmVhM2YifQ==\",\n" + + " \"type\": \"split\"\n" + + " },\n" + + " \"payload\": {\n" + + " \"isEncrypted\": true,\n" + + " \"mimeType\": \"application/octet-stream\",\n" + + " \"protocol\": \"zip\",\n" + + " \"type\": \"reference\",\n" + + " \"url\": \"0.payload\"\n" + + " }\n" + + "}"; + + GsonBuilder gsonBuilder = new GsonBuilder(); + Gson gson = gsonBuilder.setPrettyPrinting().create(); + Manifest manifest = gson.fromJson(kManifestJsonFromTDF, Manifest.class); + + // Test payload + assertEquals(manifest.payload.url, "0.payload"); + assertEquals(manifest.payload.isEncrypted, true); + + // Test encryptionInformation + assertEquals(manifest.encryptionInformation.keyAccessType, "split"); + assertEquals(manifest.encryptionInformation.keyAccessObj.size(), 1); + + List keyAccess = manifest.encryptionInformation.keyAccessObj; + assertEquals(keyAccess.get(0).keyType, "wrapped"); + assertEquals(keyAccess.get(0).protocol, "kas"); + + assertEquals(manifest.encryptionInformation.method.algorithm, "AES-256-GCM"); + assertEquals(manifest.encryptionInformation.integrityInformation.rootSignature.algorithm, "HS256"); + assertEquals(manifest.encryptionInformation.integrityInformation.segmentHashAlg, "GMAC"); + assertEquals(manifest.encryptionInformation.integrityInformation.segments.get(0).segmentSize, 1048576); + + System.out.println(gson.toJson(manifest)); + + + Manifest.Payload payload = new Manifest.Payload(); + payload.protocol = "zip"; + + Manifest.EncryptionInformation encryptionInformation = manifest.encryptionInformation; + encryptionInformation.policy = "updated policy"; + + Manifest newManifest = new Manifest(); + newManifest.payload = payload; + newManifest.encryptionInformation = encryptionInformation; + + System.out.println(gson.toJson(newManifest)); + } +} diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java new file mode 100644 index 00000000..09841d69 --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFTest.java @@ -0,0 +1,54 @@ +package io.opentdf.platform.sdk; + +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +public class TDFTest { + @Test + void testSimpleTDFEncryptAndDecrypt() throws Exception { + Config.KASInfo kasInfo = new Config.KASInfo(); + kasInfo.URL = "http://127.0.0.1:8080/kas"; + kasInfo.PublicKey = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArvKYimFpxEp58ZGTgiaP\n" + + "RYEzrikTZ3GP0KhWIYrQFAbWdE0qvSS+8LxcUDQoisFk1ux1CO9iuUlyZdKeGsbz\n" + + "sTmJjdk4nHoH5f/BiLzTEJemDIjXPV5vYcY++4QKhFbZf/XLLZ2hSzAuXz5ZOCel\n" + + "A/KZs+Zb19Vlra5DCDJ43mqdoqFIDS4cl8mtuRDC5Uw3x1S52tnO/TKPDGj32aVS\n" + + "GBKh0CWGAXWRmphzGj7kFpkAxT1b827MrQMYxkn4w2WB8B/bGKz0+dWyqnnzGYAS\n" + + "hVJ0rIiNE8dDWzQCRBfivLemXhX8UFICyoS5i0IwenFvTr6T85EvMxK3aSAlGya3\n" + + "3wIDAQAB\n" + + "-----END PUBLIC KEY-----";; + + // TODO: SDK should kas public key in cert format. +//""" +//-----BEGIN CERTIFICATE----- +//MIICmDCCAYACCQC3BCaSANRhYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANr +//YXMwHhcNMjEwOTE1MTQxMTQ4WhcNMjIwOTE1MTQxMTQ4WjAOMQwwCgYDVQQDDANr +//YXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOpiotrvV2i5h6clHM +//zDGgh3h/kMa0LoGx2OkDPd8jogycUh7pgE5GNiN2lpSmFkjxwYMXnyrwr9ExyczB +//WJ7sRGDCDaQg5fjVUIloZ8FJVbn+sEcfQ9iX6vmI9/S++oGK79QM3V8M8cp41r/T +//1YVmuzUHE1say/TLHGhjtGkxHDF8qFy6Z2rYFTCVJQHNqGmwNVGd0qG7gim86Haw +//u/CMYj4jG9oITlj8rJtQOaJ6ZqemQVoNmb3j1LkyeUKzRIt+86aoBiz+T3TfOEvX +//F6xgBj3XoiOhPYK+abFPYcrArvb6oubT8NjjQoj3j0sXWUnIIMg+e4f+XNVU54Zz +//DaLZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABewfZOJ4/KNRE8IQ5TsW/AVn7C1 +//l5ty6tUUBSVi8/df7WYts0bHEdQh9yl9agEU5i4rj43y8vMVZNzSeHcurtV/+C0j +//fbkHQHeiQ1xn7cq3Sbh4UVRyuu4C5PklEH4AN6gxmgXC3kT15uWw8I4nm/plzYLs +//I099IoRfC5djHUYYLMU/VkOIHuPC3sb7J65pSN26eR8bTMVNagk187V/xNwUuvkf +//+NUxDO615/5BwQKnAu5xiIVagYnDZqKCOtYS5qhxF33Nlnwlm7hH8iVZ1RI+n52l +//wVyElqp317Ksz+GtTIc+DE6oryxK3tZd4hrj9fXT4KiJvQ4pcRjpePgH7B8= +//-----END CERTIFICATE----- +//"""; + + + Config.TDFConfig config = Config.newTDFConfig(Config.withKasInformation(kasInfo)); + + String plainText = "text"; + InputStream plainTextInputStream = new ByteArrayInputStream(plainText.getBytes()); + ByteArrayOutputStream tdfOutputStream = new ByteArrayOutputStream(); + + TDF tdf = new TDF(); + tdf.createTDF(plainTextInputStream, plainText.length(), tdfOutputStream, config); + } +} diff --git a/sdk/src/test/java/io/opentdf/platform/sdk/TDFWriterTest.java b/sdk/src/test/java/io/opentdf/platform/sdk/TDFWriterTest.java new file mode 100644 index 00000000..2a4eb0ec --- /dev/null +++ b/sdk/src/test/java/io/opentdf/platform/sdk/TDFWriterTest.java @@ -0,0 +1,67 @@ +package io.opentdf.platform.sdk; + +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TDFWriterTest { + @Test + void simpleTDFCreate() throws IOException { + + String kManifestJsonFromTDF = "{\n" + + " \"encryptionInformation\": {\n" + + " \"integrityInformation\": {\n" + + " \"encryptedSegmentSizeDefault\": 1048604,\n" + + " \"rootSignature\": {\n" + + " \"alg\": \"HS256\",\n" + + " \"sig\": \"N2Y1ZjJlYWE4N2EzNjc2Nzc3NzgxNGU2ZGE1NmI4NDNhZTI5ZWY5NDc2OGI1ZTMzYTIyMTU4MDBlZTY3NzQzNA==\"\n" + + " },\n" + + " \"segmentHashAlg\": \"GMAC\",\n" + + " \"segmentSizeDefault\": 1048576,\n" + + " \"segments\": [\n" + + " {\n" + + " \"encryptedSegmentSize\": 41,\n" + + " \"hash\": \"ZWEyZTkwYjZiZThmYWZhNzg5ZmNjOWIyZTA2Njg5OTQ=\",\n" + + " \"segmentSize\": 1048576\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"keyAccess\": [\n" + + " {\n" + + " \"policyBinding\": \"YTgzNThhNzc5NWRhMjdjYThlYjk4ZmNmODliNzc2Y2E5ZmZiZDExZDQ3OTM5ODFjZTRjNmE3MmVjOTUzZTFlMA==\",\n" + + " \"protocol\": \"kas\",\n" + + " \"type\": \"wrapped\",\n" + + " \"url\": \"http://localhost:65432/kas\",\n" + + " \"wrappedKey\": \"dJ3PdscXWvLv/juSkL7EMhl4lgLSBfI9EeoG2ct6NeSwPkPm/ieMF6ryDQjGeqZttoLlx2qBCVpik/BooGd/FtpYMIF/7a5RFTJ3G+o4Lww/zG6zIgV2APEPO+Gp7ORlFyMNJfn6Tj8ChTweKBqfXEXLihTV6sTZFtsWjdV96Z4KXbLe8tGpkXBpUAsSlmjcDJ920vrqnp3dvt2GwfmAiRWYCMXxnqUECqN5kVXMJywcvHatv2ZJSA/ixjDOrix+MocDJ69K/yFA17DXgfjf5X4SLyS0XgaZcXsdACBb+ogBlPw6vAbBrAyqI0Vi1msMRYNDS+FTl1yWEXl1HpyyCw==\"\n" + + " }\n" + + " ],\n" + + " \"method\": {\n" + + " \"algorithm\": \"AES-256-GCM\",\n" + + " \"isStreamable\": true,\n" + + " \"iv\": \"tozen81HLtZktNOP\"\n" + + " },\n" + + " \"policy\": \"eyJib2R5Ijp7ImRhdGFBdHRyaWJ1dGVzIjpbXSwiZGlzc2VtIjpbXX0sInV1aWQiOiJiNTM3MDllMy03NmE3LTRmYzctOGEwZi1mZDBhNjcyNmVhM2YifQ==\",\n" + + " \"type\": \"split\"\n" + + " },\n" + + " \"payload\": {\n" + + " \"isEncrypted\": true,\n" + + " \"mimeType\": \"application/octet-stream\",\n" + + " \"protocol\": \"zip\",\n" + + " \"type\": \"reference\",\n" + + " \"url\": \"0.payload\"\n" + + " }\n" + + "}"; + String payload = "Hello, world!"; + FileOutputStream fileOutStream = new FileOutputStream("sample.tdf"); + TDFWriter writer = new TDFWriter(fileOutStream); + writer.appendPayload(payload.getBytes()); + writer.appendManifest(kManifestJsonFromTDF); + writer.finish(); + fileOutStream.close(); + } +}