From 2035b1a6164f5d4f87f92d6da6f923f2a68aaced Mon Sep 17 00:00:00 2001 From: Sed Date: Tue, 14 Jan 2025 14:41:42 +0800 Subject: [PATCH 1/7] chore: update readme --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a223434..b1c72a9 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,97 @@ [![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 +### Creating a Secure Session -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: +1. First, initialize a secure session with your Xendit public key: ```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 +""" -let decryptedData = secureSession.decryptCardData(secret: secret, iv: iv) +do { + // Create secure session + let secureSession = try Xenissuing.createSecureSession( + xenditPublicKeyData: Data(base64Encoded: publicKey)! + ) + + // Get session key (for validation) + let sessionKey = secureSession.getKey().base64EncodedString() + + // Get encrypted session ID and URL encode it for API requests + let sessionId = secureSession.getEncryptedKey().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 when making API requests, for example: + // https://api.xendit.co/your/endpoint?session_id={encodedSessionId} +} catch { + print("Error creating secure session:", 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 + +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 From dfd785d027b37cf5fa2772580e8218a931b145e4 Mon Sep 17 00:00:00 2001 From: Sed Date: Tue, 14 Jan 2025 15:08:08 +0800 Subject: [PATCH 2/7] fix: rectify the session id generation --- .../SecureSession/SecureSession.swift | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) 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 } } From e530816fe9231fc6841e3fe2a35fcc4f536bd005 Mon Sep 17 00:00:00 2001 From: Sed Date: Tue, 14 Jan 2025 15:27:35 +0800 Subject: [PATCH 3/7] fix: rectify test convert to string --- Tests/XenissuingTests/SecureSessionTests.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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() { From acbbbcf416194bbccbb2230f78fba76802da1870 Mon Sep 17 00:00:00 2001 From: Sed Date: Tue, 14 Jan 2025 15:36:25 +0800 Subject: [PATCH 4/7] chore: nit update on readme --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index b1c72a9..2a8a8ed 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,6 @@ The XenIssuing SDK provides a secure way to handle sensitive operations in your ### Creating a Secure Session -1. First, initialize a secure session with your Xendit public key: - ```swift import Xenissuing @@ -30,20 +28,17 @@ do { xenditPublicKeyData: Data(base64Encoded: publicKey)! ) - // Get session key (for validation) - let sessionKey = secureSession.getKey().base64EncodedString() - - // Get encrypted session ID and URL encode it for API requests - let sessionId = secureSession.getEncryptedKey().base64EncodedString() + // 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 when making API requests, for example: - // https://api.xendit.co/your/endpoint?session_id={encodedSessionId} + // Use encodedSessionId in API requests + let apiUrl = "https://api.xendit.co/card_issuing/cards/{cardId}/pan?session_id=\(encodedSessionId)" } catch { - print("Error creating secure session:", error) + print("Error:", error) } ``` From ecade41f7bca52cded43faf323644b58ed68e730 Mon Sep 17 00:00:00 2001 From: Sed Date: Tue, 14 Jan 2025 16:56:40 +0800 Subject: [PATCH 5/7] testing workflow failing issue --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 4dd2bfd..8202514 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,7 +9,7 @@ jobs: matrix: swift: ["5.5", "5.6"] fail-fast: false - runs-on: macos-11 + runs-on: macos-latest steps: - uses: swift-actions/setup-swift@v1 with: From 28f25a1d0ea21257c372eeb8e5f4e9dcb4d51bfc Mon Sep 17 00:00:00 2001 From: Sed Date: Tue, 14 Jan 2025 17:03:02 +0800 Subject: [PATCH 6/7] fix --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8202514..a679322 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -7,7 +7,7 @@ jobs: name: Swift ${{ matrix.swift }} strategy: matrix: - swift: ["5.5", "5.6"] + swift: ["5.3", "5.4"] fail-fast: false runs-on: macos-latest steps: From 6d4522be69eb889224939e4e4144683226fe3281 Mon Sep 17 00:00:00 2001 From: Sed Date: Tue, 14 Jan 2025 17:07:30 +0800 Subject: [PATCH 7/7] revert --- .github/workflows/tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a679322..4dd2bfd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -7,9 +7,9 @@ jobs: name: Swift ${{ matrix.swift }} strategy: matrix: - swift: ["5.3", "5.4"] + swift: ["5.5", "5.6"] fail-fast: false - runs-on: macos-latest + runs-on: macos-11 steps: - uses: swift-actions/setup-swift@v1 with: