Skip to content

Commit

Permalink
Added ente with tests (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldavidw committed Aug 7, 2024
1 parent da06777 commit ca30175
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 8 deletions.
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)
}
}

0 comments on commit ca30175

Please sign in to comment.