diff --git a/Notification Service Extension/NotificationService.swift b/Notification Service Extension/NotificationService.swift index 758a75c..1e7826c 100644 --- a/Notification Service Extension/NotificationService.swift +++ b/Notification Service Extension/NotificationService.swift @@ -1,10 +1,7 @@ // Copyright © 2020 Metabolist. All rights reserved. -import CryptoKit -import Keychain import Kingfisher import Mastodon -import Secrets import ServiceLayer import UserNotifications @@ -28,13 +25,13 @@ final class NotificationService: UNNotificationServiceExtension { guard let bestAttemptContent = bestAttemptContent else { return } - let pushNotification: PushNotification + let parsingService = PushNotificationParsingService(environment: environment) let decryptedJSON: Data let identityId: Identity.Id + let pushNotification: PushNotification do { - (decryptedJSON, identityId) = try Self.extractAndDecrypt(userInfo: request.content.userInfo) - + (decryptedJSON, identityId) = try parsingService.extractAndDecrypt(userInfo: request.content.userInfo) pushNotification = try MastodonDecoder().decode(PushNotification.self, from: decryptedJSON) } catch { contentHandler(bestAttemptContent) @@ -72,105 +69,7 @@ final class NotificationService: UNNotificationServiceExtension { } } -enum NotificationServiceError: Error { - case userInfoDataAbsent - case keychainDataAbsent -} - private extension NotificationService { - static let identityIdUserInfoKey = "i" - static let encryptedMessageUserInfoKey = "m" - static let saltUserInfoKey = "s" - static let serverPublicKeyUserInfoKey = "k" - static let keyLength = 16 - static let nonceLength = 12 - static let pseudoRandomKeyLength = 32 - static let paddedByteCount = 2 - static let curve = "P-256" - - enum HKDFInfo: String { - case auth, aesgcm, nonce - - var bytes: [UInt8] { - Array("Content-Encoding: \(self)\0".utf8) - } - } - - static func extractAndDecrypt(userInfo: [AnyHashable: Any]) throws -> (Data, Identity.Id) { - guard - let identityIdString = userInfo[identityIdUserInfoKey] as? String, - let identityId = Identity.Id(uuidString: identityIdString), - let encryptedMessageBase64 = (userInfo[encryptedMessageUserInfoKey] as? String)?.URLSafeBase64ToBase64(), - let encryptedMessage = Data(base64Encoded: encryptedMessageBase64), - let saltBase64 = (userInfo[saltUserInfoKey] as? String)?.URLSafeBase64ToBase64(), - let salt = Data(base64Encoded: saltBase64), - let serverPublicKeyBase64 = (userInfo[serverPublicKeyUserInfoKey] as? String)?.URLSafeBase64ToBase64(), - let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64) - else { throw NotificationServiceError.userInfoDataAbsent } - - let secretsService = Secrets(identityId: identityId, keychain: LiveKeychain.self) - - guard - let auth = try secretsService.getPushAuth(), - let pushKey = try secretsService.getPushKey() - else { throw NotificationServiceError.keychainDataAbsent } - - return (try decrypt(encryptedMessage: encryptedMessage, - privateKeyData: pushKey, - serverPublicKeyData: serverPublicKeyData, - auth: auth, - salt: salt), - identityId) - } - - static func decrypt(encryptedMessage: Data, - privateKeyData: Data, - serverPublicKeyData: Data, - auth: Data, - salt: Data) throws -> Data { - let privateKey = try P256.KeyAgreement.PrivateKey(x963Representation: privateKeyData) - let serverPublicKey = try P256.KeyAgreement.PublicKey(x963Representation: serverPublicKeyData) - let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: serverPublicKey) - - var keyInfo = HKDFInfo.aesgcm.bytes - var nonceInfo = HKDFInfo.nonce.bytes - var context = Array(curve.utf8) - let publicKeyData = privateKey.publicKey.x963Representation - - context.append(0) - context.append(0) - context.append(UInt8(publicKeyData.count)) - context += Array(publicKeyData) - context.append(0) - context.append(UInt8(serverPublicKeyData.count)) - context += Array(serverPublicKeyData) - - keyInfo += context - nonceInfo += context - - let pseudoRandomKey = sharedSecret.hkdfDerivedSymmetricKey( - using: SHA256.self, - salt: auth, - sharedInfo: HKDFInfo.auth.bytes, - outputByteCount: pseudoRandomKeyLength) - let key = HKDF.deriveKey( - inputKeyMaterial: pseudoRandomKey, - salt: salt, - info: keyInfo, - outputByteCount: keyLength) - let nonce = HKDF.deriveKey( - inputKeyMaterial: pseudoRandomKey, - salt: salt, - info: nonceInfo, - outputByteCount: nonceLength) - - let sealedBox = try AES.GCM.SealedBox(combined: nonce.withUnsafeBytes(Array.init) + encryptedMessage) - let decrypted = try AES.GCM.open(sealedBox, using: key) - let unpadded = decrypted.suffix(from: paddedByteCount) - - return Data(unpadded) - } - static func addImage(pushNotification: PushNotification, bestAttemptContent: UNMutableNotificationContent, contentHandler: @escaping (UNNotificationContent) -> Void) { @@ -208,16 +107,3 @@ private extension NotificationService { } } } - -extension String { - func URLSafeBase64ToBase64() -> String { - var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") - let countMod4 = count % 4 - - if countMod4 != 0 { - base64.append(String(repeating: "=", count: 4 - countMod4)) - } - - return base64 - } -} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/PushNotificationParsingService.swift b/ServiceLayer/Sources/ServiceLayer/Services/PushNotificationParsingService.swift new file mode 100644 index 0000000..a11a749 --- /dev/null +++ b/ServiceLayer/Sources/ServiceLayer/Services/PushNotificationParsingService.swift @@ -0,0 +1,129 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import CryptoKit +import Foundation +import Mastodon +import Secrets + +enum NotificationExtensionServiceError: Error { + case userInfoDataAbsent + case keychainDataAbsent +} + +public struct PushNotificationParsingService { + private let environment: AppEnvironment + + public init(environment: AppEnvironment) { + self.environment = environment + } +} + +public extension PushNotificationParsingService { + func extractAndDecrypt(userInfo: [AnyHashable: Any]) throws -> (Data, Identity.Id) { + guard let identityIdString = userInfo[Self.identityIdUserInfoKey] as? String, + let identityId = Identity.Id(uuidString: identityIdString), + let encryptedMessageBase64 = (userInfo[Self.encryptedMessageUserInfoKey] as? String)? + .URLSafeBase64ToBase64(), + let encryptedMessage = Data(base64Encoded: encryptedMessageBase64), + let saltBase64 = (userInfo[Self.saltUserInfoKey] as? String)?.URLSafeBase64ToBase64(), + let salt = Data(base64Encoded: saltBase64), + let serverPublicKeyBase64 = (userInfo[Self.serverPublicKeyUserInfoKey] as? String)? + .URLSafeBase64ToBase64(), + let serverPublicKeyData = Data(base64Encoded: serverPublicKeyBase64) + else { throw NotificationExtensionServiceError.userInfoDataAbsent } + + let secrets = Secrets(identityId: identityId, keychain: environment.keychain) + + guard let auth = try secrets.getPushAuth(), + let pushKey = try secrets.getPushKey() + else { throw NotificationExtensionServiceError.keychainDataAbsent } + + return (try Self.decrypt(encryptedMessage: encryptedMessage, + privateKeyData: pushKey, + serverPublicKeyData: serverPublicKeyData, + auth: auth, + salt: salt), + identityId) + } +} + +private extension PushNotificationParsingService { + static let identityIdUserInfoKey = "i" + static let encryptedMessageUserInfoKey = "m" + static let saltUserInfoKey = "s" + static let serverPublicKeyUserInfoKey = "k" + static let keyLength = 16 + static let nonceLength = 12 + static let pseudoRandomKeyLength = 32 + static let paddedByteCount = 2 + static let curve = "P-256" + + enum HKDFInfo: String { + case auth, aesgcm, nonce + + var bytes: [UInt8] { + Array("Content-Encoding: \(self)\0".utf8) + } + } + + static func decrypt(encryptedMessage: Data, + privateKeyData: Data, + serverPublicKeyData: Data, + auth: Data, + salt: Data) throws -> Data { + let privateKey = try P256.KeyAgreement.PrivateKey(x963Representation: privateKeyData) + let serverPublicKey = try P256.KeyAgreement.PublicKey(x963Representation: serverPublicKeyData) + let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: serverPublicKey) + + var keyInfo = HKDFInfo.aesgcm.bytes + var nonceInfo = HKDFInfo.nonce.bytes + var context = Array(curve.utf8) + let publicKeyData = privateKey.publicKey.x963Representation + + context.append(0) + context.append(0) + context.append(UInt8(publicKeyData.count)) + context += Array(publicKeyData) + context.append(0) + context.append(UInt8(serverPublicKeyData.count)) + context += Array(serverPublicKeyData) + + keyInfo += context + nonceInfo += context + + let pseudoRandomKey = sharedSecret.hkdfDerivedSymmetricKey( + using: SHA256.self, + salt: auth, + sharedInfo: HKDFInfo.auth.bytes, + outputByteCount: pseudoRandomKeyLength) + let key = HKDF.deriveKey( + inputKeyMaterial: pseudoRandomKey, + salt: salt, + info: keyInfo, + outputByteCount: keyLength) + let nonce = HKDF.deriveKey( + inputKeyMaterial: pseudoRandomKey, + salt: salt, + info: nonceInfo, + outputByteCount: nonceLength) + + let sealedBox = try AES.GCM.SealedBox(combined: nonce.withUnsafeBytes(Array.init) + encryptedMessage) + let decrypted = try AES.GCM.open(sealedBox, using: key) + let unpadded = decrypted.suffix(from: paddedByteCount) + + return Data(unpadded) + } +} + +private extension String { + func URLSafeBase64ToBase64() -> String { + var base64 = replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + let countMod4 = count % 4 + + if countMod4 != 0 { + base64.append(String(repeating: "=", count: 4 - countMod4)) + } + + return base64 + } +}