From a6894639076657f5ba0b308ec4feaffea1fbce84 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 6 Sep 2020 14:37:54 -0700 Subject: [PATCH] Modularize base16 encoding --- Base16/.gitignore | 5 + Base16/Package.swift | 22 ++++ Base16/Sources/Base16/Data+Base16.swift | 68 ++++++++++++ Base16/Tests/Base16Tests/Base16Tests.swift | 101 ++++++++++++++++++ DB/Sources/DB/Entities/Identity.swift | 2 +- DB/Sources/DB/Identity/IdentityDatabase.swift | 6 +- DB/Sources/DB/Identity/IdentityRecord.swift | 2 +- Extensions/Data+Extensions.swift | 9 -- Metatext.xcodeproj/project.pbxproj | 6 +- Secrets/Package.swift | 3 +- Secrets/Sources/Secrets/Secrets.swift | 9 +- .../Services/AllIdentitiesService.swift | 2 +- .../Services/IdentityService.swift | 5 +- System/AppDelegate.swift | 4 +- .../Sources/ViewModels/RootViewModel.swift | 4 +- 15 files changed, 215 insertions(+), 33 deletions(-) create mode 100644 Base16/.gitignore create mode 100644 Base16/Package.swift create mode 100644 Base16/Sources/Base16/Data+Base16.swift create mode 100644 Base16/Tests/Base16Tests/Base16Tests.swift delete mode 100644 Extensions/Data+Extensions.swift diff --git a/Base16/.gitignore b/Base16/.gitignore new file mode 100644 index 0000000..95c4320 --- /dev/null +++ b/Base16/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ diff --git a/Base16/Package.swift b/Base16/Package.swift new file mode 100644 index 0000000..7ed2758 --- /dev/null +++ b/Base16/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Base16", + products: [ + .library( + name: "Base16", + targets: ["Base16"]) + ], + dependencies: [], + targets: [ + .target( + name: "Base16", + dependencies: []), + .testTarget( + name: "Base16Tests", + dependencies: ["Base16"]) + ] +) diff --git a/Base16/Sources/Base16/Data+Base16.swift b/Base16/Sources/Base16/Data+Base16.swift new file mode 100644 index 0000000..e2a3a17 --- /dev/null +++ b/Base16/Sources/Base16/Data+Base16.swift @@ -0,0 +1,68 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +public enum Base16EncodingError: Error { + case invalidLength + case invalidByteString(String) + case invalidStringEncoding +} + +public extension Data { + enum Base16EncodingOptions { + case uppercase + } + + func base16EncodedString(options: [Base16EncodingOptions] = []) -> String { + map { String(format: Self.format(options: options), $0) }.joined() + } + + func base16EncodedData(options: [Base16EncodingOptions] = []) -> Data { + Data(base16EncodedString(options: options).utf8) + } + + init(base16Encoded string: String) throws { + let stringLength = string.count + + guard stringLength % 2 == 0 else { + throw Base16EncodingError.invalidLength + } + + var data = [UInt8]() + + data.reserveCapacity(stringLength / 2) + + var i = string.startIndex + + while i != string.endIndex { + let j = string.index(i, offsetBy: 2) + let byteString = string[i.. String { + options.contains(.uppercase) ? uppercaseBase16Format : lowercaseBase16Format + } +} diff --git a/Base16/Tests/Base16Tests/Base16Tests.swift b/Base16/Tests/Base16Tests/Base16Tests.swift new file mode 100644 index 0000000..3586bd3 --- /dev/null +++ b/Base16/Tests/Base16Tests/Base16Tests.swift @@ -0,0 +1,101 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +@testable import Base16 +import XCTest + +final class Base16Tests: XCTestCase { + let testData = Data([182, 239, 215, 173, 251, 168, 76, 252, + 140, 7, 39, 163, 56, 255, 171, 35, + 121, 205, 26, 252, 53, 166, 159, 67, + 100, 70, 140, 79, 47, 26, 138, 209]) + let testDataLowercaseString = "b6efd7adfba84cfc8c0727a338ffab2379cd1afc35a69f4364468c4f2f1a8ad1" + let testDataLowercaseStringData = Data([98, 54, 101, 102, 100, 55, 97, 100, + 102, 98, 97, 56, 52, 99, 102, 99, + 56, 99, 48, 55, 50, 55, 97, 51, 51, + 56, 102, 102, 97, 98, 50, 51, 55, + 57, 99, 100, 49, 97, 102, 99, 51, + 53, 97, 54, 57, 102, 52, 51, 54, + 52, 52, 54, 56, 99, 52, 102, 50, + 102, 49, 97, 56, 97, 100, 49]) + let testDataUppercaseString = "B6EFD7ADFBA84CFC8C0727A338FFAB2379CD1AFC35A69F4364468C4F2F1A8AD1" + let testDataUppercaseStringData = Data([66, 54, 69, 70, 68, 55, 65, 68, + 70, 66, 65, 56, 52, 67, 70, 67, + 56, 67, 48, 55, 50, 55, 65, 51, + 51, 56, 70, 70, 65, 66, 50, 51, + 55, 57, 67, 68, 49, 65, 70, 67, + 51, 53, 65, 54, 57, 70, 52, 51, + 54, 52, 52, 54, 56, 67, 52, 70, + 50, 70, 49, 65, 56, 65, 68, 49]) + + func testLowercaseString() { + XCTAssertEqual(testData.base16EncodedString(), testDataLowercaseString) + } + + func testUppercaseString() { + XCTAssertEqual(testData.base16EncodedString(options: [.uppercase]), + testDataUppercaseString) + } + + func testLowercaseData() { + XCTAssertEqual(testData.base16EncodedData(), testDataLowercaseStringData) + } + + func testUppercaseData() { + XCTAssertEqual(testData.base16EncodedData(options: [.uppercase]), + testDataUppercaseStringData) + } + + func testInitializationFromLowercaseString() throws { + XCTAssertEqual(try Data(base16Encoded: testDataLowercaseString), testData) + } + + func testInitializationFromUppercaseString() throws { + XCTAssertEqual(try Data(base16Encoded: testDataUppercaseString), testData) + } + + func testInitializationFromLowercaseData() throws { + XCTAssertEqual(try Data(base16Encoded: testDataLowercaseStringData), testData) + } + + func testInitializationFromUppercaseData() throws { + XCTAssertEqual(try Data(base16Encoded: testDataUppercaseStringData), testData) + } + + func testInvalidLength() throws { + let invalidLength = String(testDataLowercaseString.prefix(testDataLowercaseString.count - 1)) + + XCTAssertThrowsError(try Data(base16Encoded: invalidLength)) { + guard case Base16EncodingError.invalidLength = $0 else { + XCTFail("Expected invalid length error") + + return + } + } + } + + func testInvalidByteString() { + let invalidString = testDataLowercaseString.replacingOccurrences(of: "a", with: "z") + + XCTAssertThrowsError(try Data(base16Encoded: invalidString)) { + guard case let Base16EncodingError.invalidByteString(invalidByteString) = $0 else { + XCTFail("Expected invalid byte string error") + + return + } + + XCTAssertEqual(invalidByteString, "zd") + } + } + + func testInvalidStringEncoding() { + let invalidData = testDataLowercaseString.data(using: .utf16)! + + XCTAssertThrowsError(try Data(base16Encoded: invalidData)) { + guard case Base16EncodingError.invalidStringEncoding = $0 else { + XCTFail("Expected string encoding error") + + return + } + } + } +} diff --git a/DB/Sources/DB/Entities/Identity.swift b/DB/Sources/DB/Entities/Identity.swift index 06f7d9e..19e6083 100644 --- a/DB/Sources/DB/Entities/Identity.swift +++ b/DB/Sources/DB/Entities/Identity.swift @@ -10,7 +10,7 @@ public struct Identity: Codable, Hashable, Identifiable { public let preferences: Identity.Preferences public let instance: Identity.Instance? public let account: Identity.Account? - public let lastRegisteredDeviceToken: String? + public let lastRegisteredDeviceToken: Data? public let pushSubscriptionAlerts: PushSubscription.Alerts } diff --git a/DB/Sources/DB/Identity/IdentityDatabase.swift b/DB/Sources/DB/Identity/IdentityDatabase.swift index 0d4d10d..75c034c 100644 --- a/DB/Sources/DB/Identity/IdentityDatabase.swift +++ b/DB/Sources/DB/Identity/IdentityDatabase.swift @@ -116,7 +116,7 @@ public extension IdentityDatabase { } func updatePushSubscription(alerts: PushSubscription.Alerts, - deviceToken: String? = nil, + deviceToken: Data? = nil, forIdentityID identityID: UUID) -> AnyPublisher { databaseQueue.writePublisher { let data = try IdentityRecord.databaseJSONEncoder(for: "pushSubscriptionAlerts").encode(alerts) @@ -180,7 +180,7 @@ public extension IdentityDatabase { .eraseToAnyPublisher() } - func identitiesWithOutdatedDeviceTokens(deviceToken: String) -> AnyPublisher<[Identity], Error> { + func identitiesWithOutdatedDeviceTokens(deviceToken: Data) -> AnyPublisher<[Identity], Error> { databaseQueue.readPublisher( value: Self.identitiesRequest() .filter(Column("lastRegisteredDeviceToken") != deviceToken) @@ -221,7 +221,7 @@ private extension IdentityDatabase { .references("instance", column: "uri") t.column("preferences", .blob).notNull() t.column("pushSubscriptionAlerts", .blob).notNull() - t.column("lastRegisteredDeviceToken", .text) + t.column("lastRegisteredDeviceToken", .blob) } try db.create(table: "account", ifNotExists: true) { t in diff --git a/DB/Sources/DB/Identity/IdentityRecord.swift b/DB/Sources/DB/Identity/IdentityRecord.swift index d1df8e8..0fe60a7 100644 --- a/DB/Sources/DB/Identity/IdentityRecord.swift +++ b/DB/Sources/DB/Identity/IdentityRecord.swift @@ -10,7 +10,7 @@ struct IdentityRecord: Codable, Hashable, FetchableRecord, PersistableRecord { let lastUsedAt: Date let preferences: Identity.Preferences let instanceURI: String? - let lastRegisteredDeviceToken: String? + let lastRegisteredDeviceToken: Data? let pushSubscriptionAlerts: PushSubscription.Alerts } diff --git a/Extensions/Data+Extensions.swift b/Extensions/Data+Extensions.swift deleted file mode 100644 index 68fb103..0000000 --- a/Extensions/Data+Extensions.swift +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright © 2020 Metabolist. All rights reserved. - -import Foundation - -extension Data { - func hexEncodedString() -> String { - map { String(format: "%02hhx", $0) }.joined() - } -} diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 4cc0dc6..6aea70f 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -41,7 +41,6 @@ D0C7D4D724F7616A001EBDBB /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */; }; D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */; }; D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D46F24F76169001EBDBB /* View+Extensions.swift */; }; - D0C7D4DC24F7616A001EBDBB /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D47124F76169001EBDBB /* Data+Extensions.swift */; }; D0E2C1D124FD97F000854680 /* ViewModels in Frameworks */ = {isa = PBXBuildFile; productRef = D0E2C1D024FD97F000854680 /* ViewModels */; }; D0E5361C24E3EB4D00FB1CE1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E5361B24E3EB4D00FB1CE1 /* NotificationService.swift */; }; D0E5362024E3EB4D00FB1CE1 /* Notification Service Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D0E5361924E3EB4D00FB1CE1 /* Notification Service Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -87,6 +86,7 @@ D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D0666A2524C677B400F3F04B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D085C3BB25008DEC008A6C5E /* DB */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DB; sourceTree = ""; }; + D0AD03552505814D0085A466 /* Base16 */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base16; sourceTree = ""; }; D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = ""; }; D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = ""; }; @@ -120,7 +120,6 @@ D0C7D46C24F76169001EBDBB /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KingfisherOptionsInfo+Extensions.swift"; sourceTree = ""; }; D0C7D46F24F76169001EBDBB /* View+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; - D0C7D47124F76169001EBDBB /* Data+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = ""; }; D0D7C013250440610039AD6F /* CodableBloomFilter */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CodableBloomFilter; sourceTree = ""; }; D0E0F1E424FC49FC002C04BF /* Mastodon */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Mastodon; sourceTree = ""; }; D0E2C1CF24FD8BA400854680 /* ViewModels */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ViewModels; sourceTree = ""; }; @@ -183,6 +182,7 @@ isa = PBXGroup; children = ( D0C7D45224F76169001EBDBB /* Assets.xcassets */, + D0AD03552505814D0085A466 /* Base16 */, D0D7C013250440610039AD6F /* CodableBloomFilter */, D085C3BB25008DEC008A6C5E /* DB */, D0C7D46824F76169001EBDBB /* Extensions */, @@ -291,7 +291,6 @@ D0C7D46824F76169001EBDBB /* Extensions */ = { isa = PBXGroup; children = ( - D0C7D47124F76169001EBDBB /* Data+Extensions.swift */, D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */, D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */, D0C7D46A24F76169001EBDBB /* String+Extensions.swift */, @@ -493,7 +492,6 @@ D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */, D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */, D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */, - D0C7D4DC24F7616A001EBDBB /* Data+Extensions.swift in Sources */, D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */, D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */, D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */, diff --git a/Secrets/Package.swift b/Secrets/Package.swift index 65bbbca..7a21c75 100644 --- a/Secrets/Package.swift +++ b/Secrets/Package.swift @@ -14,12 +14,13 @@ let package = Package( targets: ["Secrets"]) ], dependencies: [ + .package(path: "Base16"), .package(path: "Keychain") ], targets: [ .target( name: "Secrets", - dependencies: ["Keychain"]), + dependencies: ["Base16", "Keychain"]), .testTarget( name: "SecretsTests", dependencies: ["Secrets"]) diff --git a/Secrets/Sources/Secrets/Secrets.swift b/Secrets/Sources/Secrets/Secrets.swift index f6765d0..d7d1133 100644 --- a/Secrets/Sources/Secrets/Secrets.swift +++ b/Secrets/Sources/Secrets/Secrets.swift @@ -1,5 +1,6 @@ // Copyright © 2020 Metabolist. All rights reserved. +import Base16 import Foundation import Keychain @@ -84,7 +85,7 @@ public extension Secrets { } } - return "x'\(passphraseData.uppercaseHexEncodedString())'" + return "x'\(passphraseData.base16EncodedString(options: [.uppercase]))'" } func deleteAllItems() throws { @@ -225,9 +226,3 @@ private struct PushKey { kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeySizeInBits as String: sizeInBits] } - -private extension Data { - func uppercaseHexEncodedString() -> String { - map { String(format: "%02hhX", $0) }.joined() - } -} diff --git a/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift b/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift index f182aec..d8bb59d 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/AllIdentitiesService.swift @@ -77,7 +77,7 @@ public extension AllIdentitiesService { .eraseToAnyPublisher() } - func updatePushSubscriptions(deviceToken: String) -> AnyPublisher { + func updatePushSubscriptions(deviceToken: Data) -> AnyPublisher { identityDatabase.identitiesWithOutdatedDeviceTokens(deviceToken: deviceToken) .tryMap { identities -> [AnyPublisher] in try identities.map { diff --git a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift index a7eec3f..47675a5 100644 --- a/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift +++ b/ServiceLayer/Sources/ServiceLayer/Services/IdentityService.swift @@ -1,5 +1,6 @@ // Copyright © 2020 Metabolist. All rights reserved. +import Base16 import Combine import DB import Foundation @@ -167,7 +168,7 @@ public extension IdentityService { .eraseToAnyPublisher() } - func createPushSubscription(deviceToken: String, alerts: PushSubscription.Alerts) -> AnyPublisher { + func createPushSubscription(deviceToken: Data, alerts: PushSubscription.Alerts) -> AnyPublisher { let publicKey: String let auth: String @@ -180,7 +181,7 @@ public extension IdentityService { let identityID = identity.id let endpoint = Self.pushSubscriptionEndpointURL - .appendingPathComponent(deviceToken) + .appendingPathComponent(deviceToken.base16EncodedString()) .appendingPathComponent(identityID.uuidString) return mastodonAPIClient.request( diff --git a/System/AppDelegate.swift b/System/AppDelegate.swift index 72d6639..2e78fea 100644 --- a/System/AppDelegate.swift +++ b/System/AppDelegate.swift @@ -9,14 +9,14 @@ class AppDelegate: NSObject { } extension AppDelegate { - func registerForRemoteNotifications() -> AnyPublisher { + func registerForRemoteNotifications() -> AnyPublisher { $application .compactMap { $0 } .handleEvents(receiveOutput: { $0.registerForRemoteNotifications() }) .setFailureType(to: Error.self) .zip(remoteNotificationDeviceTokens) .first() - .map { $1.hexEncodedString() } + .map { $1 } .eraseToAnyPublisher() } } diff --git a/ViewModels/Sources/ViewModels/RootViewModel.swift b/ViewModels/Sources/ViewModels/RootViewModel.swift index bcddfc5..adf4d3c 100644 --- a/ViewModels/Sources/ViewModels/RootViewModel.swift +++ b/ViewModels/Sources/ViewModels/RootViewModel.swift @@ -10,11 +10,11 @@ public final class RootViewModel: ObservableObject { @Published private var mostRecentlyUsedIdentityID: UUID? private let allIdentitiesService: AllIdentitiesService private let userNotificationService: UserNotificationService - private let registerForRemoteNotifications: () -> AnyPublisher + private let registerForRemoteNotifications: () -> AnyPublisher private var cancellables = Set() public init(environment: AppEnvironment, - registerForRemoteNotifications: @escaping () -> AnyPublisher) throws { + registerForRemoteNotifications: @escaping () -> AnyPublisher) throws { allIdentitiesService = try AllIdentitiesService(environment: environment) userNotificationService = UserNotificationService(environment: environment) self.registerForRemoteNotifications = registerForRemoteNotifications