mirror of
https://github.com/twofas/2fas-ios.git
synced 2024-11-22 02:10:19 +01:00
209 lines
8.1 KiB
Swift
209 lines
8.1 KiB
Swift
//
|
|
// 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 <https://www.gnu.org/licenses/>
|
|
//
|
|
|
|
import WidgetKit
|
|
import Data
|
|
import Intents
|
|
import Common
|
|
import CommonUIKit
|
|
import IntentsUI
|
|
import Protection
|
|
import Storage
|
|
import TimeVerification
|
|
|
|
struct Provider: IntentTimelineProvider {
|
|
private enum Values {
|
|
case placeholder
|
|
case entry(EntryDescription)
|
|
}
|
|
|
|
struct EntryDescription {
|
|
let identifier: String
|
|
let secret: String
|
|
let title: String
|
|
let subtitle: String?
|
|
let iconTypeID: IconTypeID
|
|
let serviceTypeID: ServiceTypeID?
|
|
let digits: Digits
|
|
let period: Period
|
|
let algorithm: Algorithm
|
|
let tokenType: TokenType
|
|
let iconType: IconType
|
|
let labelTitle: String
|
|
let labelColor: TintColor
|
|
}
|
|
private let protection: Protection
|
|
private let serviceHandler: WidgetServiceHandlerType
|
|
|
|
init() {
|
|
protection = Protection()
|
|
EncryptionHolder.initialize(with: protection.localKeyEncryption)
|
|
serviceHandler = Storage(readOnly: true, logError: nil).widgetService
|
|
}
|
|
|
|
func placeholder(in context: Context) -> CodeEntry {
|
|
CodeEntry.placeholder(with: context.family.servicesCount)
|
|
}
|
|
|
|
func getSnapshot(
|
|
for configuration: SelectServiceIntent,
|
|
in context: Context,
|
|
completion: @escaping (CodeEntry) -> Void
|
|
) {
|
|
completion(CodeEntry.snapshot(with: context.family.servicesCount))
|
|
}
|
|
|
|
func getTimeline(
|
|
for configuration: SelectServiceIntent,
|
|
in context: Context,
|
|
completion: @escaping (Timeline<CodeEntry>) -> Void
|
|
) {
|
|
let slots = context.family.servicesCount
|
|
|
|
guard protection.extensionsStorage.areWidgetsEnabled else {
|
|
let timeline = Timeline<CodeEntry>(entries: [CodeEntry.placeholder(with: slots)], policy: .never)
|
|
completion(timeline)
|
|
return
|
|
}
|
|
|
|
let selectedServices = configuration.service ?? []
|
|
let currentServices = serviceHandler.listServices(with: selectedServices.compactMap { $0.secret })
|
|
.filter({ $0.period != .period10 })
|
|
|
|
var entries: [CodeEntry] = []
|
|
|
|
let services: [Values] = {
|
|
let list = selectedServices
|
|
var result: [Values] = Array(repeating: Values.placeholder, count: slots)
|
|
for i in 0..<slots {
|
|
if let service = list[safe: i],
|
|
let identifier = service.identifier,
|
|
let secret = service.secret,
|
|
let widgetService = currentServices.first(where: { $0.serviceID == secret }) {
|
|
let entryDescription = EntryDescription(
|
|
identifier: identifier,
|
|
secret: secret,
|
|
title: widgetService.serviceName,
|
|
subtitle: widgetService.serviceInfo,
|
|
iconTypeID: widgetService.iconTypeID,
|
|
serviceTypeID: widgetService.serviceTypeID,
|
|
digits: widgetService.digits,
|
|
period: widgetService.period,
|
|
algorithm: widgetService.algorithm,
|
|
tokenType: widgetService.tokenType,
|
|
iconType: widgetService.iconType,
|
|
labelTitle: widgetService.labelTitle,
|
|
labelColor: widgetService.labelColor
|
|
)
|
|
|
|
result[i] = .entry(entryDescription)
|
|
}
|
|
}
|
|
return result
|
|
}()
|
|
|
|
let calendar = Calendar.current
|
|
let halfMinute: Int = 30
|
|
|
|
let currentDate = Date()
|
|
let correctedDate = currentDate + Double(TimeOffsetStorage.offset ?? 0)
|
|
let seconds = calendar.component(.second, from: correctedDate)
|
|
let offset: Int = {
|
|
if seconds < halfMinute {
|
|
return halfMinute - seconds
|
|
}
|
|
return (2 * halfMinute) - seconds
|
|
}()
|
|
let upTo: Int = {
|
|
if slots < 9 {
|
|
// one hour of 30s
|
|
return 122 // 2 + 120 -> current (e.g. :17), next in :00 or :30 and next is normal :00 or :30
|
|
}
|
|
return 62
|
|
}()
|
|
|
|
for i in 0 ..< upTo {
|
|
let currentOffset: Int = {
|
|
if i == 0 {
|
|
return 0
|
|
} else if i == 1 {
|
|
return offset
|
|
}
|
|
return offset + halfMinute * (i - 1)
|
|
}()
|
|
let entryDate = calendar.date(byAdding: .second, value: currentOffset, to: currentDate)!
|
|
let tokenDate = calendar.date(byAdding: .second, value: currentOffset, to: correctedDate)!
|
|
let entriesList: [CodeEntry.Entry] = services.map { value in
|
|
guard case Values.entry(let entryDescription) = value
|
|
else { return CodeEntry.Entry.placeholder() }
|
|
|
|
let secondsToNewOne: Int = {
|
|
let period = entryDescription.period.rawValue
|
|
let currentSeconds: Int = calendar.component(.second, from: tokenDate)
|
|
if currentSeconds >= period {
|
|
return 2 * period - currentSeconds
|
|
}
|
|
return period - currentSeconds
|
|
}()
|
|
|
|
let countdownTo = calendar.date(byAdding: .second, value: secondsToNewOne, to: entryDate)!
|
|
let token = TokenGenerator.generateTOTP(
|
|
secret: entryDescription.secret,
|
|
time: tokenDate,
|
|
period: entryDescription.period,
|
|
digits: entryDescription.digits,
|
|
algoritm: entryDescription.algorithm,
|
|
tokenType: entryDescription.tokenType
|
|
)
|
|
let entryData = CodeEntry.EntryData(
|
|
id: entryDescription.identifier,
|
|
secret: entryDescription.secret,
|
|
name: entryDescription.title,
|
|
info: entryDescription.subtitle,
|
|
iconType: {
|
|
switch entryDescription.iconType {
|
|
case .brand: return .brand
|
|
case .label: return .label
|
|
}
|
|
}(),
|
|
labelTitle: entryDescription.labelTitle,
|
|
labelColor: entryDescription.labelColor,
|
|
iconTypeID: entryDescription.iconTypeID,
|
|
code: token.formattedValue(for: entryDescription.tokenType),
|
|
countdownTo: countdownTo,
|
|
rawEntry: .init(
|
|
secret: entryDescription.secret,
|
|
period: entryDescription.period.rawValue,
|
|
digits: entryDescription.digits.rawValue,
|
|
algorithm: entryDescription.algorithm.rawValue,
|
|
tokenType: entryDescription.tokenType.rawValue
|
|
)
|
|
)
|
|
return .init(kind: .singleEntry, data: entryData)
|
|
}
|
|
|
|
let entry = CodeEntry(date: entryDate, entries: entriesList)
|
|
entries.append(entry)
|
|
}
|
|
|
|
let timeline = Timeline(entries: entries, policy: .atEnd)
|
|
completion(timeline)
|
|
}
|
|
}
|