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

Prompt for Primary Password for FF Key3.db logins #2066

Merged
merged 5 commits into from
Jan 18, 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
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,15 @@ 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
}
}

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])
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading {

typealias KeyReaderFileLineError = FileLineError<FirefoxEncryptionKeyReader>

private enum Constants {
static let passwordCheckStr = "password-check"
static let passwordCheckLength = 16
static let key3length = 24
}

init() {
}

Expand All @@ -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)
Expand Down Expand Up @@ -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 )
Expand All @@ -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..<min(entrySalt.count, pes.count), with: entrySalt[0..<min(entrySalt.count, pes.count)])
let len = min(entrySalt.count, pes.count)
pes.replaceSubrange(0..<len, with: entrySalt[entrySalt.startIndex..<entrySalt.index(entrySalt.startIndex, offsetBy: len)])

let hp = SHA.from(data: globalSalt + primaryPasswordData)
let chp = SHA.from(data: hp + entrySalt)
Expand All @@ -188,7 +216,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading {
let k2 = calculateHMAC(key: chp, message: (tk + entrySalt))
let k = k1 + k2

let key = k.prefix(24)
let key = k.prefix(Constants.key3length)
let iv = k.suffix(8)

return try Cryptography.decrypt3DES(data: ciphertext, key: key, iv: iv)
Expand Down Expand Up @@ -329,7 +357,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading {

let passwordCheckString = String(data: decryptedCiphertext, encoding: .utf8)

guard passwordCheckString == "password-check" else { throw FirefoxLoginReader.ImportError(type: .requiresPrimaryPassword, underlyingError: nil) }
guard passwordCheckString == Constants.passwordCheckStr else { throw FirefoxLoginReader.ImportError(type: .requiresPrimaryPassword, underlyingError: nil) }

guard let nssPrivateRow = try NssPrivateRow.fetchOne(database, sql: "SELECT a11, a102 FROM nssPrivate;") else { throw KeyReaderFileLineError() }

Expand All @@ -350,7 +378,7 @@ final class FirefoxEncryptionKeyReader: FirefoxEncryptionKeyReading {
let passwordCheckString = String(data: decryptedItem2, encoding: .utf8)

// The password check is technically "password-check\x02\x02", it's converted to UTF-8 and checked here for simplicity
guard passwordCheckString == "password-check" else { throw FirefoxLoginReader.ImportError(type: .requiresPrimaryPassword, underlyingError: nil) }
guard passwordCheckString == Constants.passwordCheckStr else { throw FirefoxLoginReader.ImportError(type: .requiresPrimaryPassword, underlyingError: nil) }

guard let nssPrivateRow = try NssPrivateRow.fetchOne(database, sql: "SELECT a11, a102 FROM nssPrivate;") else { throw KeyReaderFileLineError() }

Expand Down
31 changes: 26 additions & 5 deletions DuckDuckGo/DataImport/View/DataImportProfilePicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,29 @@ struct DataImportProfilePicker: View {

private let profiles: [DataImport.BrowserProfile]
@Binding private var selectedProfile: DataImport.BrowserProfile?
private let shouldDisplayFolderName: Bool

private enum ProfileSubtitle {
case none
case parentFolderName
case profileFolderName
}
private let profileSubtitle: ProfileSubtitle

init(profileList: DataImport.BrowserProfileList?, selectedProfile: Binding<DataImport.BrowserProfile?>) {
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 {
Expand All @@ -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)
}
}
Expand Down
3 changes: 1 addition & 2 deletions UnitTests/DataImport/FirefoxKeyReaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
Expand Down
Loading