End-to-End Encryption [iOS|Android|RSA]

In this short tutorial, you will learn how to use RSA private-public key to achieve end-to-end encryption that compatible with both iOS and Android. This is a simple approach and not the best approach. (According to the internet, best approach is Hybrid encryption with RSA and AES).

This simple approach fulfilled using below;

  1. SwiftyRSA (iOS)
  2. KeyStore (Andoird)
  3. Crypto Cipher

Encryption on iOS

  • Add dependencies

Add this to your Podfile

pod 'SwiftyRSA'

run pod install

  • RSAKeyManager
import UIKit
import SwiftyRSA
class RSAKeyManager {
public static let KEY_SIZE = 2048
private var publicKey, privateKey: SecKey?

private let tagPrivate = "\(Bundle.main.bundleIdentifier).tagPrivate"
private let tagPublic = "\(Bundle.main.bundleIdentifier).tagPublic"

static let shared = RSAKeyManager()
let exportImportManager = CryptoExportImportManager()

public func getMyPublicKey() -> PublicKey? {
do {
if let pubKey = publicKey {
return try PublicKey(reference: pubKey)
} else {
if getKeysFromKeychain(), let pubKey = publicKey {
return try PublicKey(reference: pubKey)
} else {
generateKeyPair()
if let pubKey = publicKey {
return try PublicKey(reference: pubKey)
}
}
}
} catch let error {
//Log Error
return nil
}
return nil
}

public func getMyPrivateKey() -> PrivateKey? {
do {
if let privKey = privateKey {
return try PrivateKey(reference: privKey)
} else {
if getKeysFromKeychain(), let privKey = privateKey {
return try PrivateKey(reference: privKey)
} else {
generateKeyPair()
if let privKey = privateKey {
return try PrivateKey(reference: privKey)
}
}
}
} catch let error {
//Log Error
return nil
}
return nil
}

public func getPublicKey(pemEncoded: String) -> PublicKey? {
do {
return try PublicKey(pemEncoded: pemEncoded)
} catch let error {
//Log Error
return nil
}
}

//Check Keychain and get keys
private func getKeysFromKeychain() -> Bool {
privateKey = getKeyTypeInKeyChain(tag: tagPrivate)
publicKey = getKeyTypeInKeyChain(tag: tagPublic)
return ((privateKey != nil)&&(publicKey != nil))
}

private func getKeyTypeInKeyChain(tag : String) -> SecKey? {
let query: [CFString: Any] = [
kSecClass: kSecClassKey,
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrApplicationTag: tag,
kSecReturnRef: true
]

var result : AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)

if status == errSecSuccess {
return result as! SecKey?
}
return nil
}

//Generate private and public keys
private func generateKeyPair() {
let privateKeyAttr: [CFString: Any] = [
kSecAttrIsPermanent: true,
kSecAttrApplicationTag: tagPrivate
]
let publicKeyAttr: [CFString: Any] = [
kSecAttrIsPermanent: true,
kSecAttrApplicationTag: tagPublic
]

let parameters: [CFString: Any] = [
kSecAttrKeyType: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits: RSAKeyManager.KEY_SIZE,
kSecPrivateKeyAttrs: privateKeyAttr,
kSecPublicKeyAttrs: publicKeyAttr
]

let status = SecKeyGeneratePair(parameters as CFDictionary, &publicKey, &privateKey)
self.updatePublicKey()

if status != noErr {
//Log Error
return
}
}
public func getMyPublicKeyString() : String? {
guard let pubKey = self.getMyPublicKey() else {
return
}
return exportImportManager.exportRSAPublicKeyToPEM(try pubKey.data(), keyType: kSecAttrKeyTypeRSA as String, keySize: RSAKeyManager.KEY_SIZE)
}

//Delete keys when required.
public func deleteAllKeysInKeyChain() {
let query : [CFString: Any] = [
kSecClass: kSecClassKey
]
let status = SecItemDelete(query as CFDictionary)

switch status {
case errSecItemNotFound:
//No key in keychain
case noErr:
//All Keys Deleted
default:
//Log Error
}
}
}
  • CryptoExportImportManager

You have to add this file to your project

  • Usage
//To get public key.
let myPublicKey = RSAKeyManager.shared.getMyPublicKey()
//To get public key string that can share with others.
let myPublicKeyString =RSAKeyManager.shared.getMyPublicKeyString()
//To get PublicKey from string that shared by others
let otherPublicKeyString: String
let otherPublicKey = RSAKeyManager.shared.getMyPublicKeyString(pemEncoded: otherPublicKeyString)
//Encrypt Message (Using other's public key)
do {
let message: String = "Hi, Ross"
let clear = try ClearMessage(string: message, using: .utf8)
let encryptedMessage = try clear!.encrypted(with: otherPublicKey, padding: .PKCS1)
let encryptedMessageString = encryptedMessage.base64String
} catch let error {
//Log error
}
//Decrypt Message (Which encrypted using my public key)
do {
let encryptedMessageString: String
let myPrivateKey = RSAKeyManager.shared.getMyPrivateKey()
let encrypted = try EncryptedMessage(base64Encoded: encryptedMessageString)
let clear = try encrypted.decrypted(with: myPrivateKey, padding: .PKCS1)
let string = try clear.string(encoding: .utf8)
} catch let error {
//Log error
}

