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

Allow user to import items from JSON exports #56

Merged
merged 18 commits into from
May 3, 2024
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import Foundation

extension Sequence {
/// Performs an operation on each element of a sequence.
/// Each operation is performed serially in order.
///
/// - Parameters:
/// - operation: The closure to run on each element.
///
func asyncForEach(
_ operation: (Element) async throws -> Void
) async rethrows {
for element in self {
try await operation(element)
}
}

/// Maps the elements of an array with an async Transform.
///
/// - Parameter transform: An asynchronous function mapping the sequence element.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public class ServiceContainer: Services {
/// The service used to export items.
let exportItemsService: ExportItemsService

/// The service used to import items.
let importItemsService: ImportItemsService

/// The service used to perform app data migrations.
let migrationService: MigrationService

Expand Down Expand Up @@ -76,6 +79,7 @@ public class ServiceContainer: Services {
/// - cryptographyService: The service used by the application to encrypt and decrypt items
/// - errorReporter: The service used by the application to report non-fatal errors.
/// - exportItemsService: The service to export items.
/// - importItemsService: The service to import items.
/// - migrationService: The service to do data migrations
/// - pasteboardService: The service used by the application for sharing data with other apps.
/// - stateService: The service for managing account state.
Expand All @@ -93,6 +97,7 @@ public class ServiceContainer: Services {
clientService: ClientService,
errorReporter: ErrorReporter,
exportItemsService: ExportItemsService,
importItemsService: ImportItemsService,
migrationService: MigrationService,
pasteboardService: PasteboardService,
stateService: StateService,
Expand All @@ -109,6 +114,7 @@ public class ServiceContainer: Services {
self.cryptographyService = cryptographyService
self.errorReporter = errorReporter
self.exportItemsService = exportItemsService
self.importItemsService = importItemsService
self.migrationService = migrationService
self.pasteboardService = pasteboardService
self.timeProvider = timeProvider
Expand Down Expand Up @@ -194,6 +200,11 @@ public class ServiceContainer: Services {
timeProvider: timeProvider
)

let importItemsService = DefaultImportItemsService(
authenticatorItemRepository: authenticatorItemRepository,
errorReporter: errorReporter
)

self.init(
application: application,
appSettingsStore: appSettingsStore,
Expand All @@ -205,6 +216,7 @@ public class ServiceContainer: Services {
clientService: clientService,
errorReporter: errorReporter,
exportItemsService: exportItemsService,
importItemsService: importItemsService,
migrationService: migrationService,
pasteboardService: pasteboardService,
stateService: stateService,
Expand Down
8 changes: 8 additions & 0 deletions AuthenticatorShared/Core/Platform/Services/Services.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ typealias Services = HasAuthenticatorItemRepository
& HasCryptographyService
& HasErrorReporter
& HasExportItemsService
& HasImportItemsService
& HasPasteboardService
& HasStateService
& HasTOTPService
Expand Down Expand Up @@ -54,6 +55,13 @@ protocol HasExportItemsService {
var exportItemsService: ExportItemsService { get }
}

/// Protocol for an object that provides an `ImportItemsService`.
///
protocol HasImportItemsService {
/// The service used to import items.
var importItemsService: ImportItemsService { get }
}

/// Protocol for an object that provides a `PasteboardService`.
///
protocol HasPasteboardService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ extension ServiceContainer {
cryptographyService: CryptographyService = MockCryptographyService(),
errorReporter: ErrorReporter = MockErrorReporter(),
exportItemsService: ExportItemsService = MockExportItemsService(),
importItemsService: ImportItemsService = MockImportItemsService(),
migrationService: MigrationService = MockMigrationService(),
pasteboardService: PasteboardService = MockPasteboardService(),
stateService: StateService = MockStateService(),
Expand All @@ -32,6 +33,7 @@ extension ServiceContainer {
clientService: clientService,
errorReporter: errorReporter,
exportItemsService: exportItemsService,
importItemsService: importItemsService,
migrationService: migrationService,
pasteboardService: pasteboardService,
stateService: stateService,
Expand Down
15 changes: 15 additions & 0 deletions AuthenticatorShared/Core/Platform/Utilities/Constants.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

typealias ClientType = String
typealias DeviceType = Int

// MARK: - Constants

/// Constant values reused throughout the app.
///
enum Constants {
// MARK: Static Properties

/// The default file name when the file name cannot be determined.
static let unknownFileName = "unknown_file_name"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// MARK: - ImportFileType

/// An enum describing the format of an import file.
///
public enum ImportFileType: Equatable {
/// A `.json` file type.
case json

/// The file extension type to use.
var fileExtension: String {
switch self {
case .json:
"json"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// MARK: - ImportFormatType

/// An enum describing the format of the items item.
///
enum ImportFormatType: Menuable {
/// A JSON exported from Bitwarden
case bitwardenJson

// MARK: Type Properties

/// The ordered list of options to display in the menu.
static let allCases: [ImportFormatType] = [.bitwardenJson]

// MARK: Properties

/// The name of the type to display in the dropdown menu.
var localizedName: String {
switch self {
case .bitwardenJson:
"Authenticator Export (JSON)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ class DefaultExportItemsService: ExportItemsService {
///
/// - Parameters:
/// - authenticatorItemRepository: The service for getting items.
/// - cryptographyService: The service for cryptography tasks.
/// - errorReporter: The service for handling errors.
/// - timeProvider: The provider for current time, used in file naming and data timestamps.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation

// MARK: - ImportItemsService

/// A service to import items from a file.
///
protocol ImportItemsService: AnyObject {
/// Import items with a given format.
///
/// - Parameters:
/// - data: The data to import.
/// - format: The format of the file to import.
///
func importItems(data: Data, format: ImportFileType) async throws
}

extension ImportItemsService {
/// Import items with a given format.
///
/// - Parameters:
/// - url: The URL of the file to import.
/// - format: The format of the file to import.
///
func importItems(url: URL, format: ImportFileType) async throws {
let data = try Data(contentsOf: url)
try await importItems(data: data, format: format)
}
}

class DefaultImportItemsService: ImportItemsService {
// MARK: Properties

/// The item service.
private let authenticatorItemRepository: AuthenticatorItemRepository

/// The error reporter used by this service.
private let errorReporter: ErrorReporter

// MARK: Initilzation

/// Initializes a new instance of a `DefaultExportItemsService`.
///
/// This service handles exporting items from local storage into a file.
///
/// - Parameters:
/// - authenticatorItemRepository: The service for storing items.
/// - errorReporter: The service for handling errors.
///
init(
authenticatorItemRepository: AuthenticatorItemRepository,
errorReporter: ErrorReporter
) {
self.authenticatorItemRepository = authenticatorItemRepository
self.errorReporter = errorReporter
}

// MARK: Methods

func importItems(data: Data, format: ImportFileType) async throws {
let items: [CipherLike]
switch format {
case .json:
items = try importJson(data)
}
try await items.asyncForEach { cipherLike in
let item = AuthenticatorItemView(
favorite: cipherLike.favorite,
id: cipherLike.id,
name: cipherLike.name,
totpKey: cipherLike.login?.totp,
username: cipherLike.login?.username
)
try await authenticatorItemRepository.addAuthenticatorItem(item)
}
}

// MARK: Private Methods

private func importJson(_ data: Data) throws -> [CipherLike] {
let decoder = JSONDecoder()
let vaultLike = try decoder.decode(VaultLike.self, from: data)
return vaultLike.items
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

@testable import AuthenticatorShared

class MockImportItemsService: ImportItemsService {
var importItemsData: Data?
var importItemsUrl: URL?
var importItemsFormat: ImportFileType?

func importItems(data: Data, format: ImportFileType) async throws {
importItemsData = data
importItemsFormat = format
}

func importItems(url: URL, format: ImportFileType) async throws {
importItemsUrl = url
importItemsFormat = format
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,9 @@
"KeyReadError" = "Cannot read key.";
"CannotAddKey" = "Cannot add key?";
"OnceTheKeyIsSuccessfullyEnteredAddCode" = "Once the key is successfully entered,\nselect Add code to store the key safely";
"Import" = "Import";
"Steam" = "Steam";
"OtpType" = "OTP Type";
"CreateVerificationCode" = "Create verification code";
"ItemsExported" = "Verification codes exported";
"ItemsImported" = "Vertification codes imported";
Loading
Loading