diff --git a/README.md b/README.md index a223434..2a8a8ed 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,92 @@ [![Swift Package Manager compatible](https://img.shields.io/badge/Swift_Package_Manager-compatible-brightgreen.svg?style=flat&colorA=28a745&&colorB=4E4E4E)](https://github.com/apple/swift-package-manager) # Xenissuing -The XenIssuing SDK includes a collection of modules designed to handle sensitive operations with ease and security in your iOS applications. Notably: -- SecureSession: This module is responsible for ensuring encrypted communication between the XenIssuing SDK and your iOS application. +The XenIssuing SDK provides a secure way to handle sensitive operations in your iOS applications. This SDK includes: +- **SecureSession**: A module that ensures encrypted communication between your application and Xendit's services. ## Prerequisites -To utilize the XenIssuing SDK, a public key granted by Xendit is required. You can obtain this key by contacting Xendit directly. +- iOS 10.15 or later +- Swift 5.0 or later +- A public key from Xendit (Contact Xendit to obtain this) ## Usage -### Establishing Secure Sessions - -The SecureSession module aids in establishing an encrypted communication link between the XenIssuing SDK and your application. Below is a Swift example demonstrating how to create a secure session and decrypt card data: +### Creating a Secure Session ```swift import Xenissuing -let secureSession = try Xenissuing.createSecureSession(xenditPublicKeyData: Data(base64Encoded: validPublicKey)!) -let sessionId = secureSession.getKey().base64EncodedString() +let publicKey = """ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... // Your RSA public key without header/footer +""" + +do { + // Create secure session + let secureSession = try Xenissuing.createSecureSession( + xenditPublicKeyData: Data(base64Encoded: publicKey)! + ) + + // Get session ID for API authentication + let sessionId = secureSession.getSessionId().base64EncodedString() + + // Important: URL encode the session ID as it will be used as a URL parameter + let allowedCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") + let encodedSessionId = sessionId.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? "" + + // Use encodedSessionId in API requests + let apiUrl = "https://api.xendit.co/card_issuing/cards/{cardId}/pan?session_id=\(encodedSessionId)" +} catch { + print("Error:", error) +} +``` + +### Public Key Format + +The public key should be: +- An RSA public key provided by Xendit +- Without the "-----BEGIN PUBLIC KEY-----" and "-----END PUBLIC KEY-----" headers +- A single continuous string (can use Swift multi-line string format for readability) + +Example format: +```swift +let publicKey = """ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... +""" +``` + +### Session ID Usage -let decryptedData = secureSession.decryptCardData(secret: secret, iv: iv) +The session ID must be URL encoded because: +- It contains base64 characters that may include '+' and '/' +- It will be used as a URL parameter in API requests +- URL encoding ensures safe transmission of the session ID in HTTP requests + +Example API usage: +```swift +let apiUrl = "https://api.xendit.co/card_issuing//cards/\(cardId)/pan?session_id=\(encodedSessionId)" ``` + +### Decrypting Card Data + +When you receive encrypted card data from Xendit's API: + +```swift +do { + let decryptedData = try secureSession.decryptCardData( + secret: encryptedCardData, // Base64 encoded encrypted data + iv: initializationVector // Base64 encoded IV + ) + + // Process the decrypted card data + let cardInfo = String(data: decryptedData, encoding: .utf8) +} catch { + print("Decryption error:", error) +} +``` + +## Support + +For issues, questions, or assistance, please reach out to the XenIssuing team at Xendit. +- Email: xenissuing@xendit.co +- API Documentation: https://developers.xendit.co diff --git a/Sources/Xenissuing/SecureSession/SecureSession.swift b/Sources/Xenissuing/SecureSession/SecureSession.swift index c90c45d..4499357 100644 --- a/Sources/Xenissuing/SecureSession/SecureSession.swift +++ b/Sources/Xenissuing/SecureSession/SecureSession.swift @@ -78,12 +78,30 @@ public class SecureSession: Crypto { /** Returns the encrypted session key. */ - public func getKey() -> Data { + public func getSessionId() -> Data { + // This should be used for API authentication return self.secureSession!.sealed } + public func getSessionKey() -> Data { + // This should be used for validation + return self.secureSession!.key + } + + // deprecate ambiguous method + @available(*, deprecated, message: "Use getSessionId() instead") + public func getEncryptedKey() -> Data { + return getSessionId() + } + + // deprecate ambiguous method + @available(*, deprecated, message: "Use getSessionKey() instead") + public func getKey() -> Data { + return getSessionKey() + } + public func decryptCardData(secret: String, iv: String) throws -> Data { - return try self.decrypt(secret: secret, sessionKey: self.getKey(), iv: iv) + return try self.decrypt(secret: secret, sessionKey: self.getSessionKey(), iv: iv) } /** @@ -111,9 +129,17 @@ public class SecureSession: Crypto { */ internal func generateSessionId(sessionKey: Data) throws -> SecuredSession { do { + // 1. Base64 encode the session key + let base64Key = sessionKey.base64EncodedString() + + // 2. Convert to raw bytes + let keyBytes = Data(base64Key.utf8) + + // 3. Encrypt using RSA-OAEP-SHA256 let sealed = try self.xenditPublicKey.encrypt( algorithm: .rsaEncryptionOAEPSHA256, - plaintext: sessionKey) + plaintext: keyBytes) + return SecuredSession(key: sessionKey, sealed: sealed) } catch { throw XenError.generateSessionIdError("") @@ -122,8 +148,8 @@ public class SecureSession: Crypto { /** Encrypts data following AES-GCM scheme. - - Parameter plain: the data to encrupt. - - Parameter iv: initilization vector randomly generated + - Parameter plain: the data to encrypt. + - Parameter iv: initialization vector randomly generated - Parameter sessionKey: sessionKey used to encrypt - Throws: `XenError.encryptionError` if there was any issue during encryption. @@ -145,7 +171,7 @@ public class SecureSession: Crypto { Decrypts data that has been encrypted following AES-GCM scheme. - Parameter secret: Secret encoded in base64 format. - Parameter sessionKey: sessionKey used to encrypt. - - Parameter iv: initilization vector or nonce in base64 format + - Parameter iv: initialization vector or nonce in base64 format - Throws: `XenError.decryptionError` if there was any issue during decryption. - Returns: The decrypted text. @@ -186,7 +212,8 @@ public class SecureSession: Crypto { if status != 0 { return nil } - return keyRef as! SecKey + return (keyRef as! SecKey) + } private static func getKeyFromKeychainAsData(tag: String) -> Data? { @@ -249,10 +276,25 @@ private extension SecKey { func encrypt(algorithm: SecKeyAlgorithm, plaintext: Data) throws -> Data { var error: Unmanaged? - let ciphertextO = SecKeyCreateEncryptedData(self, algorithm, - plaintext as CFData, &error) - if let error = error?.takeRetainedValue() { throw error } - guard let ciphertext = ciphertextO else { throw XenError.encryptRSAError("") } - return ciphertext as Data + + // 1. Verify algorithm support + guard SecKeyIsAlgorithmSupported(self, .encrypt, .rsaEncryptionOAEPSHA256) else { + throw XenError.encryptRSAError("RSA-OAEP-SHA256 not supported") + } + + // 2. Perform encryption with OAEP padding + guard let ciphertext = SecKeyCreateEncryptedData( + self, + .rsaEncryptionOAEPSHA256, + plaintext as CFData, + &error + ) as Data? else { + if let error = error?.takeRetainedValue() { + throw error + } + throw XenError.encryptRSAError("Encryption failed") + } + + return ciphertext } } diff --git a/Tests/XenissuingTests/SecureSessionTests.swift b/Tests/XenissuingTests/SecureSessionTests.swift index f1ac23f..50de668 100644 --- a/Tests/XenissuingTests/SecureSessionTests.swift +++ b/Tests/XenissuingTests/SecureSessionTests.swift @@ -40,9 +40,14 @@ final class SecureSessionTests: XCTestCase { let xcrypt = try! SecureSession(xenditPublicKeyData: publicKeyData) let sessionKey = try! xcrypt.generateRandom() let sessionId = try! xcrypt.generateSessionId(sessionKey: sessionKey) + + // Decrypt the sealed data let decryptedBytes = try! privateKey.decrypt(algorithm: .rsaEncryptionOAEPSHA256, ciphertext: sessionId.sealed) let decryptedData = Data(decryptedBytes) - XCTAssertEqual(sessionKey.base64EncodedString(), decryptedData.base64EncodedString()) + + // Convert decrypted bytes back to string and compare with original base64 encoded session key + let decryptedString = String(data: decryptedData, encoding: .utf8)! + XCTAssertEqual(sessionKey.base64EncodedString(), decryptedString) } func testDecrypt() {