Skip to content

Commit

Permalink
feat: Implement Google Authenticator import (#45)
Browse files Browse the repository at this point in the history
* Added Google Auth as an import option
Update import service

* Force fail if algo contains md5

* bump package

* Fixed and added GA tests

* Fixed lint
  • Loading branch information
joeldavidw authored Jul 25, 2024
1 parent a528d47 commit c1d141e
Show file tree
Hide file tree
Showing 9 changed files with 733 additions and 128 deletions.
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

0 comments on commit c1d141e

Please sign in to comment.