//
// This file is part of the 2FAS iOS app (https://github.com/twofas/2fas-ios)
// Copyright © 2023 Two Factor Authentication Service, Inc.
// Contributed by Zbigniew Cisiński. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see
//
import Foundation
import Compression
import Common
extension Code {
static func checkLastPass(with str: String) -> [Code]? {
guard let components = NSURLComponents(string: str),
let scheme = components.scheme, scheme == "lpaauth-migration",
let host = components.host, host == "offline",
let query = components.queryItems,
let data = query.first(where: { $0.name == "data" }),
let value = data.value?.removingPercentEncoding,
let encodeData = Data(base64Encoded: value),
let decompressedData = decompress(encodeData),
let codes = parseAndDecompressMainStructure(for: decompressedData)
else { return nil }
return codes
}
}
private extension Code {
struct LastPassService: Decodable {
let a: String // Algorithm
let iN: String? // Issuer/title
let s: String // Secret
let d: Int // Digits
let uN: String? // Account
let tS: Int? // Period
}
static func decompress(_ data: Data) -> Data? {
let pageSize = 128
var decompressedData = Data()
var inputFilter: InputFilter
do {
var index = 10 // Skipping header
let bufferSize = data.count
inputFilter = try InputFilter(.decompress, using: .zlib) { (length: Int) -> Data? in
let rangeLength = min(length, bufferSize - index)
let subdata = data.subdata(in: index ..< index + rangeLength)
index += rangeLength
return subdata
}
} catch {
Log("Error occurred while creating input filter for LastPass scanner: \(error as NSError)")
return nil
}
do {
while let page = try inputFilter.readData(ofLength: pageSize) {
decompressedData.append(page)
}
} catch {
Log("Error occurred during decoding from LastPass export url: \(error as NSError)")
return nil
}
return decompressedData
}
static func parseAndDecompressMainStructure(for data: Data) -> [Code]? {
let supportedVersion: Int = 3
struct MainStructure: Decodable {
let content: String
let version: Int
}
struct ContentStructure: Decodable {
let a: [LastPassService]
}
guard let mainStruct = try? JSONDecoder().decode(MainStructure.self, from: data) else {
Log("Error occurred during parsing main structure from LastPass")
return nil
}
guard mainStruct.version == supportedVersion else {
Log("Error during parsing main structure from LastPass - trying to import newer version")
return nil
}
guard let baseEncodedContent = Data(base64Encoded: mainStruct.content) else {
Log("Error during parsing main structure from LastPass - can't parse base64 of the content")
return nil
}
guard let contentStruct = decompress(baseEncodedContent) else {
Log("Error during decompresssing main structure from LastPass")
return nil
}
guard let content = try? JSONDecoder().decode(ContentStructure.self, from: contentStruct) else {
Log("Error during parsing content structure from LastPass - can't parse ContentStructure")
return nil
}
return content.a.map({ parseLastPassService($0) })
}
static func parseLastPassService(_ service: LastPassService) -> Code {
Code(
issuer: service.iN,
label: service.uN?.sanitizeInfo(),
secret: service.s.sanitazeSecret(),
period: .create(service.tS),
digits: .create(service.d),
algorithm: .create(service.a),
tokenType: .totp,
counter: 0,
otpAuth: nil
)
}
}