diff --git a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxBerkeleyDatabaseReader.swift b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxBerkeleyDatabaseReader.swift index a11e4b9e7e..ce5168b32c 100644 --- a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxBerkeleyDatabaseReader.swift +++ b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxBerkeleyDatabaseReader.swift @@ -38,12 +38,9 @@ enum FirefoxBerkeleyDatabaseReader { Data(bytes: pointer, count: currentDataDBT.size) } - let currentKeyHexadecimalString = currentKeyData.hexadecimalString - let currentKeyString = String(bytes: currentKeyData, encoding: .utf8) - - if currentKeyHexadecimalString == Self.firefoxASN1Key { + if currentKeyData == Self.firefoxASN1Key { results["data"] = currentData - } else if let currentKeyString { + } else if let currentKeyString = currentKeyData.utf8String() { results[currentKeyString] = currentData } } @@ -51,11 +48,5 @@ enum FirefoxBerkeleyDatabaseReader { return results } - private static let firefoxASN1Key: String = "f8000000000000000000000000000001" -} - -private extension Data { - var hexadecimalString: String { - Array(self).reduce(into: String()) { $0.append(String(format: "%02lx", $1)) } - } + private static let firefoxASN1Key = Data([0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]) } diff --git a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxEncryptionKeyReader.swift b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxEncryptionKeyReader.swift index 8643ce6ad7..23d1261196 100644 --- a/DuckDuckGo/DataImport/Logins/Firefox/FirefoxEncryptionKeyReader.swift +++ b/DuckDuckGo/DataImport/Logins/Firefox/FirefoxEncryptionKeyReader.swift @@ -32,6 +32,12 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { typealias KeyReaderFileLineError = FileLineError + private enum Constants { + static let passwordCheckStr = "password-check" + static let passwordCheckLength = 16 + static let key3length = 24 + } + init() { } @@ -56,6 +62,26 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { guard asnData.count > 4 else { throw KeyReaderFileLineError() } asnData = asnData[4...] // Drop the first 4 bytes, they aren't required for decryption and can be ignored + // decrypted password-check should match "password-check" + if let passwordCheck = result[Constants.passwordCheckStr] { + guard passwordCheck.count > Constants.passwordCheckLength else { throw KeyReaderFileLineError() } + let entrySaltLength = Int(passwordCheck[1]) + guard entrySaltLength > 0 else { throw KeyReaderFileLineError() } + guard passwordCheck.count >= Constants.passwordCheckLength + 3 + entrySaltLength else { throw KeyReaderFileLineError() } + let entrySalt = passwordCheck[3..<(entrySaltLength + 3)] + let passwordCheckCiphertext = passwordCheck[(passwordCheck.count - Constants.passwordCheckLength)...] + + do { + let decryptedCiphertext = try tripleDesDecrypt(ciphertext: passwordCheckCiphertext, + globalSalt: globalSalt, + entrySalt: entrySalt, + primaryPassword: primaryPassword) + guard decryptedCiphertext.utf8String() == Constants.passwordCheckStr else { throw KeyReaderFileLineError() } + } catch { + throw FirefoxLoginReader.ImportError(type: .requiresPrimaryPassword, underlyingError: nil) + } + } + // Part 1: Take the data from the database and decrypt it. let decodedASNData = try ASN1Parser.parse(data: asnData) @@ -160,11 +186,12 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { var lineError = KeyReaderFileLineError.nextLine() guard case let .sequence(outerSequence) = node, lineError.next(), let integer = outerSequence[safe: 3], lineError.next(), - case let .integer(data: data) = integer else { + case let .integer(data: data) = integer, lineError.next(), + data.count >= Constants.key3length else { throw lineError } - return data + return data.dropFirst(data.count - Constants.key3length) } /// HP = SHA1( global-salt | PrimaryPassword ) @@ -179,7 +206,8 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading { private func tripleDesDecrypt(ciphertext: Data, globalSalt: Data, entrySalt: Data, primaryPassword: String) throws -> Data { let primaryPasswordData = primaryPassword.utf8data var pes = Data(count: 20) - pes.replaceSubrange(0..) { self.profiles = profileList?.validImportableProfiles ?? [] self._selectedProfile = selectedProfile - shouldDisplayFolderName = Set(self.profiles.map { + // display parent folder name as a subtitle if there are multiple + // browser build profile folders (Chrome, Chrome Dev, Canary...) + if Set(profiles.map { $0.profileURL.deletingLastPathComponent() - }).count > 1 + }).count > 1 { + profileSubtitle = .parentFolderName + } else if Set(profiles.map(\.profileName)).count != profiles.count { + // when there‘re repeated profile names display profile folder names + profileSubtitle = .profileFolderName + } else { + profileSubtitle = .none + } } var body: some View { @@ -44,13 +59,19 @@ struct DataImportProfilePicker: View { }) { ForEach(profiles.indices, id: \.self) { idx in // display profiles folder name if multiple profiles folders are present (Chrome, Chrome Canary…) - if shouldDisplayFolderName { + switch profileSubtitle { + case .parentFolderName: Text(profiles[idx].profileName + " ") + Text(profiles[idx].profileURL .deletingLastPathComponent().lastPathComponent) .font(.system(size: 10)) .fontWeight(.light) - } else { + case .profileFolderName: + Text(profiles[idx].profileName + " ") + + Text(profiles[idx].profileURL.lastPathComponent) + .font(.system(size: 10)) + .fontWeight(.light) + case .none: Text(profiles[idx].profileName) } } diff --git a/UnitTests/DataImport/FirefoxKeyReaderTests.swift b/UnitTests/DataImport/FirefoxKeyReaderTests.swift index 1c58436857..10923ee698 100644 --- a/UnitTests/DataImport/FirefoxKeyReaderTests.swift +++ b/UnitTests/DataImport/FirefoxKeyReaderTests.swift @@ -54,8 +54,7 @@ class FirefoxKeyReaderTests: XCTestCase { switch result { case .failure(let error as FirefoxLoginReader.ImportError): - XCTAssertEqual(error.type, .key3readerStage2) - XCTAssertTrue(error.underlyingError is ASN1Parser.ParserError) + XCTAssertEqual(error.type, .requiresPrimaryPassword) default: XCTFail("Received unexpected \(result)") }