Skip to content

Commit e11672e

Browse files
authored
Merge pull request #8 from xendit/fix/invalid-skey-length
chore: update readme
2 parents 5e3f318 + 6d4522b commit e11672e

File tree

3 files changed

+137
-22
lines changed

3 files changed

+137
-22
lines changed

Diff for: README.md

+77-9
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,92 @@
22
[![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)
33
# Xenissuing
44

5-
The XenIssuing SDK includes a collection of modules designed to handle sensitive operations with ease and security in your iOS applications. Notably:
6-
- SecureSession: This module is responsible for ensuring encrypted communication between the XenIssuing SDK and your iOS application.
5+
The XenIssuing SDK provides a secure way to handle sensitive operations in your iOS applications. This SDK includes:
6+
- **SecureSession**: A module that ensures encrypted communication between your application and Xendit's services.
77

88
## Prerequisites
99

10-
To utilize the XenIssuing SDK, a public key granted by Xendit is required. You can obtain this key by contacting Xendit directly.
10+
- iOS 10.15 or later
11+
- Swift 5.0 or later
12+
- A public key from Xendit (Contact Xendit to obtain this)
1113

1214
## Usage
1315

14-
### Establishing Secure Sessions
15-
16-
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:
16+
### Creating a Secure Session
1717

1818
```swift
1919
import Xenissuing
2020

21-
let secureSession = try Xenissuing.createSecureSession(xenditPublicKeyData: Data(base64Encoded: validPublicKey)!)
22-
let sessionId = secureSession.getKey().base64EncodedString()
21+
let publicKey = """
22+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... // Your RSA public key without header/footer
23+
"""
24+
25+
do {
26+
// Create secure session
27+
let secureSession = try Xenissuing.createSecureSession(
28+
xenditPublicKeyData: Data(base64Encoded: publicKey)!
29+
)
30+
31+
// Get session ID for API authentication
32+
let sessionId = secureSession.getSessionId().base64EncodedString()
33+
34+
// Important: URL encode the session ID as it will be used as a URL parameter
35+
let allowedCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")
36+
let encodedSessionId = sessionId.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? ""
37+
38+
// Use encodedSessionId in API requests
39+
let apiUrl = "https://api.xendit.co/card_issuing/cards/{cardId}/pan?session_id=\(encodedSessionId)"
40+
} catch {
41+
print("Error:", error)
42+
}
43+
```
44+
45+
### Public Key Format
46+
47+
The public key should be:
48+
- An RSA public key provided by Xendit
49+
- Without the "-----BEGIN PUBLIC KEY-----" and "-----END PUBLIC KEY-----" headers
50+
- A single continuous string (can use Swift multi-line string format for readability)
51+
52+
Example format:
53+
```swift
54+
let publicKey = """
55+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
56+
"""
57+
```
58+
59+
### Session ID Usage
2360

24-
let decryptedData = secureSession.decryptCardData(secret: secret, iv: iv)
61+
The session ID must be URL encoded because:
62+
- It contains base64 characters that may include '+' and '/'
63+
- It will be used as a URL parameter in API requests
64+
- URL encoding ensures safe transmission of the session ID in HTTP requests
65+
66+
Example API usage:
67+
```swift
68+
let apiUrl = "https://api.xendit.co/card_issuing//cards/\(cardId)/pan?session_id=\(encodedSessionId)"
2569
```
70+
71+
### Decrypting Card Data
72+
73+
When you receive encrypted card data from Xendit's API:
74+
75+
```swift
76+
do {
77+
let decryptedData = try secureSession.decryptCardData(
78+
secret: encryptedCardData, // Base64 encoded encrypted data
79+
iv: initializationVector // Base64 encoded IV
80+
)
81+
82+
// Process the decrypted card data
83+
let cardInfo = String(data: decryptedData, encoding: .utf8)
84+
} catch {
85+
print("Decryption error:", error)
86+
}
87+
```
88+
89+
## Support
90+
91+
For issues, questions, or assistance, please reach out to the XenIssuing team at Xendit.
92+
93+
- API Documentation: https://developers.xendit.co

Diff for: Sources/Xenissuing/SecureSession/SecureSession.swift

+54-12
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,30 @@ public class SecureSession: Crypto {
7878
/**
7979
Returns the encrypted session key.
8080
*/
81-
public func getKey() -> Data {
81+
public func getSessionId() -> Data {
82+
// This should be used for API authentication
8283
return self.secureSession!.sealed
8384
}
8485

86+
public func getSessionKey() -> Data {
87+
// This should be used for validation
88+
return self.secureSession!.key
89+
}
90+
91+
// deprecate ambiguous method
92+
@available(*, deprecated, message: "Use getSessionId() instead")
93+
public func getEncryptedKey() -> Data {
94+
return getSessionId()
95+
}
96+
97+
// deprecate ambiguous method
98+
@available(*, deprecated, message: "Use getSessionKey() instead")
99+
public func getKey() -> Data {
100+
return getSessionKey()
101+
}
102+
85103
public func decryptCardData(secret: String, iv: String) throws -> Data {
86-
return try self.decrypt(secret: secret, sessionKey: self.getKey(), iv: iv)
104+
return try self.decrypt(secret: secret, sessionKey: self.getSessionKey(), iv: iv)
87105
}
88106

89107
/**
@@ -111,9 +129,17 @@ public class SecureSession: Crypto {
111129
*/
112130
internal func generateSessionId(sessionKey: Data) throws -> SecuredSession {
113131
do {
132+
// 1. Base64 encode the session key
133+
let base64Key = sessionKey.base64EncodedString()
134+
135+
// 2. Convert to raw bytes
136+
let keyBytes = Data(base64Key.utf8)
137+
138+
// 3. Encrypt using RSA-OAEP-SHA256
114139
let sealed = try self.xenditPublicKey.encrypt(
115140
algorithm: .rsaEncryptionOAEPSHA256,
116-
plaintext: sessionKey)
141+
plaintext: keyBytes)
142+
117143
return SecuredSession(key: sessionKey, sealed: sealed)
118144
} catch {
119145
throw XenError.generateSessionIdError("")
@@ -122,8 +148,8 @@ public class SecureSession: Crypto {
122148

123149
/**
124150
Encrypts data following AES-GCM scheme.
125-
- Parameter plain: the data to encrupt.
126-
- Parameter iv: initilization vector randomly generated
151+
- Parameter plain: the data to encrypt.
152+
- Parameter iv: initialization vector randomly generated
127153
- Parameter sessionKey: sessionKey used to encrypt
128154
- Throws: `XenError.encryptionError`
129155
if there was any issue during encryption.
@@ -145,7 +171,7 @@ public class SecureSession: Crypto {
145171
Decrypts data that has been encrypted following AES-GCM scheme.
146172
- Parameter secret: Secret encoded in base64 format.
147173
- Parameter sessionKey: sessionKey used to encrypt.
148-
- Parameter iv: initilization vector or nonce in base64 format
174+
- Parameter iv: initialization vector or nonce in base64 format
149175
- Throws: `XenError.decryptionError`
150176
if there was any issue during decryption.
151177
- Returns: The decrypted text.
@@ -186,7 +212,8 @@ public class SecureSession: Crypto {
186212
if status != 0 {
187213
return nil
188214
}
189-
return keyRef as! SecKey
215+
return (keyRef as! SecKey)
216+
190217
}
191218

192219
private static func getKeyFromKeychainAsData(tag: String) -> Data? {
@@ -249,10 +276,25 @@ private extension SecKey {
249276

250277
func encrypt(algorithm: SecKeyAlgorithm, plaintext: Data) throws -> Data {
251278
var error: Unmanaged<CFError>?
252-
let ciphertextO = SecKeyCreateEncryptedData(self, algorithm,
253-
plaintext as CFData, &error)
254-
if let error = error?.takeRetainedValue() { throw error }
255-
guard let ciphertext = ciphertextO else { throw XenError.encryptRSAError("") }
256-
return ciphertext as Data
279+
280+
// 1. Verify algorithm support
281+
guard SecKeyIsAlgorithmSupported(self, .encrypt, .rsaEncryptionOAEPSHA256) else {
282+
throw XenError.encryptRSAError("RSA-OAEP-SHA256 not supported")
283+
}
284+
285+
// 2. Perform encryption with OAEP padding
286+
guard let ciphertext = SecKeyCreateEncryptedData(
287+
self,
288+
.rsaEncryptionOAEPSHA256,
289+
plaintext as CFData,
290+
&error
291+
) as Data? else {
292+
if let error = error?.takeRetainedValue() {
293+
throw error
294+
}
295+
throw XenError.encryptRSAError("Encryption failed")
296+
}
297+
298+
return ciphertext
257299
}
258300
}

Diff for: Tests/XenissuingTests/SecureSessionTests.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,14 @@ final class SecureSessionTests: XCTestCase {
4040
let xcrypt = try! SecureSession(xenditPublicKeyData: publicKeyData)
4141
let sessionKey = try! xcrypt.generateRandom()
4242
let sessionId = try! xcrypt.generateSessionId(sessionKey: sessionKey)
43+
44+
// Decrypt the sealed data
4345
let decryptedBytes = try! privateKey.decrypt(algorithm: .rsaEncryptionOAEPSHA256, ciphertext: sessionId.sealed)
4446
let decryptedData = Data(decryptedBytes)
45-
XCTAssertEqual(sessionKey.base64EncodedString(), decryptedData.base64EncodedString())
47+
48+
// Convert decrypted bytes back to string and compare with original base64 encoded session key
49+
let decryptedString = String(data: decryptedData, encoding: .utf8)!
50+
XCTAssertEqual(sessionKey.base64EncodedString(), decryptedString)
4651
}
4752

4853
func testDecrypt() {

0 commit comments

Comments
 (0)