Skip to content

Commit

Permalink
Prompt for Primary Password for FF Key3.db logins (#2066)
Browse files Browse the repository at this point in the history
  • Loading branch information
mallexxx authored Jan 18, 2024
1 parent c8600b6 commit bfd2c22
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 25 deletions.
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

0 comments on commit bfd2c22

Please sign in to comment.