Skip to content

Commit 0ab293f

Browse files
committed
Add iOS encryption code
1 parent b8136e7 commit 0ab293f

19 files changed

+499
-5386
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Build files
2+
/build
3+
/plugin/build
4+
5+
yarn.lock
6+
17
# OSX
28
#
39
.DS_Store

.npmignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
__mocks__
55
__tests__
66

7+
/yarn.lock
78
/babel.config.js
89
/android/src/androidTest/
910
/android/src/test/

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# ttmobile-notification-service
22

3-
Adds support for communication notifications
3+
This is a notification service that modifies incoming notifications.
4+
5+
Currently it support communication notifications for ios. Adding company logo as avatar.
6+
7+
It also supports e2e encryption for notifications-
48

59
# Installation in managed Expo projects
610

android/TTFirebaseMessagingService.java

-45
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
public class TTFirebaseMessagingService extends ExpoFirebaseMessagingService {
2626

2727
private static final String TAG = "TTFirebaseMessagingService";
28-
private static final String KEY_ALIAS = "my_private_key_alias"; // Replace with your key alias
29-
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
3028

3129
@Override
3230
public void onMessageReceived(RemoteMessage remoteMessage) {
@@ -38,9 +36,6 @@ public void onMessageReceived(RemoteMessage remoteMessage) {
3836
String decryptedDataString = originalData.get("encrypted_data");
3937

4038
try {
41-
// Decrypt the data
42-
// String decryptedDataString = decryptData(encryptedData);
43-
4439
// Parse the decrypted JSON string into a Map
4540
Map<String, String> decryptedData = parseJsonToMap(decryptedDataString);
4641

@@ -80,46 +75,6 @@ public void onMessageReceived(RemoteMessage remoteMessage) {
8075
super.onMessageReceived(remoteMessage);
8176
}
8277
}
83-
84-
// Method to decrypt the encrypted data string
85-
private String decryptData(String encryptedData) throws Exception {
86-
// Get the private key from the KeyStore
87-
PrivateKey privateKey = getPrivateKeyFromKeyStore();
88-
89-
if (privateKey == null) {
90-
throw new Exception("Private key not found in KeyStore");
91-
}
92-
93-
// Decrypt the data
94-
byte[] encryptedBytes = android.util.Base64.decode(encryptedData, android.util.Base64.DEFAULT);
95-
96-
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
97-
cipher.init(Cipher.DECRYPT_MODE, privateKey);
98-
99-
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
100-
101-
return new String(decryptedBytes, "UTF-8");
102-
}
103-
104-
// Method to get the private key from the Android KeyStore
105-
private PrivateKey getPrivateKeyFromKeyStore() throws Exception {
106-
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
107-
keyStore.load(null);
108-
109-
Entry entry = keyStore.getEntry(KEY_ALIAS, null);
110-
111-
if (entry == null) {
112-
Log.e(TAG, "No key found under alias: " + KEY_ALIAS);
113-
return null;
114-
}
115-
116-
if (!(entry instanceof PrivateKeyEntry)) {
117-
Log.e(TAG, "Key under alias " + KEY_ALIAS + " is not a private key");
118-
return null;
119-
}
120-
121-
return ((PrivateKeyEntry) entry).getPrivateKey();
122-
}
12378

12479
// Method to parse the decrypted JSON string into a Map<String, String>
12580
private Map<String, String> parseJsonToMap(String jsonString) throws JSONException {

expo-module.config.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "ttmobile-notification-service",
3+
"platforms": ["ios"],
4+
"ios": {
5+
"modules": ["EncryptionModule"]
6+
}
7+
}

ios/CryptoUtils.swift

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import Foundation
2+
import Security
3+
import CryptoKit
4+
5+
public final class CryptoUtils {
6+
private static let keyTag = "com.teamtailor.app.cryptoutils"
7+
private static let keychainGroup = "group.com.teamtailor.keys"
8+
9+
public enum CryptoError: Error {
10+
case keyGenerationFailed(String)
11+
case keychainAccessFailed(String)
12+
case publicKeyExtractionFailed
13+
case privateKeyNotFound
14+
case invalidBase64Input
15+
case rsaDecryptionFailed
16+
case invalidAESKey
17+
case aesDecryptionFailed
18+
case rsaEncryptionFailed
19+
}
20+
21+
private static func generateKeyPair(tag: Data) throws -> SecKey {
22+
guard let access = SecAccessControlCreateWithFlags(
23+
kCFAllocatorDefault,
24+
kSecAttrAccessibleAfterFirstUnlock, // This means that the key is only accessible after the device has been unlocked once. So if we get notifications before that, then we will show fallback gdpr compliant message. Any other option are more restrictive.
25+
[],
26+
nil
27+
) else {
28+
throw CryptoError.keyGenerationFailed("Failed to create access control")
29+
}
30+
31+
let attributes: [String: Any] = [
32+
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
33+
kSecAttrKeySizeInBits as String: 2048,
34+
kSecPrivateKeyAttrs as String: [
35+
kSecAttrIsPermanent as String: true,
36+
kSecAttrApplicationTag as String: tag,
37+
kSecAttrAccessGroup as String: keychainGroup,
38+
kSecAttrAccessControl as String: access
39+
]
40+
]
41+
42+
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, nil) else {
43+
throw CryptoError.keyGenerationFailed("Failed to generate key pair")
44+
}
45+
46+
return privateKey
47+
}
48+
49+
private static func getPrivateKey() throws -> SecKey {
50+
let tag = keyTag.data(using: .utf8)!
51+
let query: [String: Any] = [
52+
kSecClass as String: kSecClassKey,
53+
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
54+
kSecAttrApplicationTag as String: tag,
55+
kSecReturnRef as String: true,
56+
kSecAttrAccessGroup as String: keychainGroup
57+
]
58+
59+
var item: CFTypeRef?
60+
let status = SecItemCopyMatching(query as CFDictionary, &item)
61+
62+
if status == errSecSuccess {
63+
return item as! SecKey
64+
}
65+
66+
if status == errSecItemNotFound {
67+
return try generateKeyPair(tag: tag)
68+
}
69+
70+
let error = SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error"
71+
NSLog("CryptoUtils: Keychain access failed with error: \(error)")
72+
throw CryptoError.keychainAccessFailed(error)
73+
}
74+
75+
public static func getPublicKey() throws -> String {
76+
let privateKey = try getPrivateKey()
77+
78+
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
79+
throw CryptoError.publicKeyExtractionFailed
80+
}
81+
82+
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
83+
throw CryptoError.publicKeyExtractionFailed
84+
}
85+
86+
return publicKeyData.base64EncodedString()
87+
}
88+
89+
public static func rsaDecrypt(encryptedBase64: String) throws -> Data {
90+
guard let cipherData = Data(base64Encoded: encryptedBase64) else {
91+
throw CryptoError.invalidBase64Input
92+
}
93+
94+
let privateKey = try getPrivateKey()
95+
96+
var error: Unmanaged<CFError>?
97+
guard let decryptedData = SecKeyCreateDecryptedData(privateKey,
98+
.rsaEncryptionOAEPSHA1,
99+
cipherData as CFData,
100+
&error) as Data? else {
101+
if let error = error?.takeRetainedValue() {
102+
NSLog("RSA Decrypt: Failed with error: \(error)")
103+
}
104+
throw CryptoError.rsaDecryptionFailed
105+
}
106+
107+
return decryptedData
108+
}
109+
110+
public static func rsaEncrypt(data: Data) throws -> String {
111+
let privateKey = try getPrivateKey()
112+
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
113+
throw CryptoError.privateKeyNotFound
114+
}
115+
116+
var error: Unmanaged<CFError>?
117+
guard let encryptedData = SecKeyCreateEncryptedData(
118+
publicKey,
119+
.rsaEncryptionOAEPSHA1,
120+
data as CFData,
121+
&error
122+
) as Data? else {
123+
if let error = error?.takeRetainedValue() {
124+
NSLog("RSA Encrypt: Failed with error: \(error)")
125+
}
126+
throw CryptoError.rsaEncryptionFailed
127+
}
128+
129+
return encryptedData.base64EncodedString()
130+
}
131+
132+
public static func hybridDecrypt(
133+
encryptedKey: String,
134+
cipherText: String,
135+
nonce: String,
136+
tag: String
137+
) throws -> String {
138+
let aesKeyData = try rsaDecrypt(encryptedBase64: encryptedKey)
139+
140+
guard let cipherData = Data(base64Encoded: cipherText),
141+
let nonceData = Data(base64Encoded: nonce),
142+
let tagData = Data(base64Encoded: tag) else {
143+
throw CryptoError.invalidBase64Input
144+
}
145+
146+
guard let aeadNonce = try? AES.GCM.Nonce(data: nonceData) else {
147+
throw CryptoError.invalidBase64Input
148+
}
149+
150+
let symmetricKey = SymmetricKey(data: aesKeyData)
151+
do {
152+
let sealedBox = try AES.GCM.SealedBox(
153+
nonce: aeadNonce,
154+
ciphertext: cipherData,
155+
tag: tagData
156+
)
157+
158+
let decryptedData = try AES.GCM.open(sealedBox, using: symmetricKey)
159+
160+
guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
161+
throw CryptoError.aesDecryptionFailed
162+
}
163+
164+
return decryptedString
165+
166+
} catch {
167+
NSLog("AES Decryption failed: \(error)")
168+
throw CryptoError.aesDecryptionFailed
169+
}
170+
}
171+
172+
public static func deleteKeyPair() {
173+
let tag = keyTag.data(using: .utf8)!
174+
let query: [String: Any] = [
175+
kSecClass as String: kSecClassKey,
176+
kSecAttrApplicationTag as String: tag,
177+
kSecAttrAccessGroup as String: keychainGroup
178+
]
179+
SecItemDelete(query as CFDictionary)
180+
}
181+
182+
public static func testEncryption(message: String) throws -> Bool {
183+
guard let messageData = message.data(using: .utf8) else {
184+
throw CryptoError.invalidBase64Input
185+
}
186+
187+
let encrypted = try rsaEncrypt(data: messageData)
188+
let decryptedData = try rsaDecrypt(encryptedBase64: encrypted)
189+
190+
guard let decryptedMessage = String(data: decryptedData, encoding: .utf8) else {
191+
throw CryptoError.rsaDecryptionFailed
192+
}
193+
194+
return message == decryptedMessage
195+
}
196+
}

