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

feat: Implement Google Authenticator import #45

Merged
merged 5 commits into from
Jul 25, 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
43 changes: 42 additions & 1 deletion Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
6BC3C3B52BA6B91E00B181B9 /* BiometricsSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC3C3B42BA6B91E00B181B9 /* BiometricsSetupView.swift */; };
6BC5F0482C4F8B1B00BA106F /* Html in Frameworks */ = {isa = PBXBuildFile; productRef = 6BC5F0472C4F8B1B00BA106F /* Html */; };
6BC5F04A2C4FDE6E00BA106F /* ParseOtpAuthUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */; };
6BC5F0522C52429100BA106F /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = 6BC5F0512C52429100BA106F /* SwiftProtobuf */; };
6BC5F0552C52464600BA106F /* GoogleAuth.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5F0542C5242B500BA106F /* GoogleAuth.pb.swift */; };
6BC5F0572C529A2A00BA106F /* GoogleAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC5F0562C529A2A00BA106F /* GoogleAuthenticator.swift */; };
6BD6D2012C11FEB4004512BF /* OTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD6D2002C11FEB4004512BF /* OTPService.swift */; };
6BD90AA52B8E34BB00FABD91 /* PasswordLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */; };
6BE122922BD6413D008636D2 /* ChronosCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BE122912BD6413D008636D2 /* ChronosCrypto.swift */; };
Expand Down Expand Up @@ -139,6 +142,8 @@
6BB37D842C483066008DA122 /* ImportFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportFailureView.swift; sourceTree = "<group>"; };
6BC3C3B42BA6B91E00B181B9 /* BiometricsSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricsSetupView.swift; sourceTree = "<group>"; };
6BC5F0492C4FDE6E00BA106F /* ParseOtpAuthUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOtpAuthUrl.swift; sourceTree = "<group>"; };
6BC5F0542C5242B500BA106F /* GoogleAuth.pb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuth.pb.swift; sourceTree = "<group>"; };
6BC5F0562C529A2A00BA106F /* GoogleAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleAuthenticator.swift; sourceTree = "<group>"; };
6BD6D2002C11FEB4004512BF /* OTPService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPService.swift; sourceTree = "<group>"; };
6BD90AA42B8E34BB00FABD91 /* PasswordLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordLoginView.swift; sourceTree = "<group>"; };
6BE122912BD6413D008636D2 /* ChronosCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChronosCrypto.swift; sourceTree = "<group>"; };
Expand All @@ -162,6 +167,7 @@
6B66D5ED2B526648006DB79D /* CodeScanner in Frameworks */,
6B7383E02B9C3962008E8867 /* Factory in Frameworks */,
6B3C7A3D2BE9E0600043FEBD /* Logging in Frameworks */,
6BC5F0522C52429100BA106F /* SwiftProtobuf in Frameworks */,
6B193C8A2C27F03300E759B7 /* CloudKitSyncMonitor in Frameworks */,
6BB37D812C46C751008DA122 /* SwiftyJSON in Frameworks */,
6BF53E4F2C317AA400356461 /* ZipArchive in Frameworks */,
Expand Down Expand Up @@ -312,6 +318,7 @@
children = (
6B4CBF2E2C490FB700983D44 /* Chronos.swift */,
6B8132F12C4975DA00DB367E /* Raivo.swift */,
6BC5F0562C529A2A00BA106F /* GoogleAuthenticator.swift */,
);
path = Import;
sourceTree = "<group>";
Expand All @@ -337,6 +344,7 @@
6B7383E12B9C3975008E8867 /* Services */ = {
isa = PBXGroup;
children = (
6BC5F04D2C5240D000BA106F /* Import */,
6B7383E42B9C4230008E8867 /* Container.swift */,
6BA5DA5B2B9E94F8009908E5 /* SecureEnclaveService.swift */,
6B39629D2BF63F27000410B0 /* SwiftDataService.swift */,
Expand All @@ -345,7 +353,6 @@
6B9D74642C14ADDC008E6582 /* StateService.swift */,
6B12B09F2C19DB7800E9ED2D /* ExportService.swift */,
6B3F92AA2C1C7987004125A8 /* VaultService.swift */,
6BB37D7D2C466B60008DA122 /* ImportService.swift */,
);
path = Services;
sourceTree = "<group>";
Expand Down Expand Up @@ -404,6 +411,23 @@
path = Import;
sourceTree = "<group>";
};
6BC5F04D2C5240D000BA106F /* Import */ = {
isa = PBXGroup;
children = (
6BC5F0532C5242A900BA106F /* Protos */,
6BB37D7D2C466B60008DA122 /* ImportService.swift */,
);
path = Import;
sourceTree = "<group>";
};
6BC5F0532C5242A900BA106F /* Protos */ = {
isa = PBXGroup;
children = (
6BC5F0542C5242B500BA106F /* GoogleAuth.pb.swift */,
);
path = Protos;
sourceTree = "<group>";
};
6BD0562A2BDF64D80099616B /* Row */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -481,6 +505,7 @@
6BB37D802C46C751008DA122 /* SwiftyJSON */,
6B8132F62C4BB2B200DB367E /* EFQRCode */,
6BC5F0472C4F8B1B00BA106F /* Html */,
6BC5F0512C52429100BA106F /* SwiftProtobuf */,
);
productName = Chronos;
productReference = 6B3BB0AD2B4EA87C00DCEF0B /* Chronos.app */;
Expand Down Expand Up @@ -545,6 +570,7 @@
6BB37D7F2C46C751008DA122 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
6B8132F52C4BB2B200DB367E /* XCRemoteSwiftPackageReference "EFQRCode" */,
6BC5F0462C4F8B1B00BA106F /* XCRemoteSwiftPackageReference "swift-html" */,
6BC5F0502C52429100BA106F /* XCRemoteSwiftPackageReference "swift-protobuf" */,
);
productRefGroup = 6B3BB0AE2B4EA87C00DCEF0B /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -646,6 +672,7 @@
6BB37D7A2C456B32008DA122 /* ImportSourceListView.swift in Sources */,
6B3962A02BF6423B000410B0 /* CryptoService.swift in Sources */,
6B27317A2B53E23800F30621 /* UpdateTokenView.swift in Sources */,
6BC5F0552C52464600BA106F /* GoogleAuth.pb.swift in Sources */,
6B27317C2B53F0B200F30621 /* TokenRowView.swift in Sources */,
6B8133002C4E634100DB367E /* PasswordReminder.swift in Sources */,
);
Expand All @@ -657,6 +684,7 @@
files = (
6B8132FD2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift in Sources */,
6B4CBF2F2C490FB700983D44 /* Chronos.swift in Sources */,
6BC5F0572C529A2A00BA106F /* GoogleAuthenticator.swift in Sources */,
6BC5F04A2C4FDE6E00BA106F /* ParseOtpAuthUrl.swift in Sources */,
6B8132F22C4975DA00DB367E /* Raivo.swift in Sources */,
6B8132FA2C4C0F6300DB367E /* TokenToOtpAuthUrl.swift in Sources */,
Expand Down Expand Up @@ -1338,6 +1366,14 @@
minimumVersion = 0.4.1;
};
};
6BC5F0502C52429100BA106F /* XCRemoteSwiftPackageReference "swift-protobuf" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-protobuf.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.27.1;
};
};
6BF53E4B2C31793100356461 /* XCRemoteSwiftPackageReference "ZipArchive" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ZipArchive/ZipArchive";
Expand Down Expand Up @@ -1404,6 +1440,11 @@
package = 6BC5F0462C4F8B1B00BA106F /* XCRemoteSwiftPackageReference "swift-html" */;
productName = Html;
};
6BC5F0512C52429100BA106F /* SwiftProtobuf */ = {
isa = XCSwiftPackageProductDependency;
package = 6BC5F0502C52429100BA106F /* XCRemoteSwiftPackageReference "swift-protobuf" */;
productName = SwiftProtobuf;
};
6BF53E4E2C317AA400356461 /* ZipArchive */ = {
isa = XCSwiftPackageProductDependency;
package = 6BF53E4B2C31793100356461 /* XCRemoteSwiftPackageReference "ZipArchive" */;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "6875bdc952d508b7fe3f1a37ca1cf2a25b17288f298b731f7dc79f78e0a0b874",
"originHash" : "19b35a59954c273e4fee0aed72f9d6d02d424ede42e3faace584d56d8e92d701",
"pins" : [
{
"identity" : "alertkit",
Expand Down Expand Up @@ -82,6 +82,15 @@
"version" : "1.6.1"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "e17d61f26df0f0e06f58f6977ba05a097a720106",
"version" : "1.27.1"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
Expand All @@ -96,8 +105,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax",
"state" : {
"revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c",
"version" : "600.0.0-prerelease-2024-06-12"
"revision" : "82a453c2dfa335c7e778695762438dfe72b328d2",
"version" : "600.0.0-prerelease-2024-07-24"
}
},
{
Expand Down
1 change: 0 additions & 1 deletion Chronos/App/MainAppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ struct MainAppView: View {
}

if biometricsEnabled && statePasswordReminderEnabled {
print("Fuck \(nextPasswordReminderTimestamp)")
if Date().timeIntervalSince1970 >= nextPasswordReminderTimestamp {
showPasswordReminder = true
}
Expand Down
105 changes: 84 additions & 21 deletions Chronos/App/Tabs/Settings/Import/ImportSourceDetailView.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import AlertKit
import CodeScanner
import Factory
import SwiftUI

class ImportSourceDetailViewModel: ObservableObject {
@Published var tokens: [Token]?
private let importService = Container.shared.importService()

func importTokensFromFile(importSource: ImportSource, fileUrl: URL) {
tokens = importService.importTokensViaFile(importSource: importSource, url: fileUrl)
}

func importTokensFromString(importSource: ImportSource, scannedStr: String) {
tokens = importService.importTokensViaString(importSource: importSource, scannedStr: scannedStr)
}
}

struct ImportSourceDetailView: View {
@State var importSource: ImportSource

@State private var showFileImporter = false
@State private var showImportConfirmation = false
@State private var tokens: [Token]?
@State private var unableToAccessCamera = false

@EnvironmentObject private var importNav: ExportNavigation

private let importService = Container.shared.importService()
@StateObject private var viewModel = ImportSourceDetailViewModel()

var body: some View {
VStack {
Expand All @@ -23,26 +38,52 @@ struct ImportSourceDetailView: View {

Spacer()

Button(action: { showFileImporter = true }) {
Text("Select file")
.bold()
.frame(maxWidth: .infinity)
.frame(height: 32)
if importSource.importType == .JSON {
Button(action: { showFileImporter = true }) {
Text("Select file")
.bold()
.frame(maxWidth: .infinity)
.frame(height: 32)
}
.buttonStyle(.bordered)
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: [.json],
allowsMultipleSelection: false,
onCompletion: handleFileImport
)
}
.buttonStyle(.bordered)
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: [.json],
allowsMultipleSelection: false,
onCompletion: handleFileImport
)
.sheet(isPresented: $showImportConfirmation) {
NavigationStack {
if let tokens = tokens {
ImportConfirmationView(tokens: tokens)
} else {
ImportFailureView()

if importSource.importType == .IMAGE {
if !unableToAccessCamera {
CodeScannerView(
codeTypes: [.qr],
scanMode: .once,
scanInterval: 0.1,
shouldVibrateOnSuccess: false,
completion: handleScan
)
.frame(height: 200, alignment: .center)
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(8)
} else {
VStack {
Spacer()
Text("Chronos requires camera access to scan 2FA QR codes")
.foregroundStyle(.white)
.multilineTextAlignment(.center)
.bold()
.padding(.horizontal, 16)
Button("Open settings") {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
}
.padding(.top, 4)
Spacer()
}
.frame(height: 200, alignment: .center)
.frame(minWidth: 0, maxWidth: .infinity)
.background(.black)
.cornerRadius(8)
}
}

Expand All @@ -53,21 +94,43 @@ struct ImportSourceDetailView: View {
.frame(height: 32)
}
.buttonStyle(.borderless)
.padding(.top, 8)
}
.padding(.horizontal, 24)
.padding(.bottom, 32)
.navigationTitle("Import from \(importSource.name)")
.sheet(isPresented: $showImportConfirmation) {
NavigationStack {
if let tokens = viewModel.tokens {
ImportConfirmationView(tokens: tokens)
} else {
ImportFailureView()
}
}
}
}

private func handleFileImport(result: Result<[URL], Error>) {
switch result {
case let .success(fileUrls):
if let fileUrl = fileUrls.first {
tokens = importService.importTokens(importSource: importSource, url: fileUrl)
viewModel.importTokensFromFile(importSource: importSource, fileUrl: fileUrl)
showImportConfirmation = true
}
case let .failure(error):
print(error)
}
}

private func handleScan(result: Result<ScanResult, ScanError>) {
switch result {
case let .success(result):
viewModel.importTokensFromString(importSource: importSource, scannedStr: result.string)
showImportConfirmation = true
case .failure:
DispatchQueue.main.async {
unableToAccessCamera = true
}
}
}
}
19 changes: 16 additions & 3 deletions Chronos/App/Tabs/Settings/Import/ImportSourceListView.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import SwiftUI

enum ImportSourceId {
case CHRONOS
case RAIVO
case GOOGLE_AUTHENTICATOR
}

enum ImportType {
case JSON
case IMAGE
}

struct ImportSource: Identifiable {
var id: String
var id: ImportSourceId
var name: String
var desc: String
var importType: ImportType
}

struct ImportSourceListView: View {
let importSources: [ImportSource] = [
ImportSource(id: "chronos", name: "Chronos", desc: "Export your tokens from Chronos to an unencrypted JSON file, then select the file below."),
ImportSource(id: "raivo", name: "Raivo", desc: "Export your tokens from Raivo using \"Export OTPs to ZIP archive\" option. Extract the JSON file from the archive, then select the file below."),
ImportSource(id: .CHRONOS, name: "Chronos", desc: "Export your tokens from Chronos to an unencrypted JSON file, then select the file below.", importType: .JSON),
ImportSource(id: .RAIVO, name: "Raivo", desc: "Export your tokens from Raivo using \"Export OTPs to ZIP archive\" option. Extract the JSON file from the archive, then select the file below.", importType: .JSON),
ImportSource(id: .GOOGLE_AUTHENTICATOR, name: "Google Authenticator", desc: "Export your tokens from Google Authenticator using the \"Transfer accounts\" option. Scan the QR code.", importType: .IMAGE),
]

@EnvironmentObject var importNav: ExportNavigation
Expand Down
Loading