Skip to content

Commit

Permalink
feat: implementing asymmetric encryption to generate session id
Browse files Browse the repository at this point in the history
  • Loading branch information
sammous committed Jan 23, 2023
1 parent b9788be commit bf74b53
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 25 deletions.
14 changes: 14 additions & 0 deletions Sources/Xenissuing/Error/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ public enum XenError: Error {
case decryptionError
case genericError
case generateRandomKeyError
case generateSessionIdError
case updateKeychainError
case convertKeyDataError
case encryptRSAError
}

extension XenError: LocalizedError {
Expand All @@ -18,6 +22,16 @@ extension XenError: LocalizedError {
return NSLocalizedString("There was an error while trying to decrypt using XenIssuing library.", comment: "Please make sure you are properly decrypted values.")
case .generateRandomKeyError:
return NSLocalizedString("There was an error while generating a random key", comment: "XenError")
case .generateSessionIdError:
return NSLocalizedString("There was an error while generating a session id", comment: "XenError")
case .updateKeychainError:
return NSLocalizedString("There was an error while tring to update the keychain", comment: "XenError")
case .convertKeyDataError:
return NSLocalizedString("There was an error while trying to read the public key data", comment: "XenError")
case .encryptRSAError:
return NSLocalizedString("There was an error while trying to encrypt with RSA", comment: "XenError")
case .generateRandomKeyError:
return NSLocalizedString("There was an error while generating a random key", comment: "XenError")
}
}
}
138 changes: 127 additions & 11 deletions Sources/Xenissuing/XenCrypt/XenCrypt.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Crypto
import CryptoKit
import CryptoSwift
import Foundation
import Security

/// https://stackoverflow.com/a/57912525
@available(macOS 10.15, *)
Expand All @@ -16,9 +18,8 @@ extension SymmetricKey {
}

