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: implemented ente with tests #61

Merged
merged 1 commit into from
Aug 7, 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
4 changes: 4 additions & 0 deletions Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
6B27317A2B53E23800F30621 /* UpdateTokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2731792B53E23800F30621 /* UpdateTokenView.swift */; };
6B27317C2B53F0B200F30621 /* TokenRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B27317B2B53F0B200F30621 /* TokenRowView.swift */; };
6B2F7A822C635B5A00DB1450 /* 2FAS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2F7A812C635B5A00DB1450 /* 2FAS.swift */; };
6B2F7A842C636CE500DB1450 /* Ente.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2F7A832C636CE500DB1450 /* Ente.swift */; };
6B35A4C92B557EE50004D4C5 /* AlertKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6B35A4C82B557EE50004D4C5 /* AlertKit */; };
6B39629A2BF5E935000410B0 /* MainAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3962992BF5E935000410B0 /* MainAppView.swift */; };
6B39629C2BF5EB3E000410B0 /* AuthenticationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B39629B2BF5EB3E000410B0 /* AuthenticationView.swift */; };
Expand Down Expand Up @@ -102,6 +103,7 @@
6B2731792B53E23800F30621 /* UpdateTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTokenView.swift; sourceTree = "<group>"; };
6B27317B2B53F0B200F30621 /* TokenRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRowView.swift; sourceTree = "<group>"; };
6B2F7A812C635B5A00DB1450 /* 2FAS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = 2FAS.swift; sourceTree = "<group>"; };
6B2F7A832C636CE500DB1450 /* Ente.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ente.swift; sourceTree = "<group>"; };
6B3962992BF5E935000410B0 /* MainAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppView.swift; sourceTree = "<group>"; };
6B39629B2BF5EB3E000410B0 /* AuthenticationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationView.swift; sourceTree = "<group>"; };
6B39629D2BF63F27000410B0 /* SwiftDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDataService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -328,6 +330,7 @@
6BBF32022C562F20003CBA66 /* Aegis.swift */,
6B4987272C569A6E00A7D97A /* LastPass.swift */,
6B2F7A812C635B5A00DB1450 /* 2FAS.swift */,
6B2F7A832C636CE500DB1450 /* Ente.swift */,
);
path = Import;
sourceTree = "<group>";
Expand Down Expand Up @@ -694,6 +697,7 @@
6B8132FD2C4E5F6B00DB367E /* QrCodeGenerationAndParsing.swift in Sources */,
6BBF32032C562F20003CBA66 /* Aegis.swift in Sources */,
6B4CBF2F2C490FB700983D44 /* Chronos.swift in Sources */,
6B2F7A842C636CE500DB1450 /* Ente.swift in Sources */,
6BC5F0572C529A2A00BA106F /* GoogleAuthenticator.swift in Sources */,
6B4987282C569A6E00A7D97A /* LastPass.swift in Sources */,
6BC5F04A2C4FDE6E00BA106F /* ParseOtpAuthUrl.swift in Sources */,
Expand Down
24 changes: 23 additions & 1 deletion Chronos/App/Tabs/Settings/Import/ImportSourceDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ class ImportSourceDetailViewModel: ObservableObject {
private let importService = Container.shared.importService()

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

if importSource.importType == .TEXT {
tokens = importService.importTokensViaTextFile(importSource: importSource, url: fileUrl)
}
}

func importTokensFromString(importSource: ImportSource, scannedStr: String) {
Expand Down Expand Up @@ -54,6 +60,22 @@ struct ImportSourceDetailView: View {
)
}

if importSource.importType == .TEXT {
Button(action: { showFileImporter = true }) {
Text("Select file")
.bold()
.frame(maxWidth: .infinity)
.frame(height: 32)
}
.buttonStyle(.bordered)
.fileImporter(
isPresented: $showFileImporter,
allowedContentTypes: [.text],
allowsMultipleSelection: false,
onCompletion: handleFileImport
)
}