Encryption on Android

  • EncryptionManager
import android.content.Context
import android.os.Build
import android.util.Base64
import androidx.annotation.RequiresApi
import java.security.KeyFactory
import java.security.PublicKey
import java.security.spec.X509EncodedKeySpec

class EncryptionManager(context: Context) {

companion object {
const val MASTER_KEY = "master_key"
const val KEY_PAIR_ALGORITHM = "RSA"
const val KEY_SIZE: Int = 2048
const val KEY_PROVIDER = "AndroidKeyStore"
const val TRANSFORMATION_ASYMMETRIC = "RSA/None/PKCS1Padding"
}

private val keyStoreWrapper = KeyStoreWrapper(context)

/*
* Encryption Stage
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun createMasterKey() {
if (keyStoreWrapper.getAndroidKeyStoreAsymmetricKeyPair(MASTER_KEY) == null) {
keyStoreWrapper.createAndroidKeyStoreAsymmetricKey(MASTER_KEY)
}
}

fun removeMasterKey() {
keyStoreWrapper.removeAndroidKeyStoreKey(MASTER_KEY)
}

fun encrypt(data: String): String? {
val masterKey = keyStoreWrapper.getAndroidKeyStoreAsymmetricKeyPair(MASTER_KEY)
return CipherWrapper(TRANSFORMATION_ASYMMETRIC).encrypt(data, masterKey?.public)
}

fun encrypt(data: ByteArray): ByteArray? {
val masterKey = keyStoreWrapper.getAndroidKeyStoreAsymmetricKeyPair(MASTER_KEY)
return CipherWrapper(TRANSFORMATION_ASYMMETRIC).encrypt(data, masterKey?.public)
}

fun encryptOthers(data: String, key: PublicKey): String? {
return CipherWrapper(TRANSFORMATION_ASYMMETRIC).encrypt(data, key)
}

fun encryptOthers(data: ByteArray, key: PublicKey): ByteArray? {
return CipherWrapper(TRANSFORMATION_ASYMMETRIC).encrypt(data, key)
}

fun decrypt(data: String): String? {
val masterKey = keyStoreWrapper.getAndroidKeyStoreAsymmetricKeyPair(MASTER_KEY)
return CipherWrapper(TRANSFORMATION_ASYMMETRIC).decrypt(data, masterKey?.private)
}

fun decrypt(data: ByteArray): ByteArray? {
val masterKey = keyStoreWrapper.getAndroidKeyStoreAsymmetricKeyPair(MASTER_KEY)
return CipherWrapper(TRANSFORMATION_ASYMMETRIC).decrypt(data, masterKey?.private)
}

/*
* Manage Keys
*/
fun getMyPublicKey(): PublicKey? {
val masterKey = keyStoreWrapper.getAndroidKeyStoreAsymmetricKeyPair(MASTER_KEY)
return masterKey?.public
}

fun getOtherPublicKey(key: String): PublicKey? {
return try {
val publicBytes: ByteArray =
Base64.decode(key, Base64.DEFAULT)
val keySpec = X509EncodedKeySpec(publicBytes)
val keyFactory = KeyFactory.getInstance(KEY_PAIR_ALGORITHM)
keyFactory.generatePublic(keySpec)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
  • CipherWrapper
import android.util.Base64
import android.util.Log
import java.io.ByteArrayOutputStream
import java.security.Key
import javax.crypto.Cipher

class CipherWrapper(private val transformation: String) {
fun encrypt(message: String, key: Key?): String? {
try {
val cipher: Cipher = Cipher.getInstance(transformation)
cipher.init(Cipher.ENCRYPT_MODE, key)

val messageData = message.toByteArray(Charsets.UTF_8)
var limit: Int = (key?.encoded?.size)?.minus(62) ?: 128

var position = 0
val byteArrayOutputStream = ByteArrayOutputStream()

while (position < messageData.size) {
if (messageData.size - position < limit)
limit = messageData.size - position
val data = cipher.doFinal(messageData, position, limit)
byteArrayOutputStream.write(data)
position += limit
}
val enc = Base64.encodeToString(byteArrayOutputStream.toByteArray(), Base64.NO_WRAP)
byteArrayOutputStream.flush()
byteArrayOutputStream.close()
return enc
} catch (e: Exception) {
Log.e("CharWrapper", e.localizedMessage?: "StringEncrypt")
return null
}
}

fun encrypt(messageData: ByteArray, key: Key?): ByteArray? {
try {
val cipher: Cipher = Cipher.getInstance(transformation)
cipher.init(Cipher.ENCRYPT_MODE, key)

var limit: Int = (key?.encoded?.size)?.minus(62) ?: 128
var position = 0
val byteArrayOutputStream = ByteArrayOutputStream()

while (position < messageData.size) {
if (messageData.size - position < limit)
limit = messageData.size - position
val data = cipher.doFinal(messageData, position, limit)
byteArrayOutputStream.write(data)
position += limit
}
val enc = byteArrayOutputStream.toByteArray()
byteArrayOutputStream.flush()
byteArrayOutputStream.close()
return enc
} catch (e: Exception) {
Log.e("CharWrapper", e.localizedMessage?: "ByteEncrypt")
return null
}
}

fun decrypt(message: String, key: Key?): String? {
try {
val cipher: Cipher = Cipher.getInstance(transformation)
cipher.init(Cipher.DECRYPT_MODE, key)
val encryptedData = Base64.decode(message.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)

var limit: Int = EncryptionManager.KEY_SIZE / 8
var position = 0
val byteArrayOutputStream = ByteArrayOutputStream()
while (position < encryptedData.size) {
if (encryptedData.size - position < limit)
limit = encryptedData.size - position
val data = cipher.doFinal(encryptedData, position, limit)
byteArrayOutputStream.write(data)
position += limit
}
val dec = byteArrayOutputStream.toString(Charsets.UTF_8.name())
byteArrayOutputStream.flush()
byteArrayOutputStream.close()
return dec
} catch (e: Exception) {
Log.e("CharWrapper", e.localizedMessage?: "StringDecrypt")
return null
}
}

fun decrypt(data: ByteArray, key: Key?): ByteArray? {
try {
val cipher: Cipher = Cipher.getInstance(transformation)
cipher.init(Cipher.DECRYPT_MODE, key)

var limit: Int = EncryptionManager.KEY_SIZE / 8
var position = 0
val byteArrayOutputStream = ByteArrayOutputStream()
while (position < data.size) {
if (data.size - position < limit)
limit = data.size - position
val data = cipher.doFinal(data, position, limit)
byteArrayOutputStream.write(data)
position += limit
}
val dec = byteArrayOutputStream.toByteArray()
byteArrayOutputStream.flush()
byteArrayOutputStream.close()
return dec
} catch (e: Exception) {
Log.e("CharWrapper", e.localizedMessage?: "ByteDecrypt")
return null
}
}
}
  • KeyStoreWrapper
import android.annotation.TargetApi
import android.content.Context
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import androidx.annotation.RequiresApi
import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.PrivateKey
import javax.security.auth.x500.X500Principal

class KeyStoreWrapper(private val context: Context) {

private val keyStore: KeyStore = createAndroidKeyStore()

fun getAndroidKeyStoreAsymmetricKeyPair(alias: String): KeyPair? {
val privateKey = keyStore.getKey(alias, null) as PrivateKey?
val publicKey = keyStore.getCertificate(alias)?.publicKey

return if (privateKey != null && publicKey != null) {
KeyPair(publicKey, privateKey)
} else {
null
}
}

@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun createAndroidKeyStoreAsymmetricKey(alias: String): KeyPair {
val generator = KeyPairGenerator.getInstance(
EncryptionManager.KEY_PAIR_ALGORITHM,
EncryptionManager.KEY_PROVIDER
)

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
initGeneratorWithKeyPairGeneratorSpec(generator, alias)
} else {
initGeneratorWithKeyGenParameterSpec(generator, alias)
}

val keyPair = generator.generateKeyPair()
val pubKeyStr = String(Base64.encode(keyPair.public?.encoded, Base64.DEFAULT))
return keyPair
}

fun removeAndroidKeyStoreKey(alias: String) = keyStore.deleteEntry(alias)

@Suppress("DEPRECATION")
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private fun initGeneratorWithKeyPairGeneratorSpec(generator: KeyPairGenerator, alias: String) {
val builder = KeyPairGeneratorSpec.Builder(context)
.setAlias(alias)
.setSerialNumber(BigInteger.ONE)
.setSubject(X500Principal("CN=${alias} CA Certificate"))

generator.initialize(builder.build())
}

@TargetApi(Build.VERSION_CODES.M)
private fun initGeneratorWithKeyGenParameterSpec(generator: KeyPairGenerator, alias: String) {
val builder = KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.setDigests(KeyProperties.DIGEST_SHA256)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.setKeySize(2048)
generator.initialize(builder.build())
}

private fun createAndroidKeyStore(): KeyStore {
val keyStore = KeyStore.getInstance(EncryptionManager.KEY_PROVIDER)
keyStore.load(null)
return keyStore
}

}
  • Usage
//Init
val encService = EncryptionManager(this.applicationContext)
encService.createMasterKey()
//To get public key.
val myPublicKey = encService.getMyPublicKey()
//To get public key string that can share with others.
val myPublicKeyString = String(Base64.encode(myPublicKey.encoded, Base64.DEFAULT))
//To get PublicKey from string that shared by others
val othersPublicKeyString: String
val othersPublicKey = encService.getOtherPublicKey(othersPublicKeyString)
//Encrypt Message (Using other's public key)
val message: String = "Hi, Ross"
val otherEnc = encService.encryptOthers(message, othersPublicKey)
//Decrypt Message (Which encrypted using my public key)
val encryptedString: String
val decryptedString = encService.decrypt(encryptedString)

From this post, we’ve learned how use RSA E2E Encryption that compatible with both iOS and Android

Thank you for reading.

Software Engineer