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

[ABW-1584] offDeviceMnemonic factor source kind #532

Merged
merged 21 commits into from
May 29, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ struct ImportMnemonicPreviewApp: App {
WindowGroup {
ImportMnemonic.View(
store: Store(
initialState: ImportMnemonic.State(saveInProfile: false),
reducer: ImportMnemonic()._printChanges()
initialState: ImportMnemonic.State(
saveInProfileKind: .offDevice
),
reducer: ImportMnemonic()
._printChanges()
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public struct FactorSourcesClient: Sendable {
public var factorSourcesAsyncSequence: FactorSourcesAsyncSequence
public var addPrivateHDFactorSource: AddPrivateHDFactorSource
public var checkIfHasOlympiaFactorSourceForAccounts: CheckIfHasOlympiaFactorSourceForAccounts
public var addOffDeviceFactorSource: AddOffDeviceFactorSource
public var saveFactorSource: SaveFactorSource
public var getSigningFactors: GetSigningFactors
public var updateLastUsed: UpdateLastUsed

Expand All @@ -18,7 +18,7 @@ public struct FactorSourcesClient: Sendable {
factorSourcesAsyncSequence: @escaping FactorSourcesAsyncSequence,
addPrivateHDFactorSource: @escaping AddPrivateHDFactorSource,
checkIfHasOlympiaFactorSourceForAccounts: @escaping CheckIfHasOlympiaFactorSourceForAccounts,
addOffDeviceFactorSource: @escaping AddOffDeviceFactorSource,
saveFactorSource: @escaping SaveFactorSource,
getSigningFactors: @escaping GetSigningFactors,
updateLastUsed: @escaping UpdateLastUsed
) {
Expand All @@ -27,7 +27,7 @@ public struct FactorSourcesClient: Sendable {
self.factorSourcesAsyncSequence = factorSourcesAsyncSequence
self.addPrivateHDFactorSource = addPrivateHDFactorSource
self.checkIfHasOlympiaFactorSourceForAccounts = checkIfHasOlympiaFactorSourceForAccounts
self.addOffDeviceFactorSource = addOffDeviceFactorSource
self.saveFactorSource = saveFactorSource
self.getSigningFactors = getSigningFactors
self.updateLastUsed = updateLastUsed
}
Expand All @@ -38,13 +38,24 @@ extension FactorSourcesClient {
public typealias GetCurrentNetworkID = @Sendable () async -> NetworkID
public typealias GetFactorSources = @Sendable () async throws -> FactorSources
public typealias FactorSourcesAsyncSequence = @Sendable () async -> AnyAsyncSequence<FactorSources>
public typealias AddPrivateHDFactorSource = @Sendable (PrivateHDFactorSource) async throws -> FactorSourceID
public typealias AddPrivateHDFactorSource = @Sendable (AddPrivateHDFactorSourceRequest) async throws -> FactorSourceID
public typealias CheckIfHasOlympiaFactorSourceForAccounts = @Sendable (NonEmpty<OrderedSet<OlympiaAccountToMigrate>>) async -> FactorSourceID?
public typealias AddOffDeviceFactorSource = @Sendable (FactorSource) async throws -> Void
public typealias SaveFactorSource = @Sendable (FactorSource) async throws -> Void
public typealias GetSigningFactors = @Sendable (GetSigningFactorsRequest) async throws -> SigningFactors
public typealias UpdateLastUsed = @Sendable (UpdateFactorSourceLastUsedRequest) async throws -> Void
}

// MARK: - AddPrivateHDFactorSourceRequest
public struct AddPrivateHDFactorSourceRequest: Sendable, Hashable {
public let privateFactorSource: PrivateHDFactorSource
/// E.g. import babylon factor sources should only be saved keychain, not profile (already there).
public let saveIntoProfile: Bool
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved
public init(privateFactorSource: PrivateHDFactorSource, saveIntoProfile: Bool) {
self.privateFactorSource = privateFactorSource
self.saveIntoProfile = saveIntoProfile
}
}

public typealias SigningFactors = OrderedDictionary<FactorSourceKind, NonEmpty<Set<SigningFactor>>>

extension SigningFactors {
Expand Down Expand Up @@ -124,6 +135,17 @@ public struct UpdateFactorSourceLastUsedRequest: Sendable, Hashable {
}
}

// MARK: - MnemonicBasedFactorSourceKind
public enum MnemonicBasedFactorSourceKind: Sendable, Hashable {
public enum OnDeviceMnemonicKind: Sendable, Hashable {
case babylon
case olympia
}

case onDevice(OnDeviceMnemonicKind)
case offDevice
}

// MARK: - SigningFactor
public struct SigningFactor: Sendable, Hashable, Identifiable {
public typealias ID = FactorSource.ID
Expand Down Expand Up @@ -155,17 +177,39 @@ public struct SigningFactor: Sendable, Hashable, Identifiable {
}

extension FactorSourcesClient {
public func importOlympiaFactorSource(
public func addOffDeviceFactorSource(
mnemonicWithPassphrase: MnemonicWithPassphrase,
label: FactorSource.Label,
description: FactorSource.Description
) async throws -> FactorSourceID {
let privateFactorSource = try FactorSource.offDeviceMnemonic(
withPassphrase: mnemonicWithPassphrase,
label: label,
description: description
)
return try await addPrivateHDFactorSource(.init(
privateFactorSource: privateFactorSource,
saveIntoProfile: true
))
}

public func addOnDeviceFactorSource(
onDeviceMnemonicKind: MnemonicBasedFactorSourceKind.OnDeviceMnemonicKind,
mnemonicWithPassphrase: MnemonicWithPassphrase
) async throws -> FactorSourceID {
let factorSource = try FactorSource.olympia(
let isOlympia = onDeviceMnemonicKind == .olympia
let hdOnDeviceFactorSource: HDOnDeviceFactorSource = isOlympia ? try FactorSource.olympia(
mnemonicWithPassphrase: mnemonicWithPassphrase
)
) : try FactorSource.babylon(mnemonicWithPassphrase: mnemonicWithPassphrase).hdOnDeviceFactorSource

let privateFactorSource = try PrivateHDFactorSource(
mnemonicWithPassphrase: mnemonicWithPassphrase,
hdOnDeviceFactorSource: factorSource
factorSource: hdOnDeviceFactorSource.factorSource
)
return try await self.addPrivateHDFactorSource(privateFactorSource)
return try await addPrivateHDFactorSource(.init(
privateFactorSource: privateFactorSource,
saveIntoProfile: isOlympia
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved
))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension FactorSourcesClient: TestDependencyKey {
factorSourcesAsyncSequence: unimplemented("\(Self.self).factorSourcesAsyncSequence"),
addPrivateHDFactorSource: unimplemented("\(Self.self).addPrivateHDFactorSource"),
checkIfHasOlympiaFactorSourceForAccounts: unimplemented("\(Self.self).checkIfHasOlympiaFactorSourceForAccounts"),
addOffDeviceFactorSource: unimplemented("\(Self.self).addOffDeviceFactorSource"),
saveFactorSource: unimplemented("\(Self.self).saveFactorSource"),
getSigningFactors: unimplemented("\(Self.self).getSigningFactors"),
updateLastUsed: unimplemented("\(Self.self).updateLastUsed")
)
Expand All @@ -27,7 +27,7 @@ extension FactorSourcesClient: TestDependencyKey {
factorSourcesAsyncSequence: { AsyncLazySequence([]).eraseToAnyAsyncSequence() },
addPrivateHDFactorSource: { _ in throw NoopError() },
checkIfHasOlympiaFactorSourceForAccounts: { _ in nil },
addOffDeviceFactorSource: { _ in },
saveFactorSource: { _ in },
getSigningFactors: { _ in throw NoopError() },
updateLastUsed: { _ in }
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension FactorSourcesClient: DependencyKey {
await getProfileStore().profile.factorSources
}

let addOffDeviceFactorSource: AddOffDeviceFactorSource = { source in
let saveFactorSource: SaveFactorSource = { source in
try await getProfileStore().updating { profile in
guard !profile.factorSources.contains(where: { $0.id == source.id }) else {
throw FactorSourceAlreadyPresent()
Expand All @@ -30,17 +30,27 @@ extension FactorSourcesClient: DependencyKey {
factorSourcesAsyncSequence: {
await getProfileStore().factorSourcesValues()
},
addPrivateHDFactorSource: { privateFactorSource in

try await secureStorageClient.saveMnemonicForFactorSource(privateFactorSource)
let factorSourceID = privateFactorSource.hdOnDeviceFactorSource.factorSource.id
do {
try await addOffDeviceFactorSource(privateFactorSource.hdOnDeviceFactorSource.factorSource)
} catch {
// We were unlucky, failed to update Profile, thus best to undo the saving of
// the mnemonic in keychain (if we can).
try? await secureStorageClient.deleteMnemonicByFactorSourceID(factorSourceID)
throw error
addPrivateHDFactorSource: { request in
let privateFactorSource = request.privateFactorSource
if privateFactorSource.kind == .device {
try await secureStorageClient.saveMnemonicForFactorSource(privateFactorSource)
}
let factorSourceID = privateFactorSource.id

/// We only need to save olympia mnemonics into Profile, the Babylon ones
/// already exist in profile, and this function is used only to save the
/// imported mnemonic into keychain (done above).
if request.saveIntoProfile {
do {
try await saveFactorSource(privateFactorSource.factorSource)
} catch {
if privateFactorSource.kind == .device {
// We were unlucky, failed to update Profile, thus best to undo the saving of
// the mnemonic in keychain (if we can).
try? await secureStorageClient.deleteMnemonicByFactorSourceID(factorSourceID)
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved
}
throw error
}
}

return factorSourceID
Expand Down Expand Up @@ -75,7 +85,7 @@ extension FactorSourcesClient: DependencyKey {
return nil // failed? to find any Olympia `.device` factor sources
}
},
addOffDeviceFactorSource: addOffDeviceFactorSource,
saveFactorSource: saveFactorSource,
getSigningFactors: { request in
assert(request.signers.allSatisfy { $0.networkID == request.networkID })
return try await signingFactors(
Expand Down Expand Up @@ -174,11 +184,12 @@ extension FactorSourceKind: Comparable {
fileprivate var signingOrder: Int {
switch self {
case .ledgerHQHardwareWallet: return 0
case .device:
// we want to sign with device last, since it would allow for us to stop using
// ephemeral notary and allow us to implement a AutoPurgingMnemonicCache which
// deletes items after 1 sec, thus `device` must come last.
return 1
case .offDeviceMnemonic: return 1

// we want to sign with device last, since it would allow for us to stop using
// ephemeral notary and allow us to implement a AutoPurgingMnemonicCache which
// deletes items after 1 sec, thus `device` must come last.
case .device: return 2
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ extension ImportLegacyWalletClient: DependencyKey {
migrateOlympiaSoftwareAccountsToBabylon: { request in

let olympiaFactorSource = request.olympiaFactorSource
let factorSource = olympiaFactorSource?.hdOnDeviceFactorSource
let factorSource = olympiaFactorSource?.factorSource

let (accounts, networkID) = try await migrate(
accounts: request.olympiaAccounts,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Clients/ProfileStore/ProfileStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ extension ProfileStore {
// profile, since it contains no network yet (no account).
try await secureStorageClient.saveMnemonicForFactorSource(PrivateHDFactorSource(
mnemonicWithPassphrase: mnemonicWithPassphrase,
hdOnDeviceFactorSource: factorSource.hdOnDeviceFactorSource
factorSource: factorSource.factorSource
))

loggerGlobal.debug("Created new profile with factorSourceID: \(factorSource.id)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ extension SecureStorageClient: DependencyKey {
},
loadProfileSnapshotData: loadProfileSnapshotData,
saveMnemonicForFactorSource: { privateFactorSource in
let factorSource = privateFactorSource.hdOnDeviceFactorSource.factorSource
let factorSource = privateFactorSource.factorSource
let mnemonicWithPassphrase = privateFactorSource.mnemonicWithPassphrase
let data = try jsonEncoder().encode(mnemonicWithPassphrase)
let mostSecureAccesibilityAndAuthenticationPolicy = try await queryMostSecureAccesibilityAndAuthenticationPolicy()
Expand Down
9 changes: 5 additions & 4 deletions Sources/Cryptography/Mnemonic/BIP39/BIP39+WordList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,18 @@ extension BIP39.WordList {
return .unknown(.tooShort)
}

let arrayOfCandidates = words.filter { $0.word.starts(with: string) }
let setOfCandidates = OrderedSet<BIP39.Word>.init(uncheckedUniqueElements: arrayOfCandidates)

guard
case let _arrayOfCandidates = words.filter({ $0.word.starts(with: string) }),
case let _setOfCandidates = OrderedSet<BIP39.Word>.init(uncheckedUniqueElements: _arrayOfCandidates),
let candidates = NonEmpty<OrderedSet<BIP39.Word>>(rawValue: _setOfCandidates)
let candidates = NonEmpty<OrderedSet<BIP39.Word>>(rawValue: setOfCandidates)
else {
if string.count >= language.numberOfCharactersWhichUnambiguouslyIdentifiesWords {
return .unknown(.notInList(input: string))
} else if string.count >= minLengthForCandidatesLookup {
return .unknown(.tooShort)
} else {
assertionFailure("what is this case?")
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved
// e.g. "x" which no word starts with in English, yielding no candidates.
return .unknown(.notInList(input: string))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public struct AddLedgerFactorSource: Sendable, FeatureReducer {

private func completeWithLedgerEffect(_ ledger: LedgerFactorSource) -> EffectTask<Action> {
.run { send in
try await factorSourcesClient.addOffDeviceFactorSource(ledger.factorSource)
try await factorSourcesClient.saveFactorSource(ledger.factorSource)
loggerGlobal.notice("Added Ledger factor source! ✅ ")
await send(.delegate(.completed(ledger)))
} catch: { error, _ in
Expand Down
64 changes: 62 additions & 2 deletions Sources/Features/DebugInspectProfile/InspectProfile+View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ extension ProfileView {
public var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: indentation.spacing) {
Labeled("Version", value: String(describing: profile.version))
HeaderView(
header: profile.header,
indentation: inOneLevel
)

PerNetworkView(
networks: profile.networks,
Expand Down Expand Up @@ -88,6 +91,49 @@ extension IndentedView {
}
}

// MARK: - HeaderView
public struct HeaderView: IndentedView {
public let header: ProfileSnapshot.Header
public let indentation: Indentation

public var body: some View {
VStack(alignment: .leading, spacing: indentation.spacing) {
Labeled("ID", value: header.id)
Labeled("Snapshot version", value: header.snapshotVersion)
CreatingDeviceView(device: header.creatingDevice, indentation: inOneLevel)
HeaderHintView(hint: header.contentHint, indentation: inOneLevel)
}
}
}

// MARK: - CreatingDeviceView
public struct CreatingDeviceView: IndentedView {
public let device: ProfileSnapshot.Header.UsedDeviceInfo
public let indentation: Indentation

public var body: some View {
VStack(alignment: .leading, spacing: indentation.spacing) {
Labeled("Device ID", value: device.id)
Labeled("Creation date", value: device.date.ISO8601Format())
Labeled("Device", value: device.description.rawValue)
Comment on lines +116 to +118
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add FIXME: Strings in every file where strings are added...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Debug Inspect Profile will never ever reach end user :)

}
}
}

// MARK: - HeaderHintView
public struct HeaderHintView: IndentedView {
public let hint: ProfileSnapshot.Header.ContentHint
public let indentation: Indentation

public var body: some View {
VStack(alignment: .leading, spacing: indentation.spacing) {
Labeled("#Networks", value: hint.numberOfNetworks)
Labeled("#Accounts", value: hint.numberOfAccountsOnAllNetworksInTotal)
Labeled("#Personas", value: hint.numberOfPersonasOnAllNetworksInTotal)
}
}
}

// MARK: - FactorSourcesView
public struct FactorSourcesView: IndentedView {
public let factorSources: FactorSources
Expand Down Expand Up @@ -137,7 +183,17 @@ extension FactorSourceView {
Labeled("ID", value: String(factorSource.id.hexCodable.hex().mask(showLast: 6)))

if let entityCreatingStorage = factorSource.storage?.entityCreating {
NextDerivationIndicesPerNetworkView(nextDerivationIndicesPerNetwork: entityCreatingStorage.nextDerivationIndicesPerNetwork, indentation: indentation.inOneLevel)
NextDerivationIndicesPerNetworkView(
nextDerivationIndicesPerNetwork: entityCreatingStorage.nextDerivationIndicesPerNetwork,
indentation: indentation.inOneLevel
)
} else if let offDeviceMnemonic = factorSource.storage?.offDeviceMnemonic {
VStack {
Labeled("Word count", value: offDeviceMnemonic.wordCount.rawValue)
Labeled("Language", value: offDeviceMnemonic.language)
Labeled("Passphrase?", value: offDeviceMnemonic.usedBip39Passphrase)
}
.padding([.leading], indentation.inOneLevel.leadingPadding)
}
}
.background {
Expand Down Expand Up @@ -656,6 +712,10 @@ public struct Labeled: SwiftUI.View {
self.value = value
}

public init<Value>(_ label: String, value: Value) where Value: CustomStringConvertible {
self.init(label, value: String(describing: value))
}

public var body: some View {
HStack(alignment: .top) {
Text(label)
Expand Down
Loading