if importSource.importType == .IMAGE {
if !unableToAccessCamera {
CodeScannerView(
Expand Down
3 changes: 3 additions & 0 deletions Chronos/App/Tabs/Settings/Import/ImportSourceListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ enum ImportSourceId {
case CHRONOS
case TWOFAS
case AEGIS
case ENTE
case RAIVO
case GOOGLE_AUTHENTICATOR
case LASTPASS
}

enum ImportType {
case JSON
case TEXT
case IMAGE
}

Expand All @@ -26,6 +28,7 @@ struct ImportSourceListView: View {
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: .TWOFAS, name: "2FAS Authenticator", desc: "Export your tokens from 2FAS Authenticator using the \"Export\" option. Make sure \"Set a password for this backup file\" is unselected, then select the file below.", importType: .JSON),
ImportSource(id: .AEGIS, name: "Aegis", desc: "Export your tokens from Aegis using \"Export\" option. Select \"JSON\" as the export format and unselect \"Encrypt the vault\", then select the file below.", importType: .JSON),
ImportSource(id: .ENTE, name: "Ente Authenticator", desc: "Export your tokens from Aegis using \"Export codes\" option. Select \"Plain text\" as the export format, then select the file below.", importType: .TEXT),
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),
ImportSource(id: .LASTPASS, name: "LastPass Authenticator", desc: "Export your tokens from LastPass Authenticator using the \"Export accounts to file\" option, then select the file below.", importType: .JSON),
Expand Down
71 changes: 64 additions & 7 deletions Chronos/Services/Import/ImportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,39 @@ import SwiftyJSON
public class ImportService {
private let logger = Logger(label: "ImportService")
private let vaultService = Container.shared.vaultService()
private let otpService = Container.shared.otpService()

func importTokensViaFile(importSource: ImportSource, url: URL) -> [Token]? {
guard let json = readFile(url: url) else {
func importTokensViaJsonFile(importSource: ImportSource, url: URL) -> [Token]? {
guard let inputJson = readJsonFile(url: url) else {
logger.error("Failed to read file at \(url)")
return nil
}

switch importSource.id {
case .CHRONOS:
return importFromChronos(json: json)
return importFromChronos(json: inputJson)
case .TWOFAS:
return importFrom2FAS(json: inputJson)
case .AEGIS:
return importFromAegis(json: json)
return importFromAegis(json: inputJson)
case .RAIVO:
return importFromRaivo(json: json)
return importFromRaivo(json: inputJson)
case .LASTPASS:
return importFromLastpass(json: json)
return importFromLastpass(json: inputJson)
default:
return nil
}
}

func importTokensViaTextFile(importSource: ImportSource, url: URL) -> [Token]? {
guard let inputText = readTextFile(url: url) else {
logger.error("Failed to read file at \(url)")
return nil
}

switch importSource.id {
case .ENTE:
return importFromEnte(enteText: inputText)
default:
return nil
}
Expand All @@ -36,7 +53,7 @@ public class ImportService {
}
}

private func readFile(url: URL) -> JSON? {
private func readJsonFile(url: URL) -> JSON? {
guard url.startAccessingSecurityScopedResource() else {
logger.error("Failed to start accessing security scoped resource for \(url)")
return nil
Expand All @@ -52,6 +69,23 @@ public class ImportService {
return nil
}
}

private func readTextFile(url: URL) -> String? {
guard url.startAccessingSecurityScopedResource() else {
logger.error("Failed to start accessing security scoped resource for \(url)")
return nil
}

defer { url.stopAccessingSecurityScopedResource() }

do {
let strData = try String(contentsOf: url, encoding: .utf8)
return strData
} catch {
logger.error("Error reading file at \(url): \(error.localizedDescription)")
return nil
}
}
}

extension ImportService {
Expand Down Expand Up @@ -106,6 +140,29 @@ extension ImportService {
return tokens
}

func importFromEnte(enteText: String) -> [Token]? {
var tokens: [Token] = []

let lines = enteText.split { $0.isNewline }
let nonEmptyLines = lines.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
let inputOtpAuthUrls = nonEmptyLines.map { String($0) }

for otpAuthUrl in inputOtpAuthUrls {
do {
let token = try otpService.parseOtpAuthUrl(otpAuthStr: otpAuthUrl)
tokens.append(token)
} catch {
return nil
}
}

if tokens.count != inputOtpAuthUrls.count {
return nil
}

return tokens
}

func importFromRaivo(json: JSON) -> [Token]? {
var tokens: [Token] = []

Expand Down
73 changes: 73 additions & 0 deletions ChronosTests/Import/Ente.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@testable import Chronos
import XCTest

final class EnteTests: XCTestCase {
func testValidImport() throws {
let inputData = """
otpauth://totp/Apple:[email protected]?secret=AB6B7FAYHW2G42ZA4FJHLRWWHU&issuer=Apple&algorithm=SHA1&digits=6&period=30
otpauth://totp/AWS:[email protected]?secret=U7WXBPTLK62EC6Y2X4ALCMWWHS&issuer=AWS&algorithm=SHA256&digits=7&period=60
otpauth://totp/PG:[email protected]?secret=V27AJDJS4HZM3CTQNZXLVCHJYE&issuer=PG&algorithm=SHA512&digits=8&period=50
otpauth://totp/:?secret=MGTMWSHCBRMOBRI2AXNJD4M332&issuer=&algorithm=SHA1&digits=6&period=30
"""

let importService = ImportService()
let tokens = importService.importFromEnte(enteText: inputData)!

XCTAssertEqual(tokens.count, 4)

XCTAssertEqual(tokens[0].digits, 6)
XCTAssertEqual(tokens[0].type, TokenTypeEnum.TOTP)
XCTAssertEqual(tokens[0].algorithm, TokenAlgorithmEnum.SHA1)
XCTAssertEqual(tokens[0].issuer, "Apple")
XCTAssertEqual(tokens[0].account, "[email protected]")
XCTAssertEqual(tokens[0].period, 30)
XCTAssertEqual(tokens[0].secret, "AB6B7FAYHW2G42ZA4FJHLRWWHU")

XCTAssertEqual(tokens[1].digits, 7)
XCTAssertEqual(tokens[1].type, TokenTypeEnum.TOTP)
XCTAssertEqual(tokens[1].algorithm, TokenAlgorithmEnum.SHA256)
XCTAssertEqual(tokens[1].issuer, "AWS")
XCTAssertEqual(tokens[1].account, "[email protected]")
XCTAssertEqual(tokens[1].period, 60)
XCTAssertEqual(tokens[1].secret, "U7WXBPTLK62EC6Y2X4ALCMWWHS")

XCTAssertEqual(tokens[2].digits, 8)
XCTAssertEqual(tokens[2].type, TokenTypeEnum.TOTP)
XCTAssertEqual(tokens[2].algorithm, TokenAlgorithmEnum.SHA512)
XCTAssertEqual(tokens[2].issuer, "PG")
XCTAssertEqual(tokens[2].account, "[email protected]")
XCTAssertEqual(tokens[2].period, 50)
XCTAssertEqual(tokens[2].secret, "V27AJDJS4HZM3CTQNZXLVCHJYE")

XCTAssertEqual(tokens[3].digits, 6)
XCTAssertEqual(tokens[3].type, TokenTypeEnum.TOTP)
XCTAssertEqual(tokens[3].algorithm, TokenAlgorithmEnum.SHA1)
XCTAssertEqual(tokens[3].issuer, "")
XCTAssertEqual(tokens[3].account, "")
XCTAssertEqual(tokens[3].period, 30)
XCTAssertEqual(tokens[3].secret, "MGTMWSHCBRMOBRI2AXNJD4M332")
}

func testValidImport_Whitepsaces() throws {
let inputData = """


otpauth://totp/Apple:[email protected]?secret=AB6B7FAYHW2G42ZA4FJHLRWWHU&issuer=Apple&algorithm=SHA1&digits=6&period=30

otpauth://totp/AWS:[email protected]?secret=U7WXBPTLK62EC6Y2X4ALCMWWHS&issuer=AWS&algorithm=SHA256&digits=7&period=60


otpauth://totp/PG:[email protected]?secret=V27AJDJS4HZM3CTQNZXLVCHJYE&issuer=PG&algorithm=SHA512&digits=8&period=50

otpauth://totp/:?secret=MGTMWSHCBRMOBRI2AXNJD4M332&issuer=&algorithm=SHA1&digits=6&period=30



"""

let importService = ImportService()
let tokens = importService.importFromEnte(enteText: inputData)!

XCTAssertEqual(tokens.count, 4)
}
}
Loading