ios/EncryptionModule.entitlements

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>keychain-access-groups</key>
6+
<array>
7+
<string>$(AppIdentifierPrefix)group.com.teamtailor.keys</string>
8+
</array>
9+
</dict>
10+
</plist>

ios/EncryptionModule.podspec

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
require 'json'
2+
3+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4+
5+
Pod::Spec.new do |s|
6+
s.name = 'EncryptionModule'
7+
s.version = package['version']
8+
s.summary = package['description']
9+
s.license = package['license']
10+
s.author = package['author']
11+
s.homepage = package['homepage']
12+
s.platform = :ios, '13.4'
13+
s.source = { git: package['repository'], tag: "v#{s.version}" }
14+
s.source_files = 'EncryptionModule.swift', 'CryptoUtils.swift'
15+
s.dependency 'ExpoModulesCore'
16+
17+
# Ensure we can use keychain
18+
s.frameworks = 'Security'
19+
20+
# Add keychain sharing entitlements
21+
s.pod_target_xcconfig = {
22+
'CODE_SIGN_ENTITLEMENTS' => 'EncryptionModule.entitlements',
23+
'OTHER_CODE_SIGN_FLAGS' => '--entitlements $(PODS_TARGET_SRCROOT)/EncryptionModule.entitlements'
24+
}
25+
end

0 commit comments

Comments
 (0)