mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 14:48:41 +00:00
ios push notification should work now #53
This commit is contained in:
parent
4af2ef8260
commit
1962c70431
10 changed files with 376 additions and 176 deletions
10
ios/NotificationService/NotificationService.entitlements
Normal file
10
ios/NotificationService/NotificationService.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)eu.twonly.shared</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -18,64 +18,20 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if let bestAttemptContent = bestAttemptContent {
|
if let bestAttemptContent = bestAttemptContent {
|
||||||
|
|
||||||
|
|
||||||
// Extract the ciphertext and nonce from the notification's userInfo
|
|
||||||
guard let _userInfo = bestAttemptContent.userInfo as? [String: Any],
|
guard let _userInfo = bestAttemptContent.userInfo as? [String: Any],
|
||||||
let ciphertextString = bestAttemptContent.userInfo["ciphertext"] as? String,
|
let push_data = bestAttemptContent.userInfo["push_data"] as? String else {
|
||||||
let nonceString = bestAttemptContent.userInfo["nonce"] as? String else {
|
return contentHandler(bestAttemptContent);
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the base64 encoded strings to Data
|
let data = getPushNotificationData(pushDataJson: push_data)
|
||||||
guard let ciphertextData = Data(base64Encoded: ciphertextString),
|
|
||||||
let nonceData = Data(base64Encoded: nonceString) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the key (32 bytes of "A")
|
if data != nil {
|
||||||
let keyString = String(repeating: "A", count: 32)
|
bestAttemptContent.title = data!.title;
|
||||||
guard let keyData = keyString.data(using: .utf8) else {
|
bestAttemptContent.body = data!.body;
|
||||||
return
|
} else {
|
||||||
}
|
bestAttemptContent.title = "\(bestAttemptContent.title) [failed to decrypt]"
|
||||||
|
|
||||||
// Ensure the key is 32 bytes
|
|
||||||
guard keyData.count == 32 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the ciphertext is more than 16 Bytes
|
|
||||||
guard ciphertextData.count >= 16 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split the ciphertextData into the actual ciphertext and the tag
|
|
||||||
let tagLength = 16
|
|
||||||
let ciphertext = ciphertextData.prefix(ciphertextData.count - tagLength)
|
|
||||||
let tag = ciphertextData.suffix(tagLength)
|
|
||||||
|
|
||||||
// Create a SymmetricKey from the key data
|
|
||||||
let key = SymmetricKey(data: keyData)
|
|
||||||
|
|
||||||
// Decrypt the ciphertext using ChaCha20
|
|
||||||
do {
|
|
||||||
let nonce = try ChaChaPoly.Nonce(data: nonceData)
|
|
||||||
let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: Data(tag))
|
|
||||||
let decryptedData = try ChaChaPoly.open(sealedBox, using: key)
|
|
||||||
|
|
||||||
// Convert decrypted data to a string
|
|
||||||
if let decryptedMessage = String(data: decryptedData, encoding: .utf8) {
|
|
||||||
NSLog("Decrypted message: \(decryptedMessage)")
|
|
||||||
|
|
||||||
bestAttemptContent.title = "\(bestAttemptContent.title)"
|
|
||||||
bestAttemptContent.body = decryptedMessage
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
NSLog("Decryption failed: \(error)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contentHandler(bestAttemptContent)
|
contentHandler(bestAttemptContent)
|
||||||
|
|
@ -91,3 +47,288 @@ class NotificationService: UNNotificationServiceExtension {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum PushKind: String, Codable {
|
||||||
|
case text
|
||||||
|
case twonly
|
||||||
|
case video
|
||||||
|
case image
|
||||||
|
case contactRequest
|
||||||
|
case acceptRequest
|
||||||
|
case storedMediaFile
|
||||||
|
case reaction
|
||||||
|
case testNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
func getPushNotificationData(pushDataJson: String) -> (title: String, body: String)? {
|
||||||
|
// Decode the pushDataJson
|
||||||
|
guard let pushData = decodePushData(pushDataJson) else {
|
||||||
|
NSLog("Failed to decode push data")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pushKind: PushKind?
|
||||||
|
var displayName: String?
|
||||||
|
|
||||||
|
// Check the keyId
|
||||||
|
if pushData.keyId == 0 {
|
||||||
|
let key = "InsecureOnlyUsedForAddingContact".data(using: .utf8)!.map { Int($0) }
|
||||||
|
pushKind = tryDecryptMessage(key: key, pushData: pushData)
|
||||||
|
} else {
|
||||||
|
let pushKeys = getPushKey()
|
||||||
|
if pushKeys != nil {
|
||||||
|
for (userId, userKeys) in pushKeys! {
|
||||||
|
for key in userKeys.keys {
|
||||||
|
if key.id == pushData.keyId {
|
||||||
|
pushKind = tryDecryptMessage(key: key.key, pushData: pushData)
|
||||||
|
if pushKind != nil {
|
||||||
|
displayName = userKeys.displayName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Found correct key and user
|
||||||
|
if displayName != nil { break }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NSLog("pushKeys are empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the push notification based on the pushKind
|
||||||
|
if let pushKind = pushKind {
|
||||||
|
|
||||||
|
let bestAttemptContent = UNMutableNotificationContent()
|
||||||
|
|
||||||
|
if pushKind == .testNotification {
|
||||||
|
return ("Test Notification", "This is a test notification.")
|
||||||
|
} else if displayName != nil {
|
||||||
|
return (displayName!, getPushNotificationText(pushKind: pushKind))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
NSLog("Failed to decrypt message or pushKind is nil")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryDecryptMessage(key: [Int], pushData: PushNotification) -> PushKind? {
|
||||||
|
// Convert the key from [Int] to Data
|
||||||
|
let keyData = Data(key.map { UInt8($0) }) // Convert Int to UInt8
|
||||||
|
|
||||||
|
guard let nonceData = Data(base64Encoded: pushData.nonce),
|
||||||
|
let cipherTextData = Data(base64Encoded: pushData.cipherText),
|
||||||
|
let macData = Data(base64Encoded: pushData.mac) else {
|
||||||
|
NSLog("Failed to decode base64 strings")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Create a nonce for ChaChaPoly
|
||||||
|
let nonce = try ChaChaPoly.Nonce(data: nonceData)
|
||||||
|
|
||||||
|
// Create a sealed box for ChaChaPoly
|
||||||
|
let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: cipherTextData, tag: macData)
|
||||||
|
|
||||||
|
// Decrypt the data using the key
|
||||||
|
let decryptedData = try ChaChaPoly.open(sealedBox, using: SymmetricKey(data: keyData))
|
||||||
|
|
||||||
|
// Convert decrypted data to a string
|
||||||
|
if let decryptedMessage = String(data: decryptedData, encoding: .utf8) {
|
||||||
|
NSLog("Decrypted message: \(decryptedMessage)")
|
||||||
|
|
||||||
|
// Here you can determine the PushKind based on the decrypted message
|
||||||
|
return determinePushKind(from: decryptedMessage)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
NSLog("Decryption failed: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder function to determine PushKind from the decrypted message
|
||||||
|
func determinePushKind(from message: String) -> PushKind? {
|
||||||
|
// Implement your logic to determine the PushKind based on the message content
|
||||||
|
// For example, you might check for specific keywords or formats in the message
|
||||||
|
// This is just a placeholder implementation
|
||||||
|
if message.contains("text") {
|
||||||
|
return .text
|
||||||
|
} else if message.contains("video") {
|
||||||
|
return .video
|
||||||
|
} else if message.contains("image") {
|
||||||
|
return .image
|
||||||
|
} else if message.contains("twonly") {
|
||||||
|
return .twonly
|
||||||
|
} else if message.contains("contactRequest") {
|
||||||
|
return .contactRequest
|
||||||
|
} else if message.contains("acceptRequest") {
|
||||||
|
return .acceptRequest
|
||||||
|
} else if message.contains("storedMediaFile") {
|
||||||
|
return .storedMediaFile
|
||||||
|
} else if message.contains("reaction") {
|
||||||
|
return .reaction
|
||||||
|
} else if message.contains("testNotification") {
|
||||||
|
return .testNotification
|
||||||
|
} else {
|
||||||
|
return nil // Unknown PushKind
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func decodePushData(_ json: String) -> PushNotification? {
|
||||||
|
// First, decode the base64 string
|
||||||
|
guard let base64Data = Data(base64Encoded: json) else {
|
||||||
|
NSLog("Failed to decode base64 string")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the base64 decoded data to a JSON string
|
||||||
|
guard let jsonString = String(data: base64Data, encoding: .utf8) else {
|
||||||
|
NSLog("Failed to convert base64 data to JSON string")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the JSON string to Data
|
||||||
|
guard let jsonData = jsonString.data(using: .utf8) else {
|
||||||
|
NSLog("Failed to convert JSON string to Data")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Use JSONDecoder to decode the JSON data into a PushNotification instance
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let pushNotification = try decoder.decode(PushNotification.self, from: jsonData)
|
||||||
|
return pushNotification
|
||||||
|
} catch {
|
||||||
|
NSLog("Error decoding JSON: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PushNotification: Codable {
|
||||||
|
let keyId: Int
|
||||||
|
let nonce: String
|
||||||
|
let cipherText: String
|
||||||
|
let mac: String
|
||||||
|
|
||||||
|
// You can add custom coding keys if the JSON keys differ from the property names
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case keyId
|
||||||
|
case nonce
|
||||||
|
case cipherText
|
||||||
|
case mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PushKeyMeta: Codable {
|
||||||
|
let id: Int
|
||||||
|
let key: [Int]
|
||||||
|
let createdAt: Date
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case key
|
||||||
|
case createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PushUser: Codable {
|
||||||
|
let displayName: String
|
||||||
|
let keys: [PushKeyMeta]
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case displayName
|
||||||
|
case keys
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPushKey() -> [Int: PushUser]? {
|
||||||
|
// Retrieve the data from secure storage (Keychain)
|
||||||
|
guard let data = readFromKeychain(key: "receivingPushKeys") else {
|
||||||
|
print("No data found for key: receivingPushKeys")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Decode the JSON data into a dictionary
|
||||||
|
let jsonData = data.data(using: .utf8)!
|
||||||
|
let jsonMap = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any]
|
||||||
|
|
||||||
|
var pushKeys: [Int: PushUser] = [:]
|
||||||
|
|
||||||
|
// Iterate through the JSON map and decode each PushUser
|
||||||
|
for (key, value) in jsonMap ?? [:] {
|
||||||
|
if let userData = try? JSONSerialization.data(withJSONObject: value, options: []),
|
||||||
|
let pushUser = try? JSONDecoder().decode(PushUser.self, from: userData) {
|
||||||
|
pushKeys[Int(key)!] = pushUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushKeys
|
||||||
|
} catch {
|
||||||
|
print("Error decoding JSON: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to read from Keychain
|
||||||
|
func readFromKeychain(key: String) -> String? {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String: key,
|
||||||
|
kSecReturnData as String: kCFBooleanTrue!,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||||
|
kSecAttrAccessGroup as String: "CN332ZUGRP.eu.twonly.shared" // Use your access group
|
||||||
|
]
|
||||||
|
|
||||||
|
var dataTypeRef: AnyObject? = nil
|
||||||
|
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
||||||
|
|
||||||
|
if status == errSecSuccess {
|
||||||
|
if let data = dataTypeRef as? Data {
|
||||||
|
return String(data: data, encoding: .utf8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPushNotificationText(pushKind: PushKind) -> String {
|
||||||
|
let systemLanguage = Locale.current.languageCode ?? "en" // Get the current system language
|
||||||
|
|
||||||
|
var pushNotificationText: [PushKind: String] = [:]
|
||||||
|
|
||||||
|
// Define the messages based on the system language
|
||||||
|
if systemLanguage.contains("de") { // German
|
||||||
|
pushNotificationText = [
|
||||||
|
.text: "hat dir eine Nachricht gesendet.",
|
||||||
|
.twonly: "hat dir ein twonly gesendet.",
|
||||||
|
.video: "hat dir ein Video gesendet.",
|
||||||
|
.image: "hat dir ein Bild gesendet.",
|
||||||
|
.contactRequest: "möchte sich mit dir vernetzen.",
|
||||||
|
.acceptRequest: "ist jetzt mit dir vernetzt.",
|
||||||
|
.storedMediaFile: "hat dein Bild gespeichert.",
|
||||||
|
.reaction: "hat auf dein Bild reagiert."
|
||||||
|
]
|
||||||
|
} else { // Default to English
|
||||||
|
pushNotificationText = [
|
||||||
|
.text: "has sent you a message.",
|
||||||
|
.twonly: "has sent you a twonly.",
|
||||||
|
.video: "has sent you a video.",
|
||||||
|
.image: "has sent you an image.",
|
||||||
|
.contactRequest: "wants to connect with you.",
|
||||||
|
.acceptRequest: "is now connected with you.",
|
||||||
|
.storedMediaFile: "has stored your image.",
|
||||||
|
.reaction: "has reacted to your image."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the corresponding message or an empty string if not found
|
||||||
|
return pushNotificationText[pushKind] ?? ""
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 54;
|
objectVersion = 77;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
|
@ -878,6 +878,7 @@
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = CN332ZUGRP;
|
DEVELOPMENT_TEAM = CN332ZUGRP;
|
||||||
|
|
@ -918,6 +919,7 @@
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = CN332ZUGRP;
|
DEVELOPMENT_TEAM = CN332ZUGRP;
|
||||||
|
|
@ -955,6 +957,7 @@
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = CN332ZUGRP;
|
DEVELOPMENT_TEAM = CN332ZUGRP;
|
||||||
|
|
|
||||||
|
|
@ -33,86 +33,24 @@ import Foundation
|
||||||
|
|
||||||
override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
override func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||||
NSLog("userNotificationCenter:willPresent")
|
NSLog("userNotificationCenter:willPresent")
|
||||||
|
|
||||||
|
/*
|
||||||
|
debugging NotificationService
|
||||||
|
let pushKeys = getPushKey();
|
||||||
|
print(pushKeys)
|
||||||
|
|
||||||
|
let bestAttemptContent = notification.request.content
|
||||||
|
|
||||||
|
guard let _userInfo = bestAttemptContent.userInfo as? [String: Any],
|
||||||
|
let push_data = bestAttemptContent.userInfo["push_data"] as? String else {
|
||||||
|
return completionHandler([.alert, .sound])
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = getPushNotificationData(pushDataJson: push_data)
|
||||||
|
print(data)
|
||||||
|
*/
|
||||||
|
|
||||||
completionHandler([.alert, .sound])
|
completionHandler([.alert, .sound])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
import Security
|
|
||||||
import CommonCrypto
|
|
||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
class KeychainHelper {
|
|
||||||
static func save(key: String, data: Data) -> OSStatus {
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrAccount as String: key,
|
|
||||||
kSecValueData as String: data
|
|
||||||
]
|
|
||||||
SecItemDelete(query as CFDictionary) // Delete any existing item
|
|
||||||
return SecItemAdd(query as CFDictionary, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func load(key: String) -> Data? {
|
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrAccount as String: key,
|
|
||||||
kSecReturnData as String: kCFBooleanTrue!,
|
|
||||||
kSecMatchLimit as String: kSecMatchLimitOne
|
|
||||||
]
|
|
||||||
|
|
||||||
var dataTypeRef: AnyObject? = nil
|
|
||||||
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
|
||||||
|
|
||||||
if status == errSecSuccess {
|
|
||||||
return dataTypeRef as? Data
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to decrypt data
|
|
||||||
func decrypt(data: Data, key: SymmetricKey) -> Data? {
|
|
||||||
do {
|
|
||||||
// Extract nonce, ciphertext, and tag
|
|
||||||
let nonceData = data.prefix(12) // ChaCha20-Poly1305 nonce is 12 bytes
|
|
||||||
let ciphertext = data.dropFirst(12).dropLast(16) // Exclude nonce and tag
|
|
||||||
let tag = data.suffix(16) // Last 16 bytes are the tag
|
|
||||||
|
|
||||||
// Create a nonce from the extracted nonce data
|
|
||||||
let nonce = try ChaChaPoly.Nonce(data: nonceData)
|
|
||||||
|
|
||||||
// Create a sealed box with the ciphertext and tag
|
|
||||||
let sealedBox = try ChaChaPoly.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
|
|
||||||
|
|
||||||
// Decrypt the data
|
|
||||||
let decryptedData = try ChaChaPoly.open(sealedBox, using: key)
|
|
||||||
return decryptedData
|
|
||||||
} catch {
|
|
||||||
print("Decryption error: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func handleReceivedMessage(encryptedMessage: Data, keyID: String) {
|
|
||||||
// Load the AES key from Keychain
|
|
||||||
guard let keyData = KeychainHelper.load(key: keyID) else {
|
|
||||||
print("Key not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = SymmetricKey(data: keyData);
|
|
||||||
|
|
||||||
// Decrypt the message
|
|
||||||
if let decryptedData = decrypt(data: encryptedMessage, key: key) {
|
|
||||||
let decryptedMessage = String(data: decryptedData, encoding: .utf8)
|
|
||||||
print("Decrypted message: \(decryptedMessage ?? "nil")")
|
|
||||||
} else {
|
|
||||||
print("Decryption failed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,5 +4,9 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<string>development</string>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>$(AppIdentifierPrefix)eu.twonly.shared</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -169,9 +169,13 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// First icon (bottom icon)
|
// First icon (bottom icon)
|
||||||
icons[0],
|
icons[0],
|
||||||
Positioned(
|
|
||||||
top: 5.0,
|
Transform(
|
||||||
left: 5.0,
|
transform: Matrix4.identity()
|
||||||
|
..scale(0.7) // Scale to half
|
||||||
|
..translate(3.0, 5.0),
|
||||||
|
// Move down by 10 pixels (adjust as needed)
|
||||||
|
alignment: Alignment.center,
|
||||||
child: icons[1],
|
child: icons[1],
|
||||||
),
|
),
|
||||||
// Second icon (top icon, slightly offset)
|
// Second icon (top icon, slightly offset)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'dart:ui' as ui;
|
||||||
import 'package:cryptography_plus/cryptography_plus.dart';
|
import 'package:cryptography_plus/cryptography_plus.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
@ -321,7 +322,13 @@ Future handlePushData(String pushDataJson) async {
|
||||||
|
|
||||||
Future<Map<int, PushUser>> getPushKeys(String storageKey) async {
|
Future<Map<int, PushUser>> getPushKeys(String storageKey) async {
|
||||||
var storage = getSecureStorage();
|
var storage = getSecureStorage();
|
||||||
String? pushKeysJson = await storage.read(key: storageKey);
|
String? pushKeysJson = await storage.read(
|
||||||
|
key: storageKey,
|
||||||
|
iOptions: IOSOptions(
|
||||||
|
groupId: "CN332ZUGRP.eu.twonly.shared",
|
||||||
|
synchronizable: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
Map<int, PushUser> pushKeys = <int, PushUser>{};
|
Map<int, PushUser> pushKeys = <int, PushUser>{};
|
||||||
if (pushKeysJson != null) {
|
if (pushKeysJson != null) {
|
||||||
Map<String, dynamic> jsonMap = jsonDecode(pushKeysJson);
|
Map<String, dynamic> jsonMap = jsonDecode(pushKeysJson);
|
||||||
|
|
@ -342,7 +349,12 @@ Future setPushKeys(String storageKey, Map<int, PushUser> pushKeys) async {
|
||||||
|
|
||||||
String jsonString = jsonEncode(jsonToSend);
|
String jsonString = jsonEncode(jsonToSend);
|
||||||
print("write: $storageKey: $pushKeys");
|
print("write: $storageKey: $pushKeys");
|
||||||
await storage.write(key: storageKey, value: jsonString);
|
await storage.write(
|
||||||
|
key: storageKey,
|
||||||
|
value: jsonString,
|
||||||
|
iOptions: IOSOptions(
|
||||||
|
groupId: "CN332ZUGRP.eu.twonly.shared", synchronizable: false),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Streams are created so that app can respond to notification-related events
|
/// Streams are created so that app can respond to notification-related events
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ class _UserListItem extends State<UserListItem> {
|
||||||
previewMessages = [];
|
previewMessages = [];
|
||||||
} else if (newMessagesNotOpened.isEmpty) {
|
} else if (newMessagesNotOpened.isEmpty) {
|
||||||
// there are no not opened messages show just the last message in the table
|
// there are no not opened messages show just the last message in the table
|
||||||
currentMessage = newLastMessages.first;
|
currentMessage = newLastMessages.last;
|
||||||
previewMessages = newLastMessages;
|
previewMessages = newLastMessages;
|
||||||
} else {
|
} else {
|
||||||
// filter first for received messages
|
// filter first for received messages
|
||||||
|
|
@ -208,28 +208,12 @@ class _UserListItem extends State<UserListItem> {
|
||||||
newMessagesNotOpened.where((x) => x.messageOtherId != null).toList();
|
newMessagesNotOpened.where((x) => x.messageOtherId != null).toList();
|
||||||
|
|
||||||
if (receivedMessages.isNotEmpty) {
|
if (receivedMessages.isNotEmpty) {
|
||||||
// There are received messages
|
|
||||||
final mediaMessages =
|
|
||||||
receivedMessages.where((x) => x.kind == MessageKind.media);
|
|
||||||
|
|
||||||
if (mediaMessages.isNotEmpty) {
|
|
||||||
currentMessage = mediaMessages.first;
|
|
||||||
} else {
|
|
||||||
currentMessage = receivedMessages.first;
|
|
||||||
}
|
|
||||||
previewMessages = receivedMessages;
|
previewMessages = receivedMessages;
|
||||||
|
currentMessage = receivedMessages.first;
|
||||||
} else {
|
} else {
|
||||||
// The not opened message was send
|
previewMessages = newMessagesNotOpened;
|
||||||
final mediaMessages =
|
|
||||||
newMessagesNotOpened.where((x) => x.kind == MessageKind.media);
|
|
||||||
|
|
||||||
if (mediaMessages.isNotEmpty) {
|
|
||||||
currentMessage = mediaMessages.first;
|
|
||||||
} else {
|
|
||||||
currentMessage = newMessagesNotOpened.first;
|
currentMessage = newMessagesNotOpened.first;
|
||||||
}
|
}
|
||||||
previewMessages = [currentMessage!];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastMessages = newLastMessages;
|
lastMessages = newLastMessages;
|
||||||
|
|
@ -299,18 +283,20 @@ class _UserListItem extends State<UserListItem> {
|
||||||
globalUpdateOfHomeViewPageIndex(0);
|
globalUpdateOfHomeViewPageIndex(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Message msg = currentMessage!;
|
List<Message> msgs = previewMessages
|
||||||
if (msg.kind == MessageKind.media &&
|
.where((x) => x.kind == MessageKind.media)
|
||||||
msg.messageOtherId != null &&
|
.toList();
|
||||||
msg.openedAt == null) {
|
if (msgs.isNotEmpty &&
|
||||||
switch (msg.downloadState) {
|
msgs.first.kind == MessageKind.media &&
|
||||||
|
msgs.first.messageOtherId != null &&
|
||||||
|
msgs.first.openedAt == null) {
|
||||||
|
switch (msgs.first.downloadState) {
|
||||||
case DownloadState.pending:
|
case DownloadState.pending:
|
||||||
MediaMessageContent content =
|
MediaMessageContent content = MediaMessageContent.fromJson(
|
||||||
MediaMessageContent.fromJson(jsonDecode(msg.contentJson!));
|
jsonDecode(msgs.first.contentJson!));
|
||||||
tryDownloadMedia(msg.messageId, msg.contactId, content,
|
tryDownloadMedia(
|
||||||
|
msgs.first.messageId, msgs.first.contactId, content,
|
||||||
force: true);
|
force: true);
|
||||||
return;
|
|
||||||
|
|
||||||
case DownloadState.downloaded:
|
case DownloadState.downloaded:
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
@ -318,11 +304,9 @@ class _UserListItem extends State<UserListItem> {
|
||||||
return MediaViewerView(widget.user.userId);
|
return MediaViewerView(widget.user.userId);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -590,10 +590,9 @@ packages:
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_secure_storage
|
path: "../flutter_secure_storage/flutter_secure_storage"
|
||||||
sha256: f7eceb0bc6f4fd0441e29d43cab9ac2a1c5ffd7ea7b64075136b718c46954874
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
|
||||||
version: "10.0.0-beta.4"
|
version: "10.0.0-beta.4"
|
||||||
flutter_secure_storage_darwin:
|
flutter_secure_storage_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,12 @@ dependencies:
|
||||||
flutter_local_notifications: ^18.0.1
|
flutter_local_notifications: ^18.0.1
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_secure_storage: ^10.0.0-beta.4
|
# flutter_secure_storage: ^10.0.0-beta.4
|
||||||
|
flutter_secure_storage:
|
||||||
|
path: ../flutter_secure_storage/flutter_secure_storage
|
||||||
|
# git:
|
||||||
|
# url: https://github.com/juliansteenbakker/flutter_secure_storage/tree/develop/flutter_secure_storage_darwin
|
||||||
|
# ref: develop # 10.0.0-beta.4 does not work because of https://github.com/juliansteenbakker/flutter_secure_storage/issues/8660
|
||||||
font_awesome_flutter: ^10.8.0
|
font_awesome_flutter: ^10.8.0
|
||||||
gal: ^2.3.1
|
gal: ^2.3.1
|
||||||
hand_signature: ^3.0.3
|
hand_signature: ^3.0.3
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue