Skip to content

Developer Experience Improvements for StandardTDF Encryption/Decryption #24

@arkavo-com

Description

@arkavo-com

Summary

While integrating OpenTDFKit for encrypting user content as TDF files, I encountered several areas where developer experience could be improved, particularly around StandardTDF encryption/decryption workflows.

Current Implementation Challenges

1. Key Unwrapping After KAS Rewrap

The KASRewrapClient.rewrapStandardTDF method returns StandardTDFKASRewrapResult with wrapped keys, but there's no clear documentation or helper method for unwrapping these keys to obtain the symmetric key.

Current Code:

let rewrapResult = try await rewrapClient.rewrapStandardTDF(
    manifest: container.manifest,
    clientPublicKeyPEM: clientPublicKeyPEM
)

// How to unwrap rewrapResult.wrappedKeys to get SymmetricKey?
let (_, rewrappedKeyData) = rewrapResult.wrappedKeys.first!
// Need guidance on proper ECDH key derivation here
let symmetricKey = SymmetricKey(data: rewrappedKeyData) // Is this correct?

Suggested Improvement:

  • Add a helper method or documentation explaining the key derivation process
  • Example: StandardTDFCrypto.deriveSymmetricKey(from: rewrappedKeyData, using: clientPrivateKey)

2. StandardTDFDecryptor Limitations

The StandardTDFDecryptor has methods for file-based decryption but lacks a convenient end-to-end method that handles KAS rewrap automatically.

Current Options:

  • decryptFile(inputURL:outputURL:symmetricKey:) - Requires pre-obtained symmetric key
  • decryptFile(inputURL:outputURL:privateKeyPEM:) - Works with local private key but not KAS

Suggested Improvement:
Add a method that handles the full KAS rewrap flow:

public func decryptFile(
    inputURL: URL,
    outputURL: URL,
    kasURL: URL,
    oauthToken: String
) async throws

// Or even simpler:
public func decrypt(
    container: StandardTDFContainer,
    kasClient: KASRewrapClient
) async throws -> Data

3. KAS Public Key Fetching

Fetching the KAS public key requires manual URL construction and JSON parsing. This is a common operation that could be simplified.

Current Code:

let url = URL(string: "\(kasEndpoint)/v2/kas_public_key")!
let (data, _) = try await URLSession.shared.data(from: url)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let publicKey = json["publicKey"] as? String

Suggested Improvement:
Add a static helper method:

public extension StandardTDFKasInfo {
    static func fetchPublicKey(from kasURL: URL) async throws -> String
}

4. DER to PEM Conversion

Converting key representations for KAS is a common need but not provided by the library.

Current Code:

private func convertDERToPEM(_ derData: Data, type: String) throws -> String {
    let base64 = derData.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed])
    return "-----BEGIN \(type)-----\n\(base64)-----END \(type)-----\n"
}

Suggested Improvement:
Add to StandardTDFCrypto or a KeyConversion utility:

public extension StandardTDFCrypto {
    static func pemRepresentation(of publicKey: P256.KeyAgreement.PublicKey) -> String
}

5. Documentation Gaps

Missing Documentation:

  • End-to-end example of StandardTDF encryption with KAS
  • Complete decrypt workflow including KAS rewrap
  • Key derivation process after rewrap
  • Policy structure examples with real-world use cases (e.g., user access control)

Suggested Additions:

  • Add "Getting Started" guide for StandardTDF with KAS
  • Document the full encrypt/decrypt cycle
  • Include code examples in README
  • API documentation for StandardTDFKASRewrapResult

6. Policy Builder Ergonomics

Creating policy JSON manually is error-prone and verbose.

Current Code:

let policyJSON = """
{
    "uuid": "\(UUID().uuidString)",
    "body": {
        "dataAttributes": [],
        "dissem": ["\(ownerId)"]
    }
}
""".data(using: .utf8)!

let policy = try StandardTDFPolicy(json: policyJSON)

Suggested Improvement:
Add a policy builder:

let policy = StandardTDFPolicy.Builder()
    .withUUID(UUID().uuidString)
    .withDissemination([ownerId])
    .withDataAttributes([])
    .build()

Suggested New APIs

High-Level Encryption/Decryption

// Simplified encryption/decryption client
public struct StandardTDFClient {
    public init(kasURL: URL, oauthToken: String)
    
    public func encrypt(
        plaintext: Data,
        policy: StandardTDFPolicy,
        mimeType: String?
    ) async throws -> StandardTDFContainer
    
    public func decrypt(
        container: StandardTDFContainer
    ) async throws -> Data
}

Key Management Helpers

public extension StandardTDFCrypto {
    // Unwrap symmetric key after KAS rewrap
    static func unwrapSymmetricKey(
        rewrappedKey: Data,
        clientPrivateKey: P256.KeyAgreement.PrivateKey,
        kasPublicKey: P256.KeyAgreement.PublicKey?
    ) throws -> SymmetricKey
}

Implementation Context

Use Case: iOS app encrypting user-generated content as TDF files

  • Files stored locally in Documents directory
  • Shared via iOS share sheet or web links
  • iOS 18.0+ deployment target

Current Status: Successfully building and integrating, but required several workarounds and assumptions about key derivation.

Priority

Medium-High - These improvements would significantly reduce integration friction for new OpenTDFKit users.

Workarounds Applied

  1. Manual key unwrapping (may be incorrect for production)
  2. Custom KAS public key fetching
  3. Custom DER-to-PEM conversion
  4. Manual policy JSON construction
  5. Direct use of KASRewrapClient instead of higher-level APIs

Additional Context

The library is very powerful and well-structured at the low level, but lacks some convenience APIs for common workflows. Adding these would make OpenTDFKit more accessible to developers who want secure TDF encryption without deep cryptographic knowledge.

Thank you for maintaining this library! Happy to contribute PRs for any of these improvements if there's interest.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions