Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Active Hub subscription unlocks full version for corresponding vault #326

Merged
merged 1 commit into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cryptomator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
4A09BFC62684D599000E40AB /* VaultDetailItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09BFC52684D599000E40AB /* VaultDetailItem.swift */; };
4A09E54C27071F3C0056D32A /* ErrorMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */; };
4A09E54E27071F4F0056D32A /* ErrorMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A09E54D27071F4F0056D32A /* ErrorMapper.swift */; };
4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */; };
4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */; };
4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */; };
4A0C07E225AC80C100B83211 /* UIView+Preview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07E125AC80C100B83211 /* UIView+Preview.swift */; };
4A0C07EB25AC832900B83211 /* VaultListPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0C07EA25AC832900B83211 /* VaultListPosition.swift */; };
4A0EAAD2296F604200E27B56 /* SessionTaskRegistratorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */; };
Expand Down Expand Up @@ -543,6 +546,9 @@
4A09BFC52684D599000E40AB /* VaultDetailItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultDetailItem.swift; sourceTree = "<group>"; };
4A09E54B27071F3C0056D32A /* ErrorMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapperTests.swift; sourceTree = "<group>"; };
4A09E54D27071F4F0056D32A /* ErrorMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorMapper.swift; sourceTree = "<group>"; };
4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProvider.swift; sourceTree = "<group>"; };
4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderImplTests.swift; sourceTree = "<group>"; };
4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionProviderMock.swift; sourceTree = "<group>"; };
4A0C07E125AC80C100B83211 /* UIView+Preview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Preview.swift"; sourceTree = "<group>"; };
4A0C07EA25AC832900B83211 /* VaultListPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultListPosition.swift; sourceTree = "<group>"; };
4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTaskRegistratorMock.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1192,6 +1198,7 @@
4A9C8DFC27A007C2000063E4 /* FileProviderNotificatorTests.swift */,
4AFBFA19282946BF00E30818 /* InMemoryProgressManagerTests.swift */,
4AB1D4EF27D20420009060AB /* LocalURLProviderTests.swift */,
4A0AA12C2ABA277800CF24FD /* PermissionProviderImplTests.swift */,
4AC1157727F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift */,
4ADC66C427A7F6D6002E6CC7 /* UnlockMonitorTests.swift */,
4A4F47F224B875070033328B /* URL+NameCollisionExtensionTests.swift */,
Expand Down Expand Up @@ -1717,6 +1724,7 @@
4AEECD3E279EC48200C6E2B5 /* NSFileProviderChangeObserverMock.swift */,
4AA782E3282A9007001A71E3 /* NSFileProviderDomainProviderMock.swift */,
4AEECD3A279EB24300C6E2B5 /* NSFileProviderEnumerationObserverMock.swift */,
4A0AA12E2ABA2A1600CF24FD /* PermissionProviderMock.swift */,
4AFBFA172829414A00E30818 /* ProgressManagerMock.swift */,
4A0EAAD1296F604200E27B56 /* SessionTaskRegistratorMock.swift */,
4ADC66C627A95E67002E6CC7 /* UnlockMonitorTaskExecutorMock.swift */,
Expand Down Expand Up @@ -1901,6 +1909,7 @@
4AB1D4EB27D0E027009060AB /* LocalURLProviderType.swift */,
4AA782DD282A8250001A71E3 /* NSFileProviderDomainProvider.swift */,
4AEE6EE02822A33400E1B35E /* NSFileProviderItemIdentifier+Database.swift */,
4A0AA12A2AB8DB1800CF24FD /* PermissionProvider.swift */,
4AEE6EE92825716400E1B35E /* ProgressManager.swift */,
4AC1157527F5BD890023F51B /* Promise+AllIgnoringResult.swift */,
4ADD233F26737CD400374E4E /* RootFileProviderItem.swift */,
Expand Down Expand Up @@ -2537,6 +2546,7 @@
4AFBFA1628293FE200E30818 /* UploadRetryingServiceSourceTests.swift in Sources */,
4AB1C33C265E9DBC00DC7A49 /* CloudTaskExecutorTestCase.swift in Sources */,
4AE5196727F495BF00BA6E4A /* WorkflowDependencyTasksCollectionMock.swift in Sources */,
4A0AA12F2ABA2A1600CF24FD /* PermissionProviderMock.swift in Sources */,
4AC1157827F5BEFD0023F51B /* Promise+AllIgnoringResultsTests.swift in Sources */,
4AE5196527F48D6600BA6E4A /* WorkflowDependencyFactoryTests.swift in Sources */,
4A49FABE271ECDE80069A0CC /* ItemEnumerationTaskManagerTests.swift in Sources */,
Expand Down Expand Up @@ -2570,6 +2580,7 @@
4ADC66C527A7F6D6002E6CC7 /* UnlockMonitorTests.swift in Sources */,
4ABC08D7250D1EB600E3CEDC /* DeletionTaskManagerTests.swift in Sources */,
4A511D45265EB13B000A0E01 /* ItemEnumerationTaskTests.swift in Sources */,
4A0AA12D2ABA277800CF24FD /* PermissionProviderImplTests.swift in Sources */,
4A2F373724B47DB800460FD3 /* UploadTaskManagerTests.swift in Sources */,
4A248221266B8D37002D9F59 /* FileProviderAdapterImportDocumentTests.swift in Sources */,
4A511D5326615439000A0E01 /* ReparentTaskExecutorTests.swift in Sources */,
Expand Down Expand Up @@ -2935,6 +2946,7 @@
4A511D5D26668E47000A0E01 /* ReparentTaskRecord.swift in Sources */,
747F2F272587BC250072FB30 /* ReparentTask.swift in Sources */,
747F2F282587BC250072FB30 /* ReparentTaskDBManager.swift in Sources */,
4A0AA12B2AB8DB1800CF24FD /* PermissionProvider.swift in Sources */,
4AB1D4EC27D0E027009060AB /* LocalURLProviderType.swift in Sources */,
4A511D4E2660FF9E000A0E01 /* WorkflowScheduler.swift in Sources */,
4AD9481A2909A66900072110 /* MaintenanceModeHelperServiceSource.swift in Sources */,
Expand Down
4 changes: 3 additions & 1 deletion Cryptomator/AddVault/Hub/HubAddVaultCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ class AddHubVaultCoordinator: Coordinator {
}

extension AddHubVaultCoordinator: HubAuthenticationFlowDelegate {
func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async {
func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async {
let jwe = response.jwe
let privateKey = response.privateKey
let hubVault = ExistingHubVault(vaultUID: vaultUID,
delegateAccountUID: accountUID,
jweData: jwe.compactSerializedData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,9 @@ public class CryptomatorDatabase {
}

class func initialHubSupportMigration(_ db: Database) throws {
try db.create(table: "hubAccountInfo", body: { table in
table.column("userID", .text).primaryKey()
})
try db.create(table: "hubVaultAccount", body: { table in
table.column("id", .integer).primaryKey()
table.column("vaultUID", .text).notNull().unique().references("vaultAccounts", onDelete: .cascade)
table.column("hubUserID", .text).notNull().references("hubAccountInfo", onDelete: .cascade)
table.column("vaultUID", .text).primaryKey().references("vaultAccounts", onDelete: .cascade)
table.column("subscriptionState", .text).notNull()
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import CryptomatorCloudAccessCore
import Foundation

public enum HubAuthenticationFlow {
case receivedExistingKey(Data)
case success(Data, [AnyHashable: Any])
case accessNotGranted
case needsDeviceRegistration
case licenseExceeded
Expand Down Expand Up @@ -53,9 +53,10 @@ public class CryptomatorHubAuthenticator: HubDeviceRegistering, HubKeyReceiving
var urlRequest = URLRequest(url: url)
urlRequest.allHTTPHeaderFields = ["Authorization": "Bearer \(accessToken)"]
let (data, response) = try await URLSession.shared.data(with: urlRequest)
switch (response as? HTTPURLResponse)?.statusCode {
let httpResponse = response as? HTTPURLResponse
switch httpResponse?.statusCode {
case 200:
return .receivedExistingKey(data)
return .success(data, httpResponse?.allHeaderFields ?? [:])
case 402:
return .licenseExceeded
case 403:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@ import CryptoKit
import JOSESwift

public protocol HubAuthenticationFlowDelegate: AnyObject {
func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async
func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async
}

public struct HubUnlockResponse {
public let jwe: JWE
public let privateKey: P384.KeyAgreement.PrivateKey
public let subscriptionState: HubSubscriptionState
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppAuthCore
import CocoaLumberjackSwift
import CryptoKit
import CryptomatorCloudAccessCore
import Foundation
Expand All @@ -8,6 +9,8 @@ import UIKit
public enum HubAuthenticationViewModelError: Error {
case missingHubConfig
case missingAuthState
case missingSubscriptionHeader
case unexpectedSubscriptionHeader
}

public class HubAuthenticationViewModel: ObservableObject {
Expand All @@ -25,6 +28,10 @@ public class HubAuthenticationViewModel: ObservableObject {
case needsAuthorization
}

private enum Constants {
static var subscriptionState: String { "hub-subscription-state" }
}

@Published var authenticationFlowState: State = .userLogin
@Published public var deviceName: String = UIDevice.current.name

Expand Down Expand Up @@ -101,8 +108,8 @@ public class HubAuthenticationViewModel: ObservableObject {
return
}
switch authFlow {
case let .receivedExistingKey(data):
await receivedExistingKey(data: data)
case let .success(data, header):
await receivedExistingKey(data: data, header: header)
case .accessNotGranted:
await setState(to: .accessNotGranted)
case .needsDeviceRegistration:
Expand All @@ -112,17 +119,22 @@ public class HubAuthenticationViewModel: ObservableObject {
}
}

private func receivedExistingKey(data: Data) async {
private func receivedExistingKey(data: Data, header: [AnyHashable: Any]) async {
let privateKey: P384.KeyAgreement.PrivateKey
let jwe: JWE
let subscriptionState: HubSubscriptionState
do {
privateKey = try CryptomatorHubKeyProvider.shared.getPrivateKey()
jwe = try JWE(compactSerialization: data)
subscriptionState = try getSubscriptionState(from: header)
} catch {
await setStateToErrorState(with: error)
return
}
await delegate?.receivedExistingKey(jwe: jwe, privateKey: privateKey)
let response = HubUnlockResponse(jwe: jwe,
privateKey: privateKey,
subscriptionState: subscriptionState)
await delegate?.didSuccessfullyRemoteUnlock(response)
}

@MainActor
Expand All @@ -133,4 +145,20 @@ public class HubAuthenticationViewModel: ObservableObject {
private func setStateToErrorState(with error: Error) async {
await setState(to: .error(description: error.localizedDescription))
}

private func getSubscriptionState(from header: [AnyHashable: Any]) throws -> HubSubscriptionState {
guard let subscriptionStateValue = header[Constants.subscriptionState] as? String else {
DDLogError("Can't retrieve hub subscription state from header -> missing value")
throw HubAuthenticationViewModelError.missingSubscriptionHeader
}
switch subscriptionStateValue {
case "ACTIVE":
return .active
case "INACTIVE":
return .inactive
default:
DDLogError("Can't retrieve hub subscription state from header -> unexpected value")
throw HubAuthenticationViewModelError.unexpectedSubscriptionHeader
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Dependencies
import Foundation
import GRDB

public protocol HubRepository {
func save(_ vault: HubVault) throws
func getHubVault(vaultID: String) throws -> HubVault?
}

public struct HubVault: Equatable {
public let vaultUID: String
public let subscriptionState: HubSubscriptionState
}

private struct HubVaultRow: Codable, Equatable, PersistableRecord, FetchableRecord {
public static let databaseTableName = "hubVaultAccount"

let vaultUID: String
let subscriptionState: HubSubscriptionState

init(from vault: HubVault) {
self.vaultUID = vault.vaultUID
self.subscriptionState = vault.subscriptionState
}

func toHubVault() -> HubVault {
HubVault(vaultUID: vaultUID, subscriptionState: subscriptionState)
}

enum Columns: String, ColumnExpression {
case vaultUID, subscriptionState
}

public func encode(to container: inout PersistenceContainer) {
container[Columns.vaultUID] = vaultUID
container[Columns.subscriptionState] = subscriptionState
}
}

extension HubSubscriptionState: DatabaseValueConvertible {}

public extension DependencyValues {
var hubRepository: HubRepository {
get { self[HubRepositoryKey.self] }
set { self[HubRepositoryKey.self] = newValue }
}
}

private enum HubRepositoryKey: DependencyKey {
static var liveValue: HubRepository = HubDBRepository()
#if DEBUG
static var testValue: HubRepository = HubRepositoryMock()
#endif
}

public class HubDBRepository: HubRepository {
@Dependency(\.database) private var database

public func save(_ vault: HubVault) throws {
let row = HubVaultRow(from: vault)
try database.write { db in
try row.save(db)
}
}

public func getHubVault(vaultID: String) throws -> HubVault? {
let row = try database.read { db in
try HubVaultRow.fetchOne(db, key: vaultID)
}
return row?.toHubVault()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public enum HubSubscriptionState: String, Codable {
case active
case inactive
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AppAuthCore
import CryptoKit
import CryptomatorCloudAccessCore
import CryptomatorCryptoLib
import Dependencies
import JOSESwift
import SwiftUI
import UIKit
Expand All @@ -15,6 +16,7 @@ public final class HubXPCLoginCoordinator: Coordinator {
let hubAuthenticator: HubAuthenticating
public let onUnlocked: () -> Void
public let onErrorAlertDismissed: () -> Void
@Dependency(\.hubRepository) private var hubRepository

public init(navigationController: UINavigationController,
domain: NSFileProviderDomain,
Expand Down Expand Up @@ -42,25 +44,26 @@ public final class HubXPCLoginCoordinator: Coordinator {
}

extension HubXPCLoginCoordinator: HubAuthenticationFlowDelegate {
public func receivedExistingKey(jwe: JWE, privateKey: P384.KeyAgreement.PrivateKey) async {
public func didSuccessfullyRemoteUnlock(_ response: HubUnlockResponse) async {
let masterkey: Masterkey
do {
masterkey = try JWEHelper.decrypt(jwe: jwe, with: privateKey)
masterkey = try JWEHelper.decrypt(jwe: response.jwe, with: response.privateKey)
} catch {
handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed)
return
}
let xpc: XPC<VaultUnlocking>
do {
xpc = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain)
let xpc: XPC<VaultUnlocking> = try await fileProviderConnector.getXPC(serviceName: .vaultUnlocking, domain: domain)
defer {
fileProviderConnector.invalidateXPC(xpc)
}
try await xpc.proxy.unlockVault(rawKey: masterkey.rawKey).getValue()
fileProviderConnector.invalidateXPC(xpc)
let hubVault = HubVault(vaultUID: domain.identifier.rawValue, subscriptionState: response.subscriptionState)
try hubRepository.save(hubVault)
onUnlocked()
} catch {
handleError(error, for: navigationController, onOKTapped: onErrorAlertDismissed)
return
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

#if DEBUG

// MARK: - HubRepositoryMock -

final class HubRepositoryMock: HubRepository {
// MARK: - save

var saveThrowableError: Error?
var saveCallsCount = 0
var saveCalled: Bool {
saveCallsCount > 0
}

var saveReceivedVault: HubVault?
var saveReceivedInvocations: [HubVault] = []
var saveClosure: ((HubVault) throws -> Void)?

func save(_ vault: HubVault) throws {
if let error = saveThrowableError {
throw error
}
saveCallsCount += 1
saveReceivedVault = vault
saveReceivedInvocations.append(vault)
try saveClosure?(vault)
}

// MARK: - getHubVault

var getHubVaultVaultIDThrowableError: Error?
var getHubVaultVaultIDCallsCount = 0
var getHubVaultVaultIDCalled: Bool {
getHubVaultVaultIDCallsCount > 0
}

var getHubVaultVaultIDReceivedVaultID: String?
var getHubVaultVaultIDReceivedInvocations: [String] = []
var getHubVaultVaultIDReturnValue: HubVault?
var getHubVaultVaultIDClosure: ((String) throws -> HubVault?)?

func getHubVault(vaultID: String) throws -> HubVault? {
if let error = getHubVaultVaultIDThrowableError {
throw error
}
getHubVaultVaultIDCallsCount += 1
getHubVaultVaultIDReceivedVaultID = vaultID
getHubVaultVaultIDReceivedInvocations.append(vaultID)
return try getHubVaultVaultIDClosure.map({ try $0(vaultID) }) ?? getHubVaultVaultIDReturnValue
}
}
#endif
Loading