Skip to content

Commit

Permalink
Implemented Lastpass import with tests (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldavidw authored Jul 28, 2024
1 parent adab357 commit 9e18a0c
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Chronos.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
6B3C7A3D2BE9E0600043FEBD /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 6B3C7A3C2BE9E0600043FEBD /* Logging */; };
6B3F92A92C1B45AA004125A8 /* Vault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3F92A82C1B45AA004125A8 /* Vault.swift */; };
6B3F92AB2C1C7987004125A8 /* VaultService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B3F92AA2C1C7987004125A8 /* VaultService.swift */; };
6B4987282C569A6E00A7D97A /* LastPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4987272C569A6E00A7D97A /* LastPass.swift */; };
6B4B48F32BD7BB3C007D357D /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4B48F22BD7BB3C007D357D /* Token.swift */; };
6B4CBF2F2C490FB700983D44 /* Chronos.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B4CBF2E2C490FB700983D44 /* Chronos.swift */; };
6B5E41CE2BD790F80045DBC6 /* EncryptedToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E41CD2BD790F80045DBC6 /* EncryptedToken.swift */; };
Expand Down Expand Up @@ -114,6 +115,7 @@
6B3C7A372BE764E70043FEBD /* StorageSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageSetupView.swift; sourceTree = "<group>"; };
6B3F92A82C1B45AA004125A8 /* Vault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vault.swift; sourceTree = "<group>"; };
6B3F92AA2C1C7987004125A8 /* VaultService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultService.swift; sourceTree = "<group>"; };
6B4987272C569A6E00A7D97A /* LastPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastPass.swift; sourceTree = "<group>"; };
6B4B48F22BD7BB3C007D357D /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = "<group>"; };
6B4CBF2C2C490FB700983D44 /* ChronosTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChronosTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
6B4CBF2E2C490FB700983D44 /* Chronos.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chronos.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -322,6 +324,7 @@
6B8132F12C4975DA00DB367E /* Raivo.swift */,
6BC5F0562C529A2A00BA106F /* GoogleAuthenticator.swift */,
6BBF32022C562F20003CBA66 /* Aegis.swift */,
6B4987272C569A6E00A7D97A /* LastPass.swift */,
);
path = Import;
sourceTree = "<group>";
Expand Down Expand Up @@ -689,6 +692,7 @@
6BBF32032C562F20003CBA66 /* Aegis.swift in Sources */,
6B4CBF2F2C490FB700983D44 /* Chronos.swift in Sources */,
6BC5F0572C529A2A00BA106F /* GoogleAuthenticator.swift in Sources */,
6B4987282C569A6E00A7D97A /* LastPass.swift in Sources */,
6BC5F04A2C4FDE6E00BA106F /* ParseOtpAuthUrl.swift in Sources */,
6B8132F22C4975DA00DB367E /* Raivo.swift in Sources */,
6B8132FA2C4C0F6300DB367E /* TokenToOtpAuthUrl.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Chronos/App/Tabs/Settings/Import/ImportSourceListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum ImportSourceId {
case AEGIS
case RAIVO
case GOOGLE_AUTHENTICATOR
case LASTPASS
}

enum ImportType {
Expand All @@ -25,6 +26,7 @@ struct ImportSourceListView: View {
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),
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),
]

@EnvironmentObject var importNav: ExportNavigation
Expand Down
38 changes: 38 additions & 0 deletions Chronos/Services/Import/ImportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class ImportService {
return importFromAegis(json: json)
case .RAIVO:
return importFromRaivo(json: json)
case .LASTPASS:
return importFromLastpass(json: json)
default:
return nil
}
Expand Down Expand Up @@ -161,6 +163,42 @@ extension ImportService {
return tokens
}

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

for (key, subJson) in json["accounts"] {
guard
let issuer = subJson["issuerName"].string,
let account = subJson["userName"].string,
let secret = subJson["secret"].string,
let digits = subJson["digits"].int,
let period = subJson["timeStep"].int,
let algorithm = subJson["algorithm"].string,
let tokenAlgorithm = TokenAlgorithmEnum(rawValue: algorithm.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.type = TokenTypeEnum.TOTP
token.algorithm = tokenAlgorithm

tokens.append(token)
}

if tokens.count != json["accounts"].count {
return nil
}

return tokens
}

func importFromGoogleAuth(otpAuthMigration: String) -> [Token]? {
guard let otpAuthMigrationUrl = URL(string: otpAuthMigration),
let components = URLComponents(url: otpAuthMigrationUrl, resolvingAgainstBaseURL: false),
Expand Down
111 changes: 111 additions & 0 deletions ChronosTests/Import/LastPass.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
@testable import Chronos
import SwiftyJSON
import XCTest

final class LastPassTests: XCTestCase {
func testValidImport() throws {
let json: JSON =
[
"accounts": [
[
"accountID": "",
"algorithm": "SHA1",
"creationTimestamp": 1_722_179_392,
"digits": 6,
"folderData": [
"folderId": 0,
"position": 0,
],
"isFavorite": false,
"issuerName": "Lith",
"lmiUserId": "",
"originalIssuerName": "",
"originalUserName": "Shanks",
"secret": "KQOZTMLOJBSMYWIO4BG4UTXDSR",
"timeStep": 30,
"userName": "Shanks",
],
[
"accountID": "",
"algorithm": "SHA256",
"creationTimestamp": 1_722_180_037,
"digits": 7,
"folderData": [
"folderId": 0,
"position": 1,
],
"isFavorite": false,
"issuerName": "Ludi",
"lmiUserId": "",
"originalIssuerName": "Ludi",
"originalUserName": "Roly Poly",
"secret": "LI3SJSNME7PSCQCXL2TIFMNH64",
"timeStep": 60,
"userName": "Roly Poly",
],
[
"accountID": "",
"algorithm": "SHA256",
"creationTimestamp": 1_722_180_037,
"digits": 8,
"folderData": [
"folderId": 0,
"position": 1,
],
"isFavorite": false,
"issuerName": "",
"lmiUserId": "",
"originalIssuerName": "",
"originalUserName": "Todd",
"secret": "F7E5IPN2KYUXE5WZLMSRVTFK7N",
"timeStep": 60,
"userName": "Todd",
],
],
"deviceName": "iPhone16,2",
"folders": [
[
"id": 1,
"isOpened": true,
"name": "Favorites",
],
[
"id": 0,
"isOpened": true,
"name": "Other Accounts",
],
],
"localDeviceId": "E21FD688-A948-4773-8AD5-4EEECFBF82E4",
"version": 1,
]

let importService = ImportService()
let tokens = importService.importFromLastpass(json: json)!

XCTAssertEqual(tokens.count, 3)

XCTAssertEqual(tokens[0].digits, 6)
XCTAssertEqual(tokens[0].type, TokenTypeEnum.TOTP)
XCTAssertEqual(tokens[0].algorithm, TokenAlgorithmEnum.SHA1)
XCTAssertEqual(tokens[0].issuer, "Lith")
XCTAssertEqual(tokens[0].account, "Shanks")
XCTAssertEqual(tokens[0].period, 30)
XCTAssertEqual(tokens[0].secret, "KQOZTMLOJBSMYWIO4BG4UTXDSR")

XCTAssertEqual(tokens[1].digits, 7)
XCTAssertEqual(tokens[1].type, TokenTypeEnum.TOTP)
XCTAssertEqual(tokens[1].algorithm, TokenAlgorithmEnum.SHA256)
XCTAssertEqual(tokens[1].issuer, "Ludi")
XCTAssertEqual(tokens[1].account, "Roly Poly")
XCTAssertEqual(tokens[1].period, 60)
XCTAssertEqual(tokens[1].secret, "LI3SJSNME7PSCQCXL2TIFMNH64")

XCTAssertEqual(tokens[2].digits, 8)
XCTAssertEqual(tokens[2].type, TokenTypeEnum.TOTP)
XCTAssertEqual(tokens[2].algorithm, TokenAlgorithmEnum.SHA256)
XCTAssertEqual(tokens[2].issuer, "")
XCTAssertEqual(tokens[2].account, "Todd")
XCTAssertEqual(tokens[2].period, 60)
XCTAssertEqual(tokens[2].secret, "F7E5IPN2KYUXE5WZLMSRVTFK7N")
}
}

0 comments on commit 9e18a0c

Please sign in to comment.