diff --git a/OpenTDFKit/KASRewrapClient.swift b/OpenTDFKit/KASRewrapClient.swift index 4b33cd1..ab4e05d 100644 --- a/OpenTDFKit/KASRewrapClient.swift +++ b/OpenTDFKit/KASRewrapClient.swift @@ -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? @@ -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) } @@ -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, @@ -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( @@ -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, ) diff --git a/OpenTDFKit/TDF/TDFCrypto.swift b/OpenTDFKit/TDF/TDFCrypto.swift index f9bf71a..1dc04cc 100644 --- a/OpenTDFKit/TDF/TDFCrypto.swift +++ b/OpenTDFKit/TDF/TDFCrypto.swift @@ -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)) @@ -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)) diff --git a/OpenTDFKitCLI/Commands.swift b/OpenTDFKitCLI/Commands.swift index 7c9f695..ea31dc5 100644 --- a/OpenTDFKitCLI/Commands.swift +++ b/OpenTDFKitCLI/Commands.swift @@ -124,7 +124,6 @@ enum Commands { filename: String, symmetricKey: SymmetricKey?, privateKeyPEM: String?, - clientPublicKeyPEM: String?, oauthToken: String?, ) async throws -> Data { print("Standard TDF Decryption") @@ -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)) @@ -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 { diff --git a/OpenTDFKitCLI/main.swift b/OpenTDFKitCLI/main.swift index 1fec9a3..5b05691 100644 --- a/OpenTDFKitCLI/main.swift +++ b/OpenTDFKitCLI/main.swift @@ -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( @@ -352,7 +350,6 @@ struct OpenTDFKitCLI { filename: inputURL.lastPathComponent, symmetricKey: symmetricKey, privateKeyPEM: privateKey, - clientPublicKeyPEM: clientPublicKey, oauthToken: oauthToken, ) usedStandardTDF = true diff --git a/OpenTDFKitTests/IntegrationTests.swift b/OpenTDFKitTests/IntegrationTests.swift index f0955e2..89f32e9 100644 --- a/OpenTDFKitTests/IntegrationTests.swift +++ b/OpenTDFKitTests/IntegrationTests.swift @@ -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") diff --git a/OpenTDFKitTests/KASRewrapClientTests.swift b/OpenTDFKitTests/KASRewrapClientTests.swift index 812453d..989bcef 100644 --- a/OpenTDFKitTests/KASRewrapClientTests.swift +++ b/OpenTDFKitTests/KASRewrapClientTests.swift @@ -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, )