protocol Crypto {
var xenditKey: Data { get }
func generateRandom() throws -> Data
func generateSessionId(sessionKey: Data) -> EncryptedMessage
func generateSessionId(sessionKey: Data) throws -> EncryptedMessage
func encrypt(plain: Data, iv: Data, sessionKey: Data) throws -> EncryptedMessage
func decrypt(secret: String, sessionKey: Data, iv: String) throws -> Data
}
Expand All @@ -38,17 +39,42 @@ public struct EncryptedMessage {
@available(macOS 10.15, *)
public class XenCrypt: Crypto {
/// The key provided by Xendit.
let xenditKey: Data
let xenditPublicKey: SecKey

/**
Initializes XenCrypt with the provided public key.
If no tag exists, it will save the provided key to the tag provided.

- Parameters:
- xenditKey: The public key provided by Xendit.

- xenditPublicKeyData: Public Key, expects PEM base64 format without Headers or Footers.
- xenditPublicKeyTag: Public Key Tag. If provided, it will try to check first keychain to get the key data.
- Returns: Module to help with encryption.
*/
init(xenditKey: Data) { self.xenditKey = xenditKey }
init(xenditPublicKeyData: Data, xenditPublicKeyTag: String? = nil) throws {
if let publicTag = xenditPublicKeyTag {
if let keyFromKeychain = XenCrypt.getKeyFromKeychainAsData(tag: publicTag) {
if keyFromKeychain != xenditPublicKeyData {
if !XenCrypt.updateKeychain(tag: publicTag, key: xenditPublicKeyData) {
throw XenError.updateKeychainError
}
}
self.xenditPublicKey = XenCrypt.getKeyFromKeychain(tag: publicTag)!
} else if let key = XenCrypt.createKeyFromData(key: xenditPublicKeyData) {
if !XenCrypt.updateKeychain(tag: publicTag, key: xenditPublicKeyData) {
throw XenError.updateKeychainError
}
self.xenditPublicKey = key
} else {
throw XenError.convertKeyDataError
}
} else {
if let key = XenCrypt.createKeyFromData(key: xenditPublicKeyData) {
self.xenditPublicKey = XenCrypt.createKeyFromData(key: xenditPublicKeyData)!
} else {
throw XenError.convertKeyDataError
}
}
}

/**
Generates a random 24 bytes keys.
Expand All @@ -70,11 +96,15 @@ public class XenCrypt: Crypto {
if there was any issue during encryption.
- Returns: The encrypted text
*/
public func generateSessionId(sessionKey: Data) -> EncryptedMessage {
let iv = AES.randomIV(AES.blockSize)
let aes = try! AES(key: xenditKey.bytes, blockMode: CBC(iv: iv), padding: .pkcs7)
let sealed = try! aes.encrypt(sessionKey.bytes)
return EncryptedMessage(key: xenditKey, sealed: Data(iv + sealed))
public func generateSessionId(sessionKey: Data) throws -> EncryptedMessage {
do {
let sealed = try self.xenditPublicKey.encrypt(
algorithm: .rsaEncryptionOAEPSHA256,
plaintext: sessionKey)
return EncryptedMessage(key: sessionKey, sealed: sealed)
} catch {
throw XenError.generateSessionIdError
}
}

/**
Expand Down Expand Up @@ -130,4 +160,90 @@ public class XenCrypt: Crypto {
throw XenError.decryptionError
}
}

private static func getKeyFromKeychain(tag: String) -> SecKey? {
var keyRef: AnyObject?
let attributes: [String: Any] = [
String(kSecAttrKeyType): kSecAttrKeyTypeRSA,
String(kSecClass): kSecClassKey,
String(kSecAttrApplicationTag): tag,
String(kSecReturnRef): true
]
let status = SecItemCopyMatching(attributes as CFDictionary, &keyRef)
if status != 0 {
return nil
}
return keyRef as! SecKey
}

private static func getKeyFromKeychainAsData(tag: String) -> Data? {
var keyRef: AnyObject?
let attributes: [String: Any] = [
String(kSecAttrKeyType): kSecAttrKeyTypeRSA,
String(kSecClass): kSecClassKey,
String(kSecAttrApplicationTag): tag,
String(kSecReturnRef): true
]
let status = SecItemCopyMatching(attributes as CFDictionary, &keyRef)
if status != 0 {
return nil
}
return keyRef as? Data
}

private static func updateKeychain(tag: String, key: Data) -> Bool {
let attributes: [String: Any] = [
String(kSecAttrKeyType): kSecAttrKeyTypeRSA,
String(kSecClass): kSecClassKey,
String(kSecAttrApplicationTag): tag
]
return SecItemUpdate(attributes as CFDictionary, [String(kSecValueData): key] as CFDictionary) == errSecSuccess
}

private static func addToKeychain(tag: String, key: Data) -> SecKey? {
let attributes: [String: Any] = [
String(kSecAttrKeyType): kSecAttrKeyTypeRSA,
String(kSecClass): kSecClassKey,
String(kSecAttrApplicationTag): tag,
String(kSecValueData): key,
String(kSecReturnPersistentRef): true
]
var keyRef: AnyObject?
let status = SecItemAdd(attributes as CFDictionary, &keyRef)
if status != 0 {
return nil
}
return keyRef as! SecKey
}

private static func createKeyFromData(key: Data) -> SecKey? {
let attributes: [String: Any] = [
String(kSecAttrKeyType): kSecAttrKeyTypeRSA,
String(kSecAttrKeyClass): kSecAttrKeyClassPublic,
String(kSecAttrKeySizeInBits): key.count * 8
]
let key = SecKeyCreateWithData(key as CFData, attributes as CFDictionary, nil)
return key
}
}

private extension SecKey {
enum KeyType {
case rsa
var secAttrKeyTypeValue: CFString {
switch self {
case .rsa:
return kSecAttrKeyTypeRSA
}
}
}

func encrypt(algorithm: SecKeyAlgorithm, plaintext: Data) throws -> Data {
var error: Unmanaged<CFError>?
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
}
}
11 changes: 8 additions & 3 deletions Sources/Xenissuing/Xenissuing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ public final class Xenissuing: XenCrypt {
Initializes XenIssuing module.

- Parameters:
- xenditKey: The key provided by Xendit.
- xenditPublicKeyData: Public Key.
- xenditPublicKeyTag: Public Key Tag. If provided, it will try to check first keychain to get the key data.

- Returns: Main module.
*/
override public init(xenditKey: Data) {
super.init(xenditKey: xenditKey)
override public init(xenditPublicKeyData: Data, xenditPublicKeyTag: String? = nil) throws {
do {
try super.init(xenditPublicKeyData: xenditPublicKeyData, xenditPublicKeyTag: xenditPublicKeyTag)
} catch {
throw error
}
}
}
85 changes: 76 additions & 9 deletions Tests/XenissuingTests/XenCryptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,23 @@ struct TestEncryption: Codable, Hashable {
}

final class XenCryptTests: XCTestCase {
// 32 bytes key generated by Xendit
let xenKey = Data(base64Encoded: "9xoXc4FS2HUuu3nrVUXKnpOhsROomJXxQlVR17x9AuM=")
// Valid RSA Public Key
let validPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArY3DXFJ2M0EHbsD9r+2XgFVtpYEQR5bxnQZVHVxtVzQP8u2cv/1APs2cft+8E682wKGY7SFUEsFsoqxoak7qsfXYL/mOdvQe6XDyNC7N6oo9Zb8dUKtuy8qPb1bVeTbxAwDVUzIdJpiRVI69fAGCW7aF3jTAV7Q+Z5qUTaLUFyKvu3+j8u/A58Nw5fjOENTLHBZRrXhFtQC1eql2O6FiQRJBDACYtzhyFBMyT/B7SKNPkEvLm1w4AQEWxxwL93B8vxstfpatbJJvorJaDEl/glncxJVtZ0lBeB3dkWdro/TrhpPD7CHKlBIUKRfvq1TgmMFs9SP90DxD9l9mE+AUAwIDAQAB"

func testGenerateSessionId() {
let xcrypt = XenCrypt(xenditKey: xenKey!)
let privateKey = try! SecKey.createRandomKey(type: .rsa, bits: 2048)
let publicKey = try! privateKey.publicKey()
let publicKeyData = try! publicKey.externalRepresentation()
let xcrypt = try! XenCrypt(xenditPublicKeyData: publicKeyData)
let sessionKey = try! xcrypt.generateRandom()
let sessionId = xcrypt.generateSessionId(sessionKey: sessionKey)
let iv = sessionId.sealed[0 ... 15]
let aes = try! AES(key: xenKey!.bytes, blockMode: CBC(iv: iv.bytes), padding: .pkcs7)
let decryptedBytes = try! aes.decrypt(sessionId.sealed[16...].bytes)
let sessionId = try! xcrypt.generateSessionId(sessionKey: sessionKey)
let decryptedBytes = try! privateKey.decrypt(algorithm: .rsaEncryptionOAEPSHA256, ciphertext: sessionId.sealed)
let decryptedData = Data(decryptedBytes)
XCTAssertEqual(sessionKey.base64EncodedString(), decryptedData.base64EncodedString())
}

func testDecrypt() {
let xcrypt = XenCrypt(xenditKey: xenKey!)
let xcrypt = try! XenCrypt(xenditPublicKeyData: Data(base64Encoded: validPublicKey)!)
for t in tests {
let decrypted = try! xcrypt.decrypt(secret: t.secret, sessionKey: Data(base64Encoded: t.sessionKey)!, iv: t.iv)
XCTAssertEqual(String(decoding: decrypted, as: UTF8.self), t.plain)
Expand All @@ -45,5 +47,70 @@ let tests: [TestEncryption] = [
secret: "dmYJfv2Ou9fyz9WXe15DAeB9cg==",
iv: "fbgKT6+XbpLWPizRNRF5sw==",
plain: "372"
)
),
]

extension SecKey {
enum KeyType {
case rsa
case ellipticCurve
var secAttrKeyTypeValue: CFString {
switch self {
case .rsa:
return kSecAttrKeyTypeRSA
case .ellipticCurve:
return kSecAttrKeyTypeECSECPrimeRandom
}
}
}

/// Creates a random key.
static func createRandomKey(type: KeyType, bits: Int) throws -> SecKey {
var error: Unmanaged<CFError>?
let keyO = SecKeyCreateRandomKey([
kSecAttrKeyType: type.secAttrKeyTypeValue,
kSecAttrKeySizeInBits: NSNumber(integerLiteral: bits),
] as CFDictionary, &error)
// See here for apple's sample code for memory-managing returned errors
// from the Security framework:
// https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_as_data
if let error = error?.takeRetainedValue() { throw error }
guard let key = keyO else { throw TestsErrors.nilKey }
return key
}

/// Gets the public key from a key pair.
func publicKey() throws -> SecKey {
let publicKeyO = SecKeyCopyPublicKey(self)
guard let publicKey = publicKeyO else { throw TestsErrors.nilPublicKey }
return publicKey
}

/// Exports a key.
/// RSA keys are returned in PKCS #1 / DER / ASN.1 format.
/// EC keys are returned in ANSI X9.63 format.
func externalRepresentation() throws -> Data {
var error: Unmanaged<CFError>?
let dataO = SecKeyCopyExternalRepresentation(self, &error)
if let error = error?.takeRetainedValue() { throw error }
guard let data = dataO else { throw TestsErrors.nilExternalRepresentation }
return data as Data
}

func decrypt(algorithm: SecKeyAlgorithm, ciphertext: Data) throws -> Data {
var error: Unmanaged<CFError>?
let plaintextO = SecKeyCreateDecryptedData(self, algorithm,
ciphertext as CFData, &error)
if let error = error?.takeRetainedValue() { throw error }
guard let plaintext = plaintextO else { throw TestsErrors.nilPlaintext }
return plaintext as Data
}

enum TestsErrors: Error {
case nilKey
case nilPublicKey
case nilExternalRepresentation
case nilCiphertext
case nilPlaintext
}
}
5 changes: 3 additions & 2 deletions Tests/XenissuingTests/XenissuingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import XCTest
@testable import Xenissuing

final class XenissuingTests: XCTestCase {
func testExample() throws {
func testValidPubicKey() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertNotNil(Xenissuing(xenditKey: Data()))
let validPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArY3DXFJ2M0EHbsD9r+2XgFVtpYEQR5bxnQZVHVxtVzQP8u2cv/1APs2cft+8E682wKGY7SFUEsFsoqxoak7qsfXYL/mOdvQe6XDyNC7N6oo9Zb8dUKtuy8qPb1bVeTbxAwDVUzIdJpiRVI69fAGCW7aF3jTAV7Q+Z5qUTaLUFyKvu3+j8u/A58Nw5fjOENTLHBZRrXhFtQC1eql2O6FiQRJBDACYtzhyFBMyT/B7SKNPkEvLm1w4AQEWxxwL93B8vxstfpatbJJvorJaDEl/glncxJVtZ0lBeB3dkWdro/TrhpPD7CHKlBIUKRfvq1TgmMFs9SP90DxD9l9mE+AUAwIDAQAB"
XCTAssertNoThrow(try Xenissuing(xenditPublicKeyData: Data(base64Encoded: validPublicKey)!))
}
}

0 comments on commit bf74b53

Please sign in to comment.