Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions OpenTDFKit/KASRewrapClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,20 @@ public class KASRewrapClient: KASRewrapClientProtocol {

public struct StandardPolicyBinding: Codable {
let hash: String
let algorithm: String?
let alg: String?

enum CodingKeys: String, CodingKey {
case hash
case alg
}
}

public struct StandardKeyAccessObject: Codable {
let keyType: String?
let kasUrl: String
let `protocol`: String
let wrappedKey: Data
let policyBinding: StandardPolicyBinding
let wrappedKey: String // Base64-encoded RSA-wrapped DEK for Standard TDF
let policyBinding: StandardPolicyBinding?
let encryptedMetadata: String?
let kid: String?
let splitId: String?
Expand Down Expand Up @@ -339,12 +344,17 @@ public class KASRewrapClient: KASRewrapClientProtocol {
/// Perform Standard TDF rewrap request to KAS.
/// - Parameters:
/// - manifest: The parsed TDF manifest containing key access entries.
/// - clientPublicKeyPEM: PEM-encoded client public key for the returned wrapped key.
/// - clientPrivateKey: Client's P-256 private key for ECDH. Its public key is sent in the request
/// and the same key is used to sign the JWT (converted to signing key).
/// - Returns: Mapping of KeyAccessObjectId to wrapped key data and optional session public key when EC wrapping is used.
public func rewrapTDF(
manifest: TDFManifest,
clientPublicKeyPEM: String,
clientPrivateKey: P256.KeyAgreement.PrivateKey,
) async throws -> TDFKASRewrapResult {
// Convert KeyAgreement key to Signing key (same underlying P-256 key)
let signingKey = try P256.Signing.PrivateKey(rawRepresentation: clientPrivateKey.rawRepresentation)
let clientPublicKeyPEM = clientPrivateKey.publicKey.pemRepresentation

let policyBody = manifest.encryptionInformation.policy
let keyAccessEntries = manifest.encryptionInformation.keyAccess.filter { matchesKasURL($0.url) }

Expand All @@ -356,20 +366,22 @@ public class KASRewrapClient: KASRewrapClientProtocol {
wrappers.reserveCapacity(keyAccessEntries.count)

for (index, kao) in keyAccessEntries.enumerated() {
guard let wrappedKeyData = Data(base64Encoded: kao.wrappedKey) else {
// Validate the wrapped key is valid base64
guard Data(base64Encoded: kao.wrappedKey) != nil else {
throw KASRewrapError.invalidWrappedKeyFormat
}

let binding = StandardPolicyBinding(
hash: kao.policyBinding.hash,
algorithm: kao.policyBinding.alg,
alg: kao.policyBinding.alg,
)

// Standard TDF uses wrappedKey field with base64-encoded RSA-wrapped DEK
let accessObject = StandardKeyAccessObject(
keyType: kao.type.rawValue,
kasUrl: kao.url,
protocol: kao.protocolValue.rawValue,
wrappedKey: wrappedKeyData,
wrappedKey: kao.wrappedKey, // Base64-encoded DEK encrypted with KAS public key
policyBinding: binding,
encryptedMetadata: kao.encryptedMetadata,
kid: kao.kid,
Expand All @@ -388,7 +400,7 @@ public class KASRewrapClient: KASRewrapClientProtocol {
let policyRequest = StandardPolicyRequest(
keyAccessObjects: wrappers,
policy: policy,
algorithm: nil,
algorithm: "rsa:2048", // Specify RSA algorithm for KAS
)

let unsignedRequest = StandardUnsignedRewrapRequest(
Expand Down Expand Up @@ -516,12 +528,10 @@ public class KASRewrapClient: KASRewrapClientProtocol {
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)

// Derive symmetric key using HKDF
// Use the same salt as NanoTDF encryption (SHA256 of magic + version)
let salt = CryptoConstants.hkdfSalt // This is SHA256("L1L") for v12 compatibility

// For Standard TDF rewrap: empty salt, empty info (matches KAS implementation)
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: salt,
salt: Data(),
sharedInfo: Data(),
outputByteCount: 32,
)
Expand Down
4 changes: 2 additions & 2 deletions OpenTDFKit/TDF/TDFCrypto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public enum TDFCrypto {
let aes = try CryptoSwift.AES(
key: Array(keyData),
blockMode: CryptoSwift.CBC(iv: Array(ivData)),
padding: .pkcs7
padding: .pkcs7,
)

let ciphertext = try aes.encrypt(Array(plaintext))
Expand All @@ -117,7 +117,7 @@ public enum TDFCrypto {
let aes = try CryptoSwift.AES(
key: Array(keyData),
blockMode: CryptoSwift.CBC(iv: Array(iv)),
padding: .pkcs7
padding: .pkcs7,
)

let plaintext = try aes.decrypt(Array(ciphertext))
Expand Down
10 changes: 4 additions & 6 deletions OpenTDFKitCLI/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ enum Commands {
filename: String,
symmetricKey: SymmetricKey?,
privateKeyPEM: String?,
clientPublicKeyPEM: String?,
oauthToken: String?,
) async throws -> Data {
print("Standard TDF Decryption")
Expand All @@ -150,16 +149,15 @@ enum Commands {
throw DecryptError.missingSymmetricMaterial
}

guard let clientPublicKeyPEM, !clientPublicKeyPEM.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw DecryptError.missingSymmetricMaterial
}

guard let oauthToken, !oauthToken.isEmpty else {
throw DecryptError.missingOAuthToken
}

print(" Requesting rewrap from KAS")

// Generate ephemeral P-256 key pair for JWT signing in rewrap request
let ephemeralPrivateKey = P256.KeyAgreement.PrivateKey()

var aggregatedWrappedKeys: [String: Data] = [:]
let uniqueKasURLs = Set(container.manifest.encryptionInformation.keyAccess.map(\.url))

Expand All @@ -171,7 +169,7 @@ enum Commands {
let client = KASRewrapClient(kasURL: kasURL, oauthToken: oauthToken)
let result = try await client.rewrapTDF(
manifest: container.manifest,
clientPublicKeyPEM: clientPublicKeyPEM,
clientPrivateKey: ephemeralPrivateKey,
)

for (kaoIdentifier, wrappedKey) in result.wrappedKeys {
Expand Down
3 changes: 0 additions & 3 deletions OpenTDFKitCLI/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -330,11 +330,9 @@ struct OpenTDFKitCLI {
case .tdf, .ztdf:
let symmetricKey = try loadSymmetricKeyFromEnvironment()
let privateKey = try loadPrivateKeyPEMFromEnvironment()
var clientPublicKey: String? = nil
var oauthToken: String? = nil

if symmetricKey == nil {
clientPublicKey = try loadClientPublicKeyPEMFromEnvironment()
let env = ProcessInfo.processInfo.environment
let tokenPath = env["TDF_OAUTH_TOKEN_PATH"] ?? env["OAUTH_TOKEN_PATH"] ?? "fresh_token.txt"
oauthToken = try? Commands.resolveOAuthToken(
Expand All @@ -352,7 +350,6 @@ struct OpenTDFKitCLI {
filename: inputURL.lastPathComponent,
symmetricKey: symmetricKey,
privateKeyPEM: privateKey,
clientPublicKeyPEM: clientPublicKey,
oauthToken: oauthToken,
)
usedStandardTDF = true
Expand Down
5 changes: 4 additions & 1 deletion OpenTDFKitTests/IntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -357,10 +357,13 @@ final class IntegrationTests: XCTestCase {
return
}

// Generate ephemeral P-256 key for JWT signing in rewrap request
let ephemeralPrivateKey = P256.KeyAgreement.PrivateKey()

let kasClient = KASRewrapClient(kasURL: kasURL, oauthToken: token)
let rewrapResult = try await kasClient.rewrapTDF(
manifest: container.manifest,
clientPublicKeyPEM: clientPrivateKey.publicKeyPEM,
clientPrivateKey: ephemeralPrivateKey,
)

XCTAssertFalse(rewrapResult.wrappedKeys.isEmpty, "Should receive wrapped keys from KAS")
Expand Down
4 changes: 2 additions & 2 deletions OpenTDFKitTests/KASRewrapClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,10 @@ final class KASRewrapClientTests: XCTestCase {

let sharedSecret = try sessionPrivateKey.sharedSecretFromKeyAgreement(with: clientPrivateKey.publicKey)

let salt = CryptoConstants.hkdfSalt
// Use empty salt/info to match KAS implementation
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: salt,
salt: Data(),
sharedInfo: Data(),
outputByteCount: 32,
)
Expand Down
Loading