mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
Refactoring
This commit is contained in:
parent
98a80e5704
commit
aea6030d43
2 changed files with 132 additions and 117 deletions
|
@ -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<SHA256>.deriveKey(
|
||||
inputKeyMaterial: pseudoRandomKey,
|
||||
salt: salt,
|
||||
info: keyInfo,
|
||||
outputByteCount: keyLength)
|
||||
let nonce = HKDF<SHA256>.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SHA256>.deriveKey(
|
||||
inputKeyMaterial: pseudoRandomKey,
|
||||
salt: salt,
|
||||
info: keyInfo,
|
||||
outputByteCount: keyLength)
|
||||
let nonce = HKDF<SHA256>.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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue