Swift RSA Encryption: A Hands-On Guide for iOS Developers

Step-by-Step Implementation in Swift

icon
Adding a small illustration to each post seems like a really cute idea!

So, I recently ran into a situation at work where I needed to implement RSA encryption. While there are tons of great articles out there by seasoned pros, I wanted to share my experience as someone tackling this for the first time.

I’m gonna walk you through the stuff that tripped me up and the little nuggets of info that I found interesting.


The Mission

When the front-end calls the back-end API, we want to ensure that sensitive information (like personal data) remains secure during transmission. To do this, we can use RSA encryption to protect the data:

  • Front-End: Uses the public key (provided by the back-end) to encrypt the data.
  • Back-End: Uses the private key to decrypt the data.

But before diving into the code, let’s break down what RSA actually is.


What Is RSA?

RSA is a type of “asymmetric encryption.” This just means that it uses different keys for encryption and decryption.

A notable characteristic is that once data is encrypted with a public key, only the private key can decrypt it.

The biggest advantage of this approach is that even if the data is intercepted during transmission, no one can decrypt it without the private key.


While researching RSA, I kept bumping into another term, AES.
How do they relate, and what sets them apart?

AES vs. RSA?

In general, common encryption methods are divided into two categories:

AES Encryption

  • Symmetric encryption: Uses the same key for both encryption and decryption.
  • Fast and efficient: Great for encrypting large amounts of data.
  • Key management is crucial: If the key gets compromised, everything’s at risk.
  • iOS Implementation: You can use a library like CryptoSwift

RSA Encryption

  • Asymmetric encryption: Uses separate keys for encryption and decryption.
  • Slower than AES: Best for smaller chunks of data.
  • iOS Implementation: Check out SwiftyRSA

Of course, you can combine both methods to get the best of both worlds:

Hybrid Encryption

For large files, you can encrypt the data with AES and then encrypt the AES key with RSA. This gives you speed and strong security.


After reading up on RSA, my back-end colleague suddenly sent me a public key in PEM format. This got me curious — what’s PEM all about?

PEM (Privacy Enhanced Mail)

You might have seen a PEM file when dealing with public or private keys. It often looks like this:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1ymUuKnPRw99S1wMbEJt
nKv7lTOyYryYiXAlRjQznFYSaLVa8TACplEKEiLPO2A3aJG9tJf6ObMRiOhKnuGI
E9QJ+ByNOqJZM3mTHm/hAnX0N8d7WCozrJGyXlrb4I/JNkwTFx8DWggT+0rLPTCs
...
-----END PUBLIC KEY-----

This is a typical PEM file 👆🏻

PEM is a human-readable text-based format commonly used for storing and exchanging certificates, public keys, and private keys. It’s also easy to save and transfer in a plain text file.

Files with extensions like .pem.crt.cer might contain PEM data. If you see -----BEGIN ...-----, you know it’s a PEM file.


iOS in Practice

With the basics covered, it’s time to put it all together in iOS!

In this particular requirement, aside from encrypting the API path and parameters, we also need to preprocess them in a specific way before sending them off. This preprocessing ensures that the API path and parameter names are simplified, making it easier for the back-end to parse and decrypt.

Here are the specific rules:

Encrypt the API info and place it in the header for transmission.

Example: RSA(user/getUserInfo?param1=value&param2=value)

1. Convert the route to lowercase and remove any "-" characters
    a. Format: route1/route2
    b. Example: user/getUserInfo
    
2. Convert parameters to lowercase, remove any "-" characters, and join them with an "&"
    a. Format: ?param=value
    b. Example: ?id=bohsunhsu&password=abcde

1. Adding the Library

Make sure you’ve added the SwiftyRSA library to your project.

2. Getting the Public Key

In my case, the back-end provided the public key. For testing, you can generate RSA keys in PEM format using a tool like Devglan.

Important: For this example, I’m storing the keys as strings, but in a real-world app, you should store them securely in the Keychain.

private let testPublicKey = """
-----BEGIN PUBLIC KEY-----
XXXXXXXXXXXXXXXXXXXXXXXXXX
-----END PUBLIC KEY-----
"""

private let testPrivateKey = """
-----BEGIN RSA PRIVATE KEY-----
XXXXXXXXXXXXXXXXXXXXXXXXXX
-----END RSA PRIVATE KEY-----
"""

3. Encrypting Data with the Public Key

func encryptData(route: String, parameters: [String: String]) -> String {
    // 1. Convert route to lowercase and remove "-"
    let lowercasedRoute = route
        .lowercased()
        .replacingOccurrences(of: "-", with: "")

    // 2. Convert parameters to lowercase, remove "-"
    let paramString = parameters
        .map { "\($0.key)=\($0.value)" }
        .joined(separator: "&")
        .lowercased()
        .replacingOccurrences(of: "-", with: "")
    
    // Combine into one string for encryption
    let stringToEncrypt = "\(lowercasedRoute)?\(paramString)"
    do {
        // Parse the public key from the PEM string
        let publicKey = try PublicKey(pemEncoded: testPublicKey)
        // ClearMessage represents the unencrypted plain text
        let clear = try ClearMessage(string: stringToEncrypt, using: .utf8)
        // EncryptedMessage represents encrypted data
        // .PKCS1 is used for padding
        let encryptedMessage = try clear.encrypted(with: publicKey, padding: .PKCS1)
        return encryptedMessage.base64String
    } catch {
        return error.localizedDescription
    }
}

Why do we need padding?

RSA encryption requires the plaintext to be a certain length. Padding adds extra bytes to the plaintext to meet this requirement.

Common padding methods include PKCS#1 and OAEP.

PKCS#1 is more widely used but older, whereas OAEP is more secure. Make sure both ends support the same padding; if possible, choose OAEP.

4. Decrypting Data with the Private Key

Typically, we won’t need to do this on iOS (the back-end handles decryption). But you can decrypt just to verify that the process works correctly:

func decryptData(readyToDecodeString: String, environment: Environment) -> String {
    do {
        // Parse the private key from the PEM string
        let privateKey = try PrivateKey(pemEncoded: testPrivateKey)
        // The encrypted data is Base64-encoded, so decode it first
        let encrypted = try EncryptedMessage(base64Encoded: readyToDecodeString)
        // Remove padding to convert encrypted data back to clear text
        let clear = try encrypted.decrypted(with: privateKey, padding: .PKCS1)
        // Convert the decrypted ClearMessage to a UTF-8 String
        let string = try clear.string(encoding: .utf8)
        return string
    } catch {
        return error.localizedDescription
    }
}

And that’s it — mission accomplished! Congratulations!


Honestly, this article is just my personal notes. In this AI-driven world, finding a solution to a new requirement is not that hard. But I still find it important to break down each term, question everything, understand it fully, and internalize that knowledge.

If you have any thoughts, feel free to reach out!

References:  
https://ithelp.ithome.com.tw/articles/10250721
https://ithelp.ithome.com.tw/articles/10251744
https://blog.csdn.net/weixin_44259720/article/details/110947742
https://www.ssldragon.com/zh/blog/rsa-aes-encryption/