diff --git a/Chronos.xcodeproj/project.pbxproj b/Chronos.xcodeproj/project.pbxproj index 6524fea..afaae6c 100644 --- a/Chronos.xcodeproj/project.pbxproj +++ b/Chronos.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 6B2583AF2B96048700938F3A /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B2583AE2B96048700938F3A /* SettingsTab.swift */; }; 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 */; }; 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 */; }; @@ -100,6 +101,7 @@ 6B2583B02B975D3200938F3A /* Chronos.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Chronos.entitlements; sourceTree = ""; }; 6B2731792B53E23800F30621 /* UpdateTokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTokenView.swift; sourceTree = ""; }; 6B27317B2B53F0B200F30621 /* TokenRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenRowView.swift; sourceTree = ""; }; + 6B2F7A812C635B5A00DB1450 /* 2FAS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = 2FAS.swift; sourceTree = ""; }; 6B3962992BF5E935000410B0 /* MainAppView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAppView.swift; sourceTree = ""; }; 6B39629B2BF5EB3E000410B0 /* AuthenticationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationView.swift; sourceTree = ""; }; 6B39629D2BF63F27000410B0 /* SwiftDataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDataService.swift; sourceTree = ""; }; @@ -325,6 +327,7 @@ 6BC5F0562C529A2A00BA106F /* GoogleAuthenticator.swift */, 6BBF32022C562F20003CBA66 /* Aegis.swift */, 6B4987272C569A6E00A7D97A /* LastPass.swift */, + 6B2F7A812C635B5A00DB1450 /* 2FAS.swift */, ); path = Import; sourceTree = ""; @@ -695,6 +698,7 @@ 6B4987282C569A6E00A7D97A /* LastPass.swift in Sources */, 6BC5F04A2C4FDE6E00BA106F /* ParseOtpAuthUrl.swift in Sources */, 6B8132F22C4975DA00DB367E /* Raivo.swift in Sources */, + 6B2F7A822C635B5A00DB1450 /* 2FAS.swift in Sources */, 6B8132FA2C4C0F6300DB367E /* TokenToOtpAuthUrl.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Chronos/App/Tabs/Settings/Import/ImportSourceListView.swift b/Chronos/App/Tabs/Settings/Import/ImportSourceListView.swift index 2f8c923..966b99e 100644 --- a/Chronos/App/Tabs/Settings/Import/ImportSourceListView.swift +++ b/Chronos/App/Tabs/Settings/Import/ImportSourceListView.swift @@ -2,6 +2,7 @@ import SwiftUI enum ImportSourceId { case CHRONOS + case TWOFAS case AEGIS case RAIVO case GOOGLE_AUTHENTICATOR @@ -23,6 +24,7 @@ struct ImportSource: Identifiable { 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.", 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: .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), diff --git a/Chronos/Services/Import/ImportService.swift b/Chronos/Services/Import/ImportService.swift index 2254409..bfa1fb1 100644 --- a/Chronos/Services/Import/ImportService.swift +++ b/Chronos/Services/Import/ImportService.swift @@ -66,6 +66,46 @@ extension ImportService { } } + func importFrom2FAS(json: JSON) -> [Token]? { + var tokens: [Token] = [] + + for (key, subJson) in json["services"] { + guard + let issuer = subJson["otp"]["issuer"].string, + let account = subJson["otp"]["account"].string, + let secret = subJson["secret"].string, + let digits = subJson["otp"]["digits"].int, + let counter = subJson["otp"]["counter"].int, + let period = subJson["otp"]["period"].int, + let algorithm = subJson["otp"]["algorithm"].string, + let tokenAlgorithm = TokenAlgorithmEnum(rawValue: algorithm.uppercased()), + let type = subJson["otp"]["tokenType"].string, + let tokenType = TokenTypeEnum(rawValue: type.uppercased()) + else { + logger.error("Error parsing token data for key: \(key)") + continue + } + + let token = Token() + token.issuer = issuer + token.account = account + token.secret = secret + token.digits = digits + token.period = period + token.counter = counter + token.type = tokenType + token.algorithm = tokenAlgorithm + + tokens.append(token) + } + + if tokens.count != json["services"].count { + return nil + } + + return tokens + } + func importFromRaivo(json: JSON) -> [Token]? { var tokens: [Token] = [] diff --git a/ChronosTests/Import/2FAS.swift b/ChronosTests/Import/2FAS.swift new file mode 100644 index 0000000..edf1221 --- /dev/null +++ b/ChronosTests/Import/2FAS.swift @@ -0,0 +1,666 @@ +@testable import Chronos +import SwiftyJSON +import XCTest + +final class TwoFASTests: XCTestCase { + func testValidImport() throws { + let json: JSON = + [ + "appOrigin": "ios", + "appVersionCode": 50308, + "appVersionName": "5.3.8", + "groups": [], + "schemaVersion": 4, + "services": [ + [ + "icon": [ + "iconCollection": [ + "id": "90CAF674-4269-4193-9749-4849F97CFB53", + ], + "label": [ + "backgroundColor": "Orange", + "text": "AP", + ], + "selected": "IconCollection", + ], + "name": "Apple", + "order": [ + "position": 0, + ], + "otp": [ + "account": "user1+totp@test.com", + "algorithm": "SHA1", + "counter": 0, + "digits": 6, + "issuer": "Apple", + "period": 30, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "AB6B7FAYHW2G42ZA4FJHLRWWHU", + "serviceTypeID": "ea250427-4465-48d7-bbcd-4616be85626a", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Brown", + "text": "AW", + ], + "selected": "Label", + ], + "name": "AWS", + "order": [ + "position": 1, + ], + "otp": [ + "account": "user2+totp@test.com", + "algorithm": "SHA256", + "counter": 0, + "digits": 7, + "issuer": "AWS", + "period": 60, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "U7WXBPTLK62EC6Y2X4ALCMWWHS", + "serviceTypeID": "71789cdc-d0be-42ad-98ab-ea146c5ea5d6", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Indigo", + "text": "PG", + ], + "selected": "Label", + ], + "name": "PG", + "order": [ + "position": 2, + ], + "otp": [ + "account": "user3+totp@test.com", + "algorithm": "SHA512", + "counter": 0, + "digits": 8, + "issuer": "PG", + "period": 50, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "V27AJDJS4HZM3CTQNZXLVCHJYE", + "serviceTypeID": "f98fdeef-03ef-4491-b408-2ecfa4f20ad4", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Gray", + "text": "UN", + ], + "selected": "Label", + ], + "name": "Unknown", + "order": [ + "position": 3, + ], + "otp": [ + "account": "", + "algorithm": "SHA1", + "counter": 0, + "digits": 6, + "issuer": "", + "period": 30, + "source": "manual", + "tokenType": "TOTP", + ], + "secret": "MGTMWSHCBRMOBRI2AXNJD4M332", + "serviceTypeID": "2b2ba134-870d-4100-91f6-1193ad569841", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "90CAF674-4269-4193-9749-4849F97CFB53", + ], + "label": [ + "backgroundColor": "Yellow", + "text": "YU", + ], + "selected": "IconCollection", + ], + "name": "Yubi", + "order": [ + "position": 4, + ], + "otp": [ + "account": "user1+hotp@test.com", + "algorithm": "SHA1", + "counter": 1, + "digits": 6, + "issuer": "Yubi", + "period": 0, + "source": "link", + "tokenType": "HOTP", + ], + "secret": "KXXYUNPQY2CDRBDFLAIIEQ4H7O", + "serviceTypeID": "256f1470-2ece-47c6-a763-235a56167d6a", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Red", + "text": "HE", + ], + "selected": "Label", + ], + "name": "Hene", + "order": [ + "position": 5, + ], + "otp": [ + "account": "user2+hotp@test.com", + "algorithm": "SHA256", + "counter": 16, + "digits": 7, + "issuer": "Hene", + "period": 0, + "source": "link", + "tokenType": "HOTP", + ], + "secret": "LI3SJSNME7PSCQCXL2TIFMNH64", + "serviceTypeID": "ebc55e92-24f6-4ddd-81d0-1e293549ad41", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Blue", + "text": "NA", + ], + "selected": "Label", + ], + "name": "Nath", + "order": [ + "position": 6, + ], + "otp": [ + "account": "user3+hotp@test.com", + "algorithm": "SHA512", + "counter": 101, + "digits": 8, + "issuer": "Nath", + "period": 0, + "source": "link", + "tokenType": "HOTP", + ], + "secret": "F7E5IPN2KYUXE5WZLMSRVTFK7N", + "serviceTypeID": "9c4d30ba-3c19-46d7-929a-7eb01e1956af", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Green", + "text": "UN", + ], + "selected": "Label", + ], + "name": "Unknown", + "order": [ + "position": 7, + ], + "otp": [ + "account": "", + "algorithm": "SHA1", + "counter": 22, + "digits": 6, + "issuer": "", + "period": 0, + "source": "manual", + "tokenType": "HOTP", + ], + "secret": "KQOZTMLOJBSMYWIO4BG4UTXDSR", + "serviceTypeID": "3cb83fc3-8e25-4df5-bdde-1f35611c1d0a", + "updatedAt": 1_721_919_046_000, + ], + ], + ] + + let importService = ImportService() + let tokens = importService.importFrom2FAS(json: json)! + + XCTAssertEqual(tokens.count, 8) + + 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, "user1+totp@test.com") + 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, "user2+totp@test.com") + 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, "user3+totp@test.com") + 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") + + XCTAssertEqual(tokens[4].digits, 6) + XCTAssertEqual(tokens[4].type, TokenTypeEnum.HOTP) + XCTAssertEqual(tokens[4].algorithm, TokenAlgorithmEnum.SHA1) + XCTAssertEqual(tokens[4].issuer, "Yubi") + XCTAssertEqual(tokens[4].account, "user1+hotp@test.com") + XCTAssertEqual(tokens[4].counter, 1) + XCTAssertEqual(tokens[4].secret, "KXXYUNPQY2CDRBDFLAIIEQ4H7O") + + XCTAssertEqual(tokens[5].digits, 7) + XCTAssertEqual(tokens[5].type, TokenTypeEnum.HOTP) + XCTAssertEqual(tokens[5].algorithm, TokenAlgorithmEnum.SHA256) + XCTAssertEqual(tokens[5].issuer, "Hene") + XCTAssertEqual(tokens[5].account, "user2+hotp@test.com") + XCTAssertEqual(tokens[5].counter, 16) + XCTAssertEqual(tokens[5].secret, "LI3SJSNME7PSCQCXL2TIFMNH64") + + XCTAssertEqual(tokens[6].digits, 8) + XCTAssertEqual(tokens[6].type, TokenTypeEnum.HOTP) + XCTAssertEqual(tokens[6].algorithm, TokenAlgorithmEnum.SHA512) + XCTAssertEqual(tokens[6].issuer, "Nath") + XCTAssertEqual(tokens[6].account, "user3+hotp@test.com") + XCTAssertEqual(tokens[6].counter, 101) + XCTAssertEqual(tokens[6].secret, "F7E5IPN2KYUXE5WZLMSRVTFK7N") + + XCTAssertEqual(tokens[7].digits, 6) + XCTAssertEqual(tokens[7].type, TokenTypeEnum.HOTP) + XCTAssertEqual(tokens[7].algorithm, TokenAlgorithmEnum.SHA1) + XCTAssertEqual(tokens[7].issuer, "") + XCTAssertEqual(tokens[7].account, "") + XCTAssertEqual(tokens[7].counter, 22) + XCTAssertEqual(tokens[7].secret, "KQOZTMLOJBSMYWIO4BG4UTXDSR") + } + + func testValidImport_Period() throws { + let json: JSON = + [ + "appOrigin": "ios", + "appVersionCode": 50308, + "appVersionName": "5.3.8", + "groups": [], + "schemaVersion": 4, + "services": [ + [ + "icon": [ + "iconCollection": [ + "id": "90CAF674-4269-4193-9749-4849F97CFB53", + ], + "label": [ + "backgroundColor": "Orange", + "text": "AP", + ], + "selected": "IconCollection", + ], + "name": "Apple", + "order": [ + "position": 0, + ], + "otp": [ + "account": "user1+totp@test.com", + "algorithm": "SHA1", + "counter": 0, + "digits": 6, + "issuer": "Apple", + "period": 10, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "AB6B7FAYHW2G42ZA4FJHLRWWHU", + "serviceTypeID": "ea250427-4465-48d7-bbcd-4616be85626a", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Brown", + "text": "AW", + ], + "selected": "Label", + ], + "name": "AWS", + "order": [ + "position": 1, + ], + "otp": [ + "account": "user2+totp@test.com", + "algorithm": "SHA256", + "counter": 0, + "digits": 7, + "issuer": "AWS", + "period": 30, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "U7WXBPTLK62EC6Y2X4ALCMWWHS", + "serviceTypeID": "71789cdc-d0be-42ad-98ab-ea146c5ea5d6", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Indigo", + "text": "PG", + ], + "selected": "Label", + ], + "name": "PG", + "order": [ + "position": 2, + ], + "otp": [ + "account": "user3+totp@test.com", + "algorithm": "SHA512", + "counter": 0, + "digits": 8, + "issuer": "PG", + "period": 60, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "V27AJDJS4HZM3CTQNZXLVCHJYE", + "serviceTypeID": "f98fdeef-03ef-4491-b408-2ecfa4f20ad4", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Gray", + "text": "UN", + ], + "selected": "Label", + ], + "name": "Unknown", + "order": [ + "position": 3, + ], + "otp": [ + "account": "", + "algorithm": "SHA1", + "counter": 0, + "digits": 6, + "issuer": "", + "period": 90, + "source": "manual", + "tokenType": "TOTP", + ], + "secret": "MGTMWSHCBRMOBRI2AXNJD4M332", + "serviceTypeID": "2b2ba134-870d-4100-91f6-1193ad569841", + "updatedAt": 1_721_919_046_000, + ], + ], + ] + + let importService = ImportService() + let tokens = importService.importFrom2FAS(json: json)! + + 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, "user1+totp@test.com") + XCTAssertEqual(tokens[0].period, 10) + 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, "user2+totp@test.com") + XCTAssertEqual(tokens[1].period, 30) + 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, "user3+totp@test.com") + XCTAssertEqual(tokens[2].period, 60) + 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, 90) + XCTAssertEqual(tokens[3].secret, "MGTMWSHCBRMOBRI2AXNJD4M332") + } + + func testInvalidImport_Algorithm() throws { + let json: JSON = + [ + "appOrigin": "ios", + "appVersionCode": 50308, + "appVersionName": "5.3.8", + "groups": [], + "schemaVersion": 4, + "services": [ + [ + "icon": [ + "iconCollection": [ + "id": "90CAF674-4269-4193-9749-4849F97CFB53", + ], + "label": [ + "backgroundColor": "Orange", + "text": "AP", + ], + "selected": "IconCollection", + ], + "name": "Apple", + "order": [ + "position": 0, + ], + "otp": [ + "account": "user1+totp@test.com", + "algorithm": "SHA1", + "counter": 0, + "digits": 6, + "issuer": "Apple", + "period": 30, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "AB6B7FAYHW2G42ZA4FJHLRWWHU", + "serviceTypeID": "ea250427-4465-48d7-bbcd-4616be85626a", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Brown", + "text": "AW", + ], + "selected": "Label", + ], + "name": "AWS", + "order": [ + "position": 1, + ], + "otp": [ + "account": "userMD5+totp@test.com", + "algorithm": "MD5", + "counter": 0, + "digits": 7, + "issuer": "AWS", + "period": 60, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "U7WXBPTLK62EC6Y2X4ALCMWWHS", + "serviceTypeID": "71789cdc-d0be-42ad-98ab-ea146c5ea5d6", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Indigo", + "text": "PG", + ], + "selected": "Label", + ], + "name": "PG", + "order": [ + "position": 2, + ], + "otp": [ + "account": "userSHA224@test.com", + "algorithm": "SHA224", + "counter": 0, + "digits": 8, + "issuer": "PG", + "period": 30, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "V27AJDJS4HZM3CTQNZXLVCHJYE", + "serviceTypeID": "f98fdeef-03ef-4491-b408-2ecfa4f20ad4", + "updatedAt": 1_721_919_046_000, + ], + [ + "icon": [ + "iconCollection": [ + "id": "A5B3FB65-4EC5-43E6-8EC1-49E24CA9E7AD", + ], + "label": [ + "backgroundColor": "Indigo", + "text": "PG", + ], + "selected": "Label", + ], + "name": "PG", + "order": [ + "position": 2, + ], + "otp": [ + "account": "userSHA334@test.com", + "algorithm": "SHA334", + "counter": 0, + "digits": 8, + "issuer": "PG", + "period": 30, + "source": "link", + "tokenType": "TOTP", + ], + "secret": "V27AJDJS4HZM3CTQNZXLVCHJYE", + "serviceTypeID": "f98fdeef-03ef-4491-b408-2ecfa4f20ad4", + "updatedAt": 1_721_919_046_000, + ], + ], + ] + + let importService = ImportService() + let tokens = importService.importFrom2FAS(json: json) + + XCTAssertNil(tokens) + } + + func testInvalidImport_Steam() throws { + let json: JSON = + [ + "appOrigin": "ios", + "appVersionCode": 50308, + "appVersionName": "5.3.8", + "groups": [], + "schemaVersion": 4, + "services": [ + [ + "icon": [ + "iconCollection": [ + "id": "90CAF674-4269-4193-9749-4849F97CFB53", + ], + "label": [ + "backgroundColor": "Orange", + "text": "AP", + ], + "selected": "IconCollection", + ], + "name": "Apple", + "order": [ + "position": 0, + ], + "otp": [ + "account": "user1+totp@test.com", + "algorithm": "SHA1", + "counter": 0, + "digits": 6, + "issuer": "Apple", + "period": 30, + "source": "link", + "tokenType": "STEAM", + ], + "secret": "AB6B7FAYHW2G42ZA4FJHLRWWHU", + "serviceTypeID": "ea250427-4465-48d7-bbcd-4616be85626a", + "updatedAt": 1_721_919_046_000, + ], + ], + ] + + let importService = ImportService() + let tokens = importService.importFrom2FAS(json: json) + + XCTAssertNil(tokens) + } +}