diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h index f6b81962..d0776490 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h @@ -96,6 +96,7 @@ namespace digidoc std::vector qcStatements() const; bool isCA() const; bool isValid(time_t *t = nullptr) const; + bool verify(bool noqscd, tm validation_time = {}) const; X509* handle() const; operator std::vector() const; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist index cb99f7de..a9d4f167 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.3.0 + 4.4.0 CFBundleSignature ???? CFBundleVersion - 46 + 48 CSResourcesFileMapped MinimumOSVersion diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp index 26358d78..8dae6632 100755 Binary files a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp and b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp differ diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h index f6b81962..d0776490 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h @@ -96,6 +96,7 @@ namespace digidoc std::vector qcStatements() const; bool isCA() const; bool isValid(time_t *t = nullptr) const; + bool verify(bool noqscd, tm validation_time = {}) const; X509* handle() const; operator std::vector() const; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist index cb99f7de..a9d4f167 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.3.0 + 4.4.0 CFBundleSignature ???? CFBundleVersion - 46 + 48 CSResourcesFileMapped MinimumOSVersion diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp index 7089aa70..de4899c2 100755 Binary files a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp and b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp differ diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h index ff85448e..59a67a4f 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h @@ -39,6 +39,9 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)libdigidocppVersion; + (NSString *)mediaType; ++ (void)extendLastSignatureToLTA:(NSString *)containerPath completion:(void (^)(NSError * _Nullable error))completion; ++ (void)extendAllSignaturesToLTA:(NSString *)containerPath completion:(void (^)(NSError * _Nullable error))completion; + @end NS_ASSUME_NONNULL_END diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm index b101549c..33ff0868 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm @@ -86,6 +86,7 @@ + (NSData *)getNSDataFromVector:(const std::vector&)vectorData { return [NSData dataWithBytes:vectorData.data() length:vectorData.size()]; } + + (DigiDocSignatureStatus)determineSignatureStatus:(int)status { typedef digidoc::Signature::Validator::Status Status; @@ -133,6 +134,16 @@ + (DigiDocSignature *)getSignature:(digidoc::Signature *)signature pos:(int)pos digiDocSignature.messageImprint = [NSData dataWithBytes:signature->messageImprint().data() length:signature->messageImprint().size()]; digiDocSignature.trustedSigningTime = [NSString stringWithUTF8String:signature->trustedSigningTime().c_str()]; + auto archiveTimestamps = signature->ArchiveTimeStamps(); + if (!archiveTimestamps.empty()) { + const auto& firstTS = archiveTimestamps.front(); + digiDocSignature.archiveTimestampTime = [NSString stringWithUTF8String:firstTS.time.c_str()]; + digiDocSignature.archiveTimestampCert = [DigiDocContainerWrapper getNSDataFromVector:firstTS.cert]; + } else { + digiDocSignature.archiveTimestampTime = @""; + digiDocSignature.archiveTimestampCert = [NSData data]; + } + std::vector signerRoles = signature->signerRoles(); NSMutableArray* signerRolesList = [NSMutableArray arrayWithCapacity: signerRoles.size()]; for (auto const& signerRole: signerRoles) { @@ -358,5 +369,31 @@ + (void)removeDataFileFromContainerWithPath:(NSString *)containerPath atIndex:(N } completion:completion]; } ++ (void)extendLastSignatureToLTA:(NSString *)containerPath completion:(void (^)(NSError * _Nullable error))completion { + [self open:containerPath validateOnline:YES command:^(digidoc::Container &container) { + auto sigs = container.signatures(); + if (!sigs.empty()) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + sigs.back()->extendSignatureProfile("time-stamp-archive"); +#pragma clang diagnostic pop + } + container.save(containerPath.UTF8String); + } completion:completion]; +} + ++ (void)extendAllSignaturesToLTA:(NSString *)containerPath completion:(void (^)(NSError * _Nullable error))completion { + [self open:containerPath validateOnline:YES command:^(digidoc::Container &container) { + auto sigs = container.signatures(); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + for (auto *sig : sigs) { + sig->extendSignatureProfile("time-stamp-archive"); + } +#pragma clang diagnostic pop + container.save(containerPath.UTF8String); + } completion:completion]; +} + @end diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h index da66b2f6..bdb2b350 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h @@ -52,5 +52,8 @@ typedef NS_ENUM(int, DigiDocSignatureStatus) { @property (nonatomic, assign) DigiDocSignatureStatus status; @property (nonatomic, strong) NSString *diagnosticsInfo; +@property (nonatomic, strong) NSString *archiveTimestampTime; +@property (nonatomic, strong) NSData *archiveTimestampCert; + @end diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift index ce3cc7c8..05c26365 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift @@ -263,6 +263,44 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { } } + @discardableResult + public func extendSignatureToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol { + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DigiDocContainerWrapper.extendLastSignature(toLTA: containerFile.resolvedPath) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + return try await open(containerFile: containerFile, isSivaConfirmed: true) + } catch { + let nsError = (error as NSError?) ?? NSError(domain: "ContainerWrapper - cannot extend signature to LTA", code: 8) + throw DigiDocError.signatureExtensionFailed(ErrorDetail(nsError: nsError)) + } + } + + @discardableResult + public func extendSignaturesToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol { + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DigiDocContainerWrapper.extendAllSignatures(toLTA: containerFile.resolvedPath) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + return try await open(containerFile: containerFile, isSivaConfirmed: true) + } catch { + let nsError = (error as NSError?) ?? NSError(domain: "ContainerWrapper - cannot extend signatures to LTA", code: 9) + throw DigiDocError.signatureExtensionFailed(ErrorDetail(nsError: nsError)) + } + } + private static func signatureStatusToDigiDocStatus(_ status: DigiDocSignatureStatus) -> SignatureStatus { switch status { case .Valid: @@ -339,7 +377,9 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { status: signatureStatusToDigiDocStatus(signature.status), format: signature.format, messageImprint: signature.messageImprint, - diagnosticsInfo: signature.diagnosticsInfo + diagnosticsInfo: signature.diagnosticsInfo, + archiveTimestampTime: signature.archiveTimestampTime, + archiveTimestampCert: signature.archiveTimestampCert ) } } diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift index 316f52b6..ef238947 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift @@ -39,6 +39,8 @@ public protocol ContainerWrapperProtocol: Sendable { userAgent: String ) async throws -> Data func addSignature(signature: Data, containerFile: URL) async throws -> ContainerWrapperProtocol + @discardableResult func extendSignatureToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol + @discardableResult func extendSignaturesToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol } extension ContainerWrapperProtocol { diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift index 5cb616cc..d8586404 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift @@ -52,6 +52,13 @@ public struct SignatureWrapper: Sendable, Identifiable, Hashable { public var status: SignatureStatus public var diagnosticsInfo: String + public var archiveTimestampTime: String + public var archiveTimestampCert: Data + + public var isLTAExtended: Bool { + !archiveTimestampCert.isEmpty + } + public init(pos: Int, signingCert: Data, timestampCert: Data, @@ -71,7 +78,9 @@ public struct SignatureWrapper: Sendable, Identifiable, Hashable { status: SignatureStatus = .unknown, format: String, messageImprint: Data, - diagnosticsInfo: String) { + diagnosticsInfo: String, + archiveTimestampTime: String = "", + archiveTimestampCert: Data = Data()) { self.pos = pos self.signingCert = signingCert self.timestampCert = timestampCert @@ -92,5 +101,7 @@ public struct SignatureWrapper: Sendable, Identifiable, Hashable { self.format = format self.messageImprint = messageImprint self.diagnosticsInfo = diagnosticsInfo + self.archiveTimestampTime = archiveTimestampTime + self.archiveTimestampCert = archiveTimestampCert } } diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift index a99ecdcd..0da82e19 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift @@ -31,6 +31,7 @@ public enum DigiDocError: Error { case signatureRemovingFailed(ErrorDetail) case dataFileRemovingFailed(ErrorDetail) case signatureAddingFailed(ErrorDetail) + case signatureExtensionFailed(ErrorDetail) public var errorDetail: ErrorDetail { switch self { @@ -43,7 +44,8 @@ public enum DigiDocError: Error { .containerDataFileSavingFailed(let errorDetail), .signatureRemovingFailed(let errorDetail), .dataFileRemovingFailed(let errorDetail), - .signatureAddingFailed(let errorDetail): + .signatureAddingFailed(let errorDetail), + .signatureExtensionFailed(let errorDetail): return errorDetail case .alreadyInitialized: diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift index e9056715..1e63272c 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift @@ -291,6 +291,42 @@ public actor SignedContainer: SignedContainerProtocol, Loggable { containerUtil: containerUtil ) } + + @discardableResult + public func extendSignature() async throws -> SignedContainerProtocol { + guard let containerFile else { + throw DigiDocError.signatureExtensionFailed( + ErrorDetail(message: "Cannot extend signature: container file is nil", code: 0) + ) + } + let containerWrapper = try await container.extendSignatureToLTA(containerFile: containerFile) + return SignedContainer( + containerFile: containerFile, + isExistingContainer: true, + container: containerWrapper, + timestamps: timestamps, + fileManager: fileManager, + containerUtil: containerUtil + ) + } + + @discardableResult + public func extendSignatures() async throws -> SignedContainerProtocol { + guard let containerFile else { + throw DigiDocError.signatureExtensionFailed( + ErrorDetail(message: "Cannot extend signatures: container file is nil", code: 0) + ) + } + let containerWrapper = try await container.extendSignaturesToLTA(containerFile: containerFile) + return SignedContainer( + containerFile: containerFile, + isExistingContainer: true, + container: containerWrapper, + timestamps: timestamps, + fileManager: fileManager, + containerUtil: containerUtil + ) + } } extension SignedContainer { diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift index a19741d4..94ad3f88 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift @@ -52,10 +52,19 @@ public protocol SignedContainerProtocol: GeneralContainer, Sendable { userAgent: String ) async throws -> Data func addSignature(signature: Data, containerFile: URL) async throws -> SignedContainerProtocol + @discardableResult func extendSignature() async throws -> SignedContainerProtocol + @discardableResult func extendSignatures() async throws -> SignedContainerProtocol } extension SignedContainerProtocol { func saveDataFile(dataFile: DataFileWrapper) async throws -> URL { try await saveDataFile(dataFile: dataFile, to: nil) } + + public func extendSignatureIfEnabled(_ enabled: Bool) async throws -> any SignedContainerProtocol { + guard enabled else { return self } + let mimetype = await getContainerMimetype() + guard mimetype != CommonsLib.Constants.MimeType.Ddoc else { return self } + return try await extendSignature() + } } diff --git a/RIADigiDoc/DI/AppContainer.swift b/RIADigiDoc/DI/AppContainer.swift index bc7eb482..afdb65c4 100644 --- a/RIADigiDoc/DI/AppContainer.swift +++ b/RIADigiDoc/DI/AppContainer.swift @@ -357,6 +357,15 @@ extension Container { } } + @MainActor + var ltaSettingsViewModel: Factory { + self { @MainActor in + LTASettingsViewModel( + dataStore: self.dataStore() + ) + } + } + var signatureUtil: Factory { self { SignatureUtil() } } diff --git a/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift b/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift index 1570e705..98bed9c5 100644 --- a/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift +++ b/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift @@ -20,4 +20,5 @@ enum SigningServicesSettingsViewTab: Int, Sendable { case timestampServices = 0 case mobileIdAndSmartId = 1 + case lta = 2 } diff --git a/RIADigiDoc/Domain/Preferences/DataStore.swift b/RIADigiDoc/Domain/Preferences/DataStore.swift index 185ad081..daf6b1fb 100644 --- a/RIADigiDoc/Domain/Preferences/DataStore.swift +++ b/RIADigiDoc/Domain/Preferences/DataStore.swift @@ -505,6 +505,16 @@ public actor DataStore: DataStoreProtocol { userDefaults().set(isAlwaysEnabled, forKey: Keys.isCrashlyticsAlwaysEnabled) } + // MARK: - LTA Settings + + public func getIsDefaultLTAEnabled() async -> Bool { + userDefaults().bool(forKey: Keys.isDefaultLTAEnabled) + } + + public func setIsDefaultLTAEnabled(_ isEnabled: Bool) async { + userDefaults().set(isEnabled, forKey: Keys.isDefaultLTAEnabled) + } + // MARK: - Migration public func getIsRecentDocumentsMigrationDone() async -> Bool { userDefaults().bool(forKey: Keys.isRecentDocumentsMigrationDone) @@ -581,5 +591,6 @@ public actor DataStore: DataStoreProtocol { static let isLogFileSaved = "isLogFileSaved" static let isCrashlyticsAlwaysEnabled = "isCrashlyticsAlwaysEnabled" static let isRecentDocumentsMigrationDone = "isRecentDocumentsMigrationDone" + static let isDefaultLTAEnabled = "isDefaultLTAEnabled" } } diff --git a/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift b/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift index c0591299..06f0dd51 100644 --- a/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift +++ b/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift @@ -123,6 +123,10 @@ public protocol DataStoreProtocol: Sendable { func getIsCrashlyticsAlwaysEnabled() async -> Bool func setIsCrashlyticsAlwaysEnabled(_ isEnabled: Bool) async + // MARK: - LTA Settings + func getIsDefaultLTAEnabled() async -> Bool + func setIsDefaultLTAEnabled(_ isEnabled: Bool) async + // MARK: - Migration func getIsRecentDocumentsMigrationDone() async -> Bool func setIsRecentDocumentsMigrationDone(_ isDone: Bool) async diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index 03b639f8..62869247 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -127,6 +127,78 @@ } } }, + "Archive timestamp" : { + "comment" : "Archive timestamp label in signature details", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive timestamp" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi ajatempel" + } + } + } + }, + "Archive timestamp valid until" : { + "comment" : "Archive timestamp valid until badge text", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive timestamp valid until: %@" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi ajatempel kehtib kuni: %@" + } + } + } + }, + "Archive TS Certificate" : { + "comment" : "Archive TS certificate label in signature details", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive TS Certificate" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi TS sertifikaat" + } + } + } + }, + "Archive TS Certificate issuer" : { + "comment" : "Archive TS certificate issuer label in signature details", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive TS Certificate issuer" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi TS sertifikaadi väljaandja" + } + } + } + }, "Authentication certificate" : { "comment" : "My eID PIN 1 title", "extractionState" : "manual", @@ -1691,6 +1763,78 @@ } } }, + "Extend" : { + "comment" : "Confirm button label in extend signatures dialog", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extend" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pikenda" + } + } + } + }, + "Extend signatures" : { + "comment" : "Button label for extending signatures to LTA", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extend signatures" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pikenda allkirju" + } + } + } + }, + "Extend signatures confirm message" : { + "comment" : "Confirmation dialog message shown before extending signatures to LTA", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All signatures in the container will be extended to LTA format." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kõik ümbriku allkirjad pikendatakse LTA formaati." + } + } + } + }, + "Extending signatures failed" : { + "comment" : "Shown as Toast when extending signatures to LTA fails", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to extend signature(s)" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allkirja(de) pikendamine ebaõnnestus" + } + } + } + }, "Failed mobile-ID transaction" : { "comment" : "Mobile-ID error for signatureHashMismatch", "extractionState" : "manual", @@ -4128,6 +4272,42 @@ } } }, + "Main settings default lta disabled" : { + "comment" : "Main settings default lta disabled", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do not use LTA format by default when signing" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei kasuta vaikimisi LTAd allkirjastamisel" + } + } + } + }, + "Main settings default lta title" : { + "comment" : "Main settings default lta title", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Use LTA format by default when signing" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kasuta vaikimisi LTAd allkirjastamisel" + } + } + } + }, "Main settings default manual access title" : { "comment" : "Main settings default manual access title", "extractionState" : "manual", @@ -4164,6 +4344,24 @@ } } }, + "Main settings lta tab title" : { + "comment" : "Tab title for LTA settings in Signing services", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "LTA" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "LTA" + } + } + } + }, "Main settings menu advanced" : { "comment" : "Main settings menu advanced", "extractionState" : "manual", @@ -7290,6 +7488,24 @@ } } }, + "Signatures extended" : { + "comment" : "Shown as Toast when extending signatures to LTA succeeds", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signature(s) extended successfully" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allkirja(de) pikendamine õnnestus" + } + } + } + }, "Signed at" : { "comment" : "Signed date and time", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift b/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift index 832fbd78..0ae11f50 100644 --- a/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift +++ b/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift @@ -24,10 +24,12 @@ struct ContainerNameBottomSheetActions { isSaveButtonShown: Bool = true, isSignButtonShown: Bool, isEncryptButtonShown: Bool, + isExtendSignaturesButtonShown: Bool, onRenameContainerButtonClick: @escaping () -> Void, onSaveContainerButtonClick: @escaping () -> Void, onSignContainerButtonClick: @escaping () -> Void, - onEncryptContainerButtonClick: @escaping () -> Void + onEncryptContainerButtonClick: @escaping () -> Void, + onExtendSignaturesClick: @escaping () -> Void ) -> [BottomSheetButton] { [ BottomSheetButton( @@ -59,6 +61,14 @@ struct ContainerNameBottomSheetActions { accessibilityLabel: "Sign document", showExtraIcon: true, onClick: onSignContainerButtonClick + ), + BottomSheetButton( + showButton: isExtendSignaturesButtonShown, + icon: "ic_m3_more_time_48pt_wght400", + title: "Extend signatures", + accessibilityLabel: "Extend signatures", + showExtraIcon: false, + onClick: onExtendSignaturesClick ) ] } diff --git a/RIADigiDoc/UI/Component/Container/ContainerNameView.swift b/RIADigiDoc/UI/Component/Container/ContainerNameView.swift index 65a52ef8..158d8b4e 100644 --- a/RIADigiDoc/UI/Component/Container/ContainerNameView.swift +++ b/RIADigiDoc/UI/Component/Container/ContainerNameView.swift @@ -41,6 +41,7 @@ struct ContainerNameView: View { let isSaveButtonShown: Bool let isSignButtonShown: Bool let isEncryptButtonShown: Bool + let isExtendSignaturesButtonShown: Bool let showLeftActionButton: Bool let showRightActionButton: Bool let leftActionButtonName: String @@ -53,6 +54,7 @@ struct ContainerNameView: View { let onRenameContainerButtonClick: () -> Void let onSignContainerButtonClick: () -> Void let onEncryptContainerButtonClick: () -> Void + let onExtendSignaturesClick: () -> Void private var bottomSheetActions: [BottomSheetButton] { ContainerNameBottomSheetActions.actions( @@ -60,10 +62,12 @@ struct ContainerNameView: View { isSaveButtonShown: isSaveButtonShown, isSignButtonShown: isSignButtonShown, isEncryptButtonShown: isEncryptButtonShown, + isExtendSignaturesButtonShown: isExtendSignaturesButtonShown, onRenameContainerButtonClick: onRenameContainerButtonClick, onSaveContainerButtonClick: onSaveContainerButtonClick, onSignContainerButtonClick: onSignContainerButtonClick, - onEncryptContainerButtonClick: onEncryptContainerButtonClick + onEncryptContainerButtonClick: onEncryptContainerButtonClick, + onExtendSignaturesClick: onExtendSignaturesClick ) } @@ -169,6 +173,7 @@ struct ContainerNameView: View { isSaveButtonShown: true, isSignButtonShown: true, isEncryptButtonShown: false, + isExtendSignaturesButtonShown: true, showLeftActionButton: true, showRightActionButton: true, leftActionButtonName: "Sign", @@ -180,7 +185,8 @@ struct ContainerNameView: View { onSaveContainerButtonClick: {}, onRenameContainerButtonClick: {}, onSignContainerButtonClick: {}, - onEncryptContainerButtonClick: {} + onEncryptContainerButtonClick: {}, + onExtendSignaturesClick: {} ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index cafd2b14..2af43f33 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -211,6 +211,7 @@ struct EncryptView: View { isSaveButtonShown: viewModel.isContainerEncrypted || viewModel.isContainerDecrypted, isSignButtonShown: viewModel.isSignButtonShown, isEncryptButtonShown: false, + isExtendSignaturesButtonShown: false, showLeftActionButton: false, showRightActionButton: viewModel.isEncryptButtonShown || viewModel.isDecryptButtonShown, @@ -262,6 +263,9 @@ struct EncryptView: View { }, onEncryptContainerButtonClick: { // Do nothing + }, + onExtendSignaturesClick: { + // Do nothing } ) .background( diff --git a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift index 5ec90543..90a556e8 100644 --- a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift @@ -117,6 +117,14 @@ struct SignatureDetailView: View { return "\(mobileTime.date) \(mobileTime.time)" } + var archiveTimestampTime: String { + let ts = DateUtil.getFormattedDateTime( + dateTimeString: signature.archiveTimestampTime, + isUTC: false + ) + return "\(ts.date) \(ts.time)" + } + init( certificateDetailViewModel: CertificateDetailViewModel = Container.shared.certificateDetailViewModel(), signature: SignatureWrapper, @@ -323,6 +331,36 @@ struct SignatureDetailView: View { value: signersMobileTime ) ) + + if !isTimestamp && signature.isLTAExtended { + SignerDetailView( + signatureDataItem: SignatureDataItem( + title: languageSettings.localized("Archive timestamp"), + value: archiveTimestampTime + ) + ) + + SignerDetailView( + signatureDataItem: SignatureDataItem( + title: languageSettings.localized("Archive TS Certificate issuer"), + value: viewModel.getIssuerName(cert: signature.archiveTimestampCert) + ) + ) + + NavigationLink( + value: NavigationDestination + .certificateDetailView(certificate: signature.archiveTimestampCert) + ) { + SignerDetailView( + signatureDataItem: SignatureDataItem( + title: languageSettings.localized("Archive TS Certificate"), + value: viewModel.getSubjectName(cert: signature.archiveTimestampCert), + extraIcon: "ic_m3_expand_content_48pt_wght400", + ) + ) + } + .buttonStyle(.plain) + } } } }) diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index 1156857f..7c572d15 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -121,6 +121,10 @@ struct IdCardView: View { languageSettings.localized("Signature added") } + private var extendingSignaturesFailedMessage: String { + languageSettings.localized("Extending signatures failed") + } + private var generalErrorMessage: String { languageSettings.localized("General error") } @@ -501,7 +505,11 @@ struct IdCardView: View { await MainActor.run { idCardActionMessage = "" - showMessage(message: signatureAddedMessage, type: .success) + if viewModel.signatureExtensionFailed { + showMessage(message: extendingSignaturesFailedMessage, type: .error) + } else { + showMessage(message: signatureAddedMessage, type: .success) + } onSuccess(container) dismiss() diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index 80bf779a..bc42bc31 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -102,6 +102,10 @@ struct NFCView: View { languageSettings.localized("Signature added") } + private var extendingSignaturesFailedMessage: String { + languageSettings.localized("Extending signatures failed") + } + let signedContainer: SignedContainerProtocol? let cryptoContainer: CryptoContainerProtocol? @@ -371,10 +375,16 @@ struct NFCView: View { return } - Toast.show(signatureAddedMessage, type: .success) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(signatureAddedMessage) + if viewModel.signatureExtensionFailed { + Toast.show(extendingSignaturesFailedMessage, type: .error) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(extendingSignaturesFailedMessage) + } + } else { + Toast.show(signatureAddedMessage, type: .success) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(signatureAddedMessage) + } } onSuccess(container) diff --git a/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift b/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift index 73ad7e49..e0e20c19 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift @@ -42,6 +42,7 @@ struct SignatureView: View { let isTimestamp: Bool let nameUtil: NameUtilProtocol let signatureUtil: SignatureUtilProtocol + let certificateUtil: CertificateUtilProtocol let showSignedDate: Bool let showMoreOptionsButton: Bool let showRole: Bool @@ -84,6 +85,7 @@ struct SignatureView: View { isTimestamp: Bool = false, nameUtil: NameUtilProtocol = Container.shared.nameUtil(), signatureUtil: SignatureUtilProtocol = Container.shared.signatureUtil(), + certificateUtil: CertificateUtilProtocol = Container.shared.certificateUtil(), showSignedDate: Bool = true, showMoreOptionsButton: Bool = true, showRole: Bool = true, @@ -98,6 +100,7 @@ struct SignatureView: View { self.isTimestamp = isTimestamp self.nameUtil = nameUtil self.signatureUtil = signatureUtil + self.certificateUtil = certificateUtil self.showSignedDate = showSignedDate self.showMoreOptionsButton = showMoreOptionsButton self.showRole = showRole @@ -106,6 +109,14 @@ struct SignatureView: View { self.onSelect = onSelect } + private var archiveTimestampInfo: (text: String, isExpired: Bool)? { + guard !isTimestamp && signature.isLTAExtended else { return nil } + guard let date = certificateUtil.getNotValidAfterDate(cert: signature.archiveTimestampCert) else { return nil } + let formatted = DateUtil.getFormattedDateTime(date: date, isUTC: false) + let text = languageSettings.localized("Archive timestamp valid until", [formatted.date]) + return (text: text, isExpired: date < Date()) + } + var body: some View { let signedDate = DateUtil.getFormattedDateTime( dateTimeString: signature.trustedSigningTime, @@ -147,7 +158,9 @@ struct SignatureView: View { text: languageSettings.localized( signatureUtil.getSignatureStatusText(status: signature.status) ), - status: signature.status + status: signature.status, + archiveTimestampText: archiveTimestampInfo?.text, + isArchiveTimestampExpired: archiveTimestampInfo?.isExpired ?? false ) .multilineTextAlignment(.center) diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index 806f8a53..87b58a47 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -48,6 +48,7 @@ struct SigningView: View { @State private var showRenameModal = false @State private var showRemoveSignatureModal = false @State private var showRemoveDataFileModal = false + @State private var showExtendSignaturesModal = false @State private var isImportingAddedFiles: Bool = false @State private var showingShareSheet = false @@ -143,6 +144,16 @@ struct SigningView: View { viewModel.isNestedContainer() } + private var isExtendSignaturesButtonShown: Bool { + isSignedContainer && !isNestedContainer && + !viewModel.isCadesContainer && !viewModel.isXadesContainer && + viewModel.containerMimetype != Constants.MimeType.Ddoc + } + + private var extendSignaturesLabel: String { + languageSettings.localized("Extend signatures") + } + private var containerName: String { URL(fileURLWithPath: viewModel.containerName) .deletingPathExtension() @@ -200,6 +211,7 @@ struct SigningView: View { isSaveButtonShown: true, isSignButtonShown: false, isEncryptButtonShown: !isContainerSigned && !isNestedContainer, + isExtendSignaturesButtonShown: isExtendSignaturesButtonShown, showLeftActionButton: isContainerSigned && isSignButtonShown, showRightActionButton: isContainerSigned && !isNestedContainer, leftActionButtonName: languageSettings.localized("Add signature"), @@ -237,6 +249,9 @@ struct SigningView: View { Task { await convertToCryptoContainer() } + }, + onExtendSignaturesClick: { + showExtendSignaturesModal = true } ) .background( @@ -458,9 +473,25 @@ struct SigningView: View { onCancel: { showRemoveDataFileModal = false } ) } + + if showExtendSignaturesModal { + ConfirmModalView( + title: languageSettings.localized("Extend signatures"), + message: languageSettings.localized("Extend signatures confirm message"), + confirmButtonTitle: languageSettings.localized("Extend"), + onConfirm: { + showExtendSignaturesModal = false + Task { + await viewModel.extendSignatures() + } + }, + onCancel: { showExtendSignaturesModal = false } + ) + } } .animation(.easeInOut, value: showRenameModal) .animation(.easeInOut, value: showRemoveSignatureModal) + .animation(.easeInOut, value: showExtendSignaturesModal) .onChange(of: viewModel.errorMessage) { _, error in guard let error else { return } let localizedMessage = languageSettings.localized(error.key, [error.args.joined(separator: ", ")]) @@ -481,6 +512,10 @@ struct SigningView: View { AccessibilityUtil.announceMessage(localizedMessage) } + if message.key == "Signatures extended" { + selectedTab = .signatures + } + viewModel.resetSuccessMessage() } .onChange(of: viewModel.navigateToNestedCryptoContainerView) { _, isNavigating in diff --git a/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift b/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift index 752cf75f..5066e8ed 100644 --- a/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift +++ b/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift @@ -26,6 +26,8 @@ struct ColoredSignedStatusText: View { let text: String let status: SignatureStatus + var archiveTimestampText: String? = nil + var isArchiveTimestampExpired: Bool = false private var isSignatureValidOrWarning: Bool { status == .valid || status == .warning || status == .nonQSCD @@ -49,12 +51,20 @@ struct ColoredSignedStatusText: View { } var body: some View { - TagBadge( - text: text, - tagBackgroundColor: tagBackgroundColor, - tagContentColor: tagContentColor, - additionalTextColor: additionalTextColor - ) + if let archiveText = archiveTimestampText { + TagBadge( + text: archiveText, + tagBackgroundColor: isArchiveTimestampExpired ? theme.errorContainer : theme.successContainer, + tagContentColor: isArchiveTimestampExpired ? theme.onErrorContainer : theme.onSuccessContainer + ) + } else { + TagBadge( + text: text, + tagBackgroundColor: tagBackgroundColor, + tagContentColor: tagContentColor, + additionalTextColor: additionalTextColor + ) + } } } @@ -73,4 +83,10 @@ struct ColoredSignedStatusText: View { text: "Signature is unknown", status: .unknown ) + + ColoredSignedStatusText( + text: "Signature is valid", + status: .valid, + archiveTimestampText: "Archive timestamp" + ) } diff --git a/RIADigiDoc/UI/Component/Shared/TagBadge.swift b/RIADigiDoc/UI/Component/Shared/TagBadge.swift index 20b8ff73..4a788b43 100644 --- a/RIADigiDoc/UI/Component/Shared/TagBadge.swift +++ b/RIADigiDoc/UI/Component/Shared/TagBadge.swift @@ -67,8 +67,9 @@ struct TagBadge: View { Text(parts[0]) .font(typography.bodyMedium) .lineLimit(nil) - .multilineTextAlignment(.center) + .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, Dimensions.Padding.XSPadding) .padding(.vertical, Dimensions.Padding.XXSPadding) .background(tagBackgroundColor) diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift index 1b5b5c42..70af197a 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift @@ -43,15 +43,19 @@ struct SigningServicesSettingsView: View { selectedTab: $selectedTab, titles: [ languageSettings.localized("Main settings timestamp services title"), - languageSettings.localized("Main settings mobile id and smart id title") + languageSettings.localized("Main settings mobile id and smart id title"), + languageSettings.localized("Main settings lta tab title") ], content: { if selectedTab == .timestampServices { TimeStampSettingsView() .padding(.horizontal, Dimensions.Padding.SPadding) - } else { + } else if selectedTab == .mobileIdAndSmartId { MobileIDSmartIDSettingsView() .padding(.horizontal, Dimensions.Padding.SPadding) + } else { + LTASettingsView() + .padding(.horizontal, Dimensions.Padding.SPadding) } } ) diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView/LTASettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView/LTASettingsView.swift new file mode 100644 index 00000000..f70df2e6 --- /dev/null +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView/LTASettingsView.swift @@ -0,0 +1,70 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import FactoryKit + +struct LTASettingsView: View { + @Environment(LanguageSettings.self) private var languageSettings + + @State private var viewModel: LTASettingsViewModel + + init() { + _viewModel = State(wrappedValue: Container.shared.ltaSettingsViewModel()) + } + + var body: some View { + AdvancedSettingsSectionColumn( + title: languageSettings.localized("Main settings lta tab title"), + isScrollable: false + ) { + OutlinedRadioButtonCard( + title: languageSettings.localized("Main settings default lta disabled"), + isSelected: !viewModel.isDefaultLTAEnabled, + onSelect: { + viewModel.isDefaultLTAEnabled = false + }, + accessibilityInputLabel: .disabledSetting + ) + OutlinedRadioButtonCard( + title: languageSettings.localized("Main settings default lta title"), + isSelected: viewModel.isDefaultLTAEnabled, + onSelect: { + viewModel.isDefaultLTAEnabled = true + }, + accessibilityInputLabel: .defaultSetting + ) + Spacer() + } + .onDisappear { + Task { + await viewModel.saveSettings() + } + } + } +} + +// MARK: - Preview + +#Preview { + LTASettingsView() + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) +} diff --git a/RIADigiDoc/Util/Certificate/CertificateUtil.swift b/RIADigiDoc/Util/Certificate/CertificateUtil.swift index e3d85bc2..2a8349ba 100644 --- a/RIADigiDoc/Util/Certificate/CertificateUtil.swift +++ b/RIADigiDoc/Util/Certificate/CertificateUtil.swift @@ -56,29 +56,30 @@ public struct CertificateUtil: CertificateUtilProtocol, Loggable { } public func getNotValidAfterWithExpiredLabel(cert: Data, expiredLabel: String) -> String { - do { - let certificate = try Certificate(derEncoded: cert.map { $0 }) - let notValidAfterDate = certificate.notValidAfter + guard let notValidAfterDate = getNotValidAfterDate(cert: cert) else { return "" } - let dateTime = DateUtil.getFormattedDateTime( - date: notValidAfterDate, - isUTC: false - ) + let dateTime = DateUtil.getFormattedDateTime(date: notValidAfterDate, isUTC: false) - let calendar = Calendar.current - let todayStart = calendar.startOfDay(for: Date()) - let certStart = calendar.startOfDay(for: notValidAfterDate) + let calendar = Calendar.current + let todayStart = calendar.startOfDay(for: Date()) + let certStart = calendar.startOfDay(for: notValidAfterDate) - if certStart < todayStart { - return "\(dateTime.date) (\(expiredLabel))" - } else { - return dateTime.date - } + if certStart < todayStart { + return "\(dateTime.date) (\(expiredLabel))" + } else { + return dateTime.date + } + } + + public func getNotValidAfterDate(cert: Data) -> Date? { + do { + let certificate = try Certificate(derEncoded: cert.map { $0 }) + return certificate.notValidAfter } catch { CertificateUtil.logger().error( - "Unable to get not valid after from certificate: \(String(reflecting: error))" + "Unable to get not valid after date from certificate: \(String(reflecting: error))" ) - return "" + return nil } } diff --git a/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift b/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift index bc2ad737..ec430383 100644 --- a/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift +++ b/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift @@ -27,4 +27,5 @@ public protocol CertificateUtilProtocol: Sendable { func getSubjectAttribute(cert: Data, attribute: ASN1ObjectIdentifier) -> String func getNotValidAfterWithExpiredLabel(cert: Data, expiredLabel: String) -> String func getNotValidDate(_ certData: Data?) throws -> String? + func getNotValidAfterDate(cert: Data) -> Date? } diff --git a/RIADigiDoc/ViewModel/LTASettingsViewModel.swift b/RIADigiDoc/ViewModel/LTASettingsViewModel.swift new file mode 100644 index 00000000..2cbcf353 --- /dev/null +++ b/RIADigiDoc/ViewModel/LTASettingsViewModel.swift @@ -0,0 +1,44 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation + +@Observable +@MainActor +class LTASettingsViewModel: LTASettingsViewModelProtocol { + + var isDefaultLTAEnabled: Bool = false + + private let dataStore: DataStoreProtocol + + init(dataStore: DataStoreProtocol) { + self.dataStore = dataStore + Task { + await loadSettings() + } + } + + func loadSettings() async { + self.isDefaultLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + } + + func saveSettings() async { + await dataStore.setIsDefaultLTAEnabled(isDefaultLTAEnabled) + } +} diff --git a/RIADigiDoc/ViewModel/Protocols/LTASettingsViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/LTASettingsViewModelProtocol.swift new file mode 100644 index 00000000..c2e9de35 --- /dev/null +++ b/RIADigiDoc/ViewModel/Protocols/LTASettingsViewModelProtocol.swift @@ -0,0 +1,27 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +/// @mockable +@MainActor +public protocol LTASettingsViewModelProtocol: Sendable { + var isDefaultLTAEnabled: Bool { get } + + func loadSettings() async + func saveSettings() async +} diff --git a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift index 9bbd858e..06182994 100644 --- a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift @@ -45,4 +45,5 @@ public protocol SigningViewModelProtocol: Sendable { func resetErrorMessage() func resetSuccessMessage() func convertToCryptoContainer() async -> Bool + func extendSignatures() async } diff --git a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift index 6b037c37..071cbf34 100644 --- a/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/IdCard/IdCardViewModel.swift @@ -37,6 +37,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { var errorMessage: String? var errorExtraArguments: [String] = [] + var signatureExtensionFailed = false var shouldDismissForError = false var showIdCardAlertMessage = false var idCardAlertMessageKey: String? @@ -140,6 +141,7 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { signedContainer: SignedContainerProtocol, roleData: RoleData ) async -> SignedContainerProtocol? { + signatureExtensionFailed = false do { let containerFile = await signedContainer.getRawContainerFile() ?? URL(fileURLWithPath: "") let pinSecureData = SecureData(Array(pin2.utf8)) @@ -183,10 +185,18 @@ class IdCardViewModel: IdCardViewModelProtocol, Loggable { pin2: pinSecureData ) - return try await signedContainer.addSignature( + let updatedContainer = try await signedContainer.addSignature( signature: signatureData, containerFile: containerFile ) + let isLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + do { + return try await updatedContainer.extendSignatureIfEnabled(isLTAEnabled) + } catch { + IdCardViewModel.logger().error("ID-CARD: Unable to extend signature: \(error)") + signatureExtensionFailed = true + return updatedContainer + } } catch { IdCardViewModel.logger().error( "ID-CARD: Unable to read ID-card data to sign with ID-Card reader. \(error)" diff --git a/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift b/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift index 0cd05bed..4c8afc3a 100644 --- a/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift @@ -239,8 +239,16 @@ class MobileIdViewModel: MobileIdViewModelProtocol, Loggable { ) MobileIdViewModel.logger().info("Signature added successfully (Mobile-ID)") - mobileIdSuccessMessageKey = "Signature added" - return updatedContainer + let isLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + do { + let extendedContainer = try await updatedContainer.extendSignatureIfEnabled(isLTAEnabled) + mobileIdSuccessMessageKey = "Signature added" + return extendedContainer + } catch { + MobileIdViewModel.logger().error("Unable to extend signature (Mobile-ID): \(error)") + mobileIdErrorMessageKey = "Extending signatures failed" + return updatedContainer + } } catch { MobileIdViewModel.logger().error("Unable to sign container with Mobile-ID: \(error)") handleSignatureAddingError(error) diff --git a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift index eea8e433..1d68a428 100644 --- a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift @@ -44,6 +44,8 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { var nfcAlertMessageExtraArguments: [String] = [] var nfcAlertMessageUrl: String? + var signatureExtensionFailed = false + private let nfcCANKeyFilename = Constants.File.nfcCANKey private let dataStore: DataStoreProtocol @@ -379,6 +381,7 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { strings: NFCSessionStrings ) async -> SignedContainerProtocol? { NFCViewModel.logger().info("NFC: Starting NFC signing") + signatureExtensionFailed = false let pin2Data = pin2.data(using: .utf8) guard let pin2Data else { NFCViewModel.logger().error("NFC: Failed to convert PIN2 to Data") @@ -408,7 +411,14 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { strings: strings ) NFCViewModel.logger().info("NFC: Signature added successfully") - return result + let isLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + do { + return try await result.extendSignatureIfEnabled(isLTAEnabled) + } catch { + NFCViewModel.logger().error("NFC: Unable to extend signature: \(error)") + signatureExtensionFailed = true + return result + } } catch { NFCViewModel.logger().error("NFC: Signing operation failed") diff --git a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift index b152c285..38ddbb7f 100644 --- a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift @@ -282,10 +282,18 @@ class SmartIdViewModel: SmartIdViewModelProtocol, Loggable { ) SmartIdViewModel.logger().info("Signature added successfully (Smart-ID)") - smartIdSuccessMessageKey = "Signature added" notificationUtil.removeNotification(id: notificationIdentifier) await endLiveActivity() - return updatedContainer + let isLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + do { + let extendedContainer = try await updatedContainer.extendSignatureIfEnabled(isLTAEnabled) + smartIdSuccessMessageKey = "Signature added" + return extendedContainer + } catch { + SmartIdViewModel.logger().error("Unable to extend signature (Smart-ID): \(error)") + smartIdErrorMessageKey = "Extending signatures failed" + return updatedContainer + } } catch { SmartIdViewModel.logger().error("Unable to sign container with Smart-ID: \(error)") handleSignatureAddingError(error) diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index 3096c174..2060e20f 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -582,6 +582,28 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { successMessage = nil } + func extendSignatures() async { + guard let container = signedContainer else { + SigningViewModel.logger().error("Cannot extend signatures: container is nil") + errorMessage = ToastMessage(key: "Extending signatures failed", args: []) + return + } + let mimetype = await container.getContainerMimetype() + guard mimetype != Constants.MimeType.Ddoc else { + SigningViewModel.logger().error("Cannot extend signatures: DDOC containers are not supported") + errorMessage = ToastMessage(key: "Extending signatures failed", args: []) + return + } + do { + let extended = try await container.extendSignatures() + await loadContainerData(signedContainer: extended) + successMessage = ToastMessage(key: "Signatures extended", args: []) + } catch { + SigningViewModel.logger().error("Unable to extend signatures: \(error)") + errorMessage = ToastMessage(key: "Extending signatures failed", args: []) + } + } + func convertToCryptoContainer() async -> Bool { do { guard let container = signedContainer else { diff --git a/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift b/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift index b993a655..3bcaa2aa 100644 --- a/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift +++ b/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift @@ -141,48 +141,6 @@ final class CertificateUtilTests { #expect(!notValidAfter.isEmpty) } - @Test - func getNotValidAfterWithExpiredLabel_successWithExpiredCert() { - let pemString = """ - -----BEGIN CERTIFICATE----- - MIID0jCCArqgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCRUUx - EzARBgNVBAgMClRlc3QgU3RhdGUxEjAQBgNVBAcMCVRlc3QgQ2l0eTEaMBgGA1UE - CgwRVGVzdCBPcmdhbml6YXRpb24xEjAQBgNVBAsMCVRlc3QgVW5pdDEZMBcGA1UE - AwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEyMzU5 - NTlaMIGBMQswCQYDVQQGEwJFRTETMBEGA1UECAwKVGVzdCBTdGF0ZTESMBAGA1UE - BwwJVGVzdCBDaXR5MRowGAYDVQQKDBFUZXN0IE9yZ2FuaXphdGlvbjESMBAGA1UE - CwwJVGVzdCBVbml0MRkwFwYDVQQDDBBUZXN0IENlcnRpZmljYXRlMIIBIjANBgkq - hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0tlBYafLZ4prdlTC+acvl+Z+p8oGxry - oRu3i/FIa8qAS/XHkL7DfmLdHkT3/N8Lclm1mQtVRtcmsMLbiPb6KiywlgZRWh4Z - JHS9t4WtcspxTjLjJ5DilmSPD1lepxCTq2VWECFPVSvh0Uo2Jr2WEWR0Az8MB0g6 - 2qfPz+ywNLKGjMMPOJEgEKpEylos/yU42qOja8r3Ocb2Bid7CbA8y3GzSZmIjS+X - GzmWqTe+4WaBTqAF3Wa5hhcVjbv9uYebgiF3puxYRGnqXR3wjxdH1Dt8VuP/cvic - ynDIEPltZbWIhLMkvIiJirtFQ2MWIJzyTgOg0EO1nFVzHBkn3OsUsQIDAQABo1Mw - UTAdBgNVHQ4EFgQUPcWKjZU/rXkRLqpssv/fUwpCkLAwHwYDVR0jBBgwFoAUPcWK - jZU/rXkRLqpssv/fUwpCkLAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsF - AAOCAQEAQbAPahc6zJ37VxBN4xDn3xXShCxVF+3qMBw7YYJDU39NSnBfCjpCZUbG - QTZAXc3iMB6luO5yoBbUSjX0YT6hqwAyb5s/2Aucv5Y8nLIl/GOAYYCWpZrFVkD0 - fmMt+rR6H5jFTtILsdsfMzmGkJ8xKWyjLUrGGALAzM1VSv1GPV+EpVjLe+bnTJ+E - mvPkvo7970DVF13AqjtE5929PJaMu4t7QHzUItdc74VOVkQ6OcC71nOWhPXlcVi2 - AzZ3Zprwro1aLDmCQRSjBi1git9957oxuQfCtGvoeaP497hWZ4wJK/HRHLlGx1cu - HorK9eEA1jaJ/RRRefXzhjOVHLOuYw== - -----END CERTIFICATE----- - """ - guard let pemData = pemString.data(using: .utf8) else { - Issue.record("Expected pemData to not be nil") - return - } - guard let derData = certificateUtil.pemToDerData(fromPEM: pemData) else { - Issue.record("Expected derData to not be nil") - return - } - let expiredLabel = "Expired" - - let notValidAfter = certificateUtil.getNotValidAfterWithExpiredLabel(cert: derData, expiredLabel: expiredLabel) - - #expect(notValidAfter.contains(expiredLabel)) - } - @Test func getSubjectAttribute_success() { let pemString = """ @@ -301,6 +259,103 @@ final class CertificateUtilTests { } } + @Test + func getNotValidAfterDate_returnsNonNilDateForValidCert() { + let pemString = """ + -----BEGIN CERTIFICATE----- + MIIEDTCCAvWgAwIBAgIUSqorLsfSI1K5t/9YhPnHqf3MBc4wDQYJKoZIhvcNAQEL + BQAwgZUxCzAJBgNVBAYTAkVFMQ4wDAYDVQQIDAVIYXJqdTEQMA4GA1UEBwwHVGFs + bGlubjEOMAwGA1UECgwFTXlPcmcxDzANBgNVBAsMBk15VW5pdDESMBAGA1UEAwwJ + VGVzdCBDZXJ0MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMQ4wDAYD + VQQFEwUxMjM0NTAeFw0yNTEwMTYxMDMyMjJaFw0yNjEwMTYxMDMyMjJaMIGVMQsw + CQYDVQQGEwJFRTEOMAwGA1UECAwFSGFyanUxEDAOBgNVBAcMB1RhbGxpbm4xDjAM + BgNVBAoMBU15T3JnMQ8wDQYDVQQLDAZNeVVuaXQxEjAQBgNVBAMMCVRlc3QgQ2Vy + dDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTEOMAwGA1UEBRMFMTIz + NDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWT6mHYaf1xuNus76z + MpVfk3HjI/ZxmswhbPG2LvAxldY7hXaCH8I0qMKorrnUqq3PmWZqG7Wzt78Lu5x8 + SGCJ+fGKH4Fo3cXnGqXQpU1xnwARE08N/g3GlogDH3y0MsbUD/B7Vq218BrWqlEU + BiYI/aO7yfal4tZjVWugBalMWYehHhEOeh0ss4bDjGvEmmPAvRa36UoVbLGrjG95 + vcZv2xCC8YlWyj11X4ci7RZHrbpNrZ21xWr59VU7dTxKIDJ64wfgddryXkiyPHJ3 + R5Syf89qNn0I9SeEuSS13QsF0UEmcT/+rvXf2o8JXNWpPe2AGYVzlWAPHboOKHLI + 2FILAgMBAAGjUzBRMB0GA1UdDgQWBBRLFCXwhwHQ2dmE3xocNJOtPB0DtzAfBgNV + HSMEGDAWgBRLFCXwhwHQ2dmE3xocNJOtPB0DtzAPBgNVHRMBAf8EBTADAQH/MA0G + CSqGSIb3DQEBCwUAA4IBAQB53+FGg8nzYBIq8K/C00GUB2R0XYxUvKsfvecMOcHy + Sl7TKOVZRDaL7Ji3G5CqouAwLFgnXqlf7aKYn4YfWNNXoS9Zm+eFJmvvWYJ/j/C0 + Ntz2mfcMcElrXvCGVnCNiHmkAw193jnya+3JxgbgE8rHoxYMHGwNTZUzCe7QGqw/ + tLdAYRezgyOx5NqaCq1GsOIP3n3eU9k92bMaWM0qtYHroL3H+oIvO0Whdsi2H7Fz + W+L77xnqmKNZDyWwyQ8MsShy8VAJt75TOLPrR6clKou1q3H77ELDtwUAHw7hJF7W + HERHoea8LiuAkZCFBh6fTEd2Wetgble1vYsK/+t+0Y4J + -----END CERTIFICATE----- + """ + guard let pemData = pemString.data(using: .utf8), + let derData = certificateUtil.pemToDerData(fromPEM: pemData) else { + Issue.record("Expected derData to not be nil") + return + } + + let result = certificateUtil.getNotValidAfterDate(cert: derData) + + #expect(result != nil) + } + + @Test + func getNotValidAfterDate_returnsExpiredDateForExpiredCert() { + let pemString = """ + -----BEGIN CERTIFICATE----- + MIID0jCCArqgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCRUUx + EzARBgNVBAgMClRlc3QgU3RhdGUxEjAQBgNVBAcMCVRlc3QgQ2l0eTEaMBgGA1UE + CgwRVGVzdCBPcmdhbml6YXRpb24xEjAQBgNVBAsMCVRlc3QgVW5pdDEZMBcGA1UE + AwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEyMzU5 + NTlaMIGBMQswCQYDVQQGEwJFRTETMBEGA1UECAwKVGVzdCBTdGF0ZTESMBAGA1UE + BwwJVGVzdCBDaXR5MRowGAYDVQQKDBFUZXN0IE9yZ2FuaXphdGlvbjESMBAGA1UE + CwwJVGVzdCBVbml0MRkwFwYDVQQDDBBUZXN0IENlcnRpZmljYXRlMIIBIjANBgkq + hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0tlBYafLZ4prdlTC+acvl+Z+p8oGxry + oRu3i/FIa8qAS/XHkL7DfmLdHkT3/N8Lclm1mQtVRtcmsMLbiPb6KiywlgZRWh4Z + JHS9t4WtcspxTjLjJ5DilmSPD1lepxCTq2VWECFPVSvh0Uo2Jr2WEWR0Az8MB0g6 + 2qfPz+ywNLKGjMMPOJEgEKpEylos/yU42qOja8r3Ocb2Bid7CbA8y3GzSZmIjS+X + GzmWqTe+4WaBTqAF3Wa5hhcVjbv9uYebgiF3puxYRGnqXR3wjxdH1Dt8VuP/cvic + ynDIEPltZbWIhLMkvIiJirtFQ2MWIJzyTgOg0EO1nFVzHBkn3OsUsQIDAQABo1Mw + UTAdBgNVHQ4EFgQUPcWKjZU/rXkRLqpssv/fUwpCkLAwHwYDVR0jBBgwFoAUPcWK + jZU/rXkRLqpssv/fUwpCkLAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsF + AAOCAQEAQbAPahc6zJ37VxBN4xDn3xXShCxVF+3qMBw7YYJDU39NSnBfCjpCZUbG + QTZAXc3iMB6luO5yoBbUSjX0YT6hqwAyb5s/2Aucv5Y8nLIl/GOAYYCWpZrFVkD0 + fmMt+rR6H5jFTtILsdsfMzmGkJ8xKWyjLUrGGALAzM1VSv1GPV+EpVjLe+bnTJ+E + mvPkvo7970DVF13AqjtE5929PJaMu4t7QHzUItdc74VOVkQ6OcC71nOWhPXlcVi2 + AzZ3Zprwro1aLDmCQRSjBi1git9957oxuQfCtGvoeaP497hWZ4wJK/HRHLlGx1cu + HorK9eEA1jaJ/RRRefXzhjOVHLOuYw== + -----END CERTIFICATE----- + """ + guard let pemData = pemString.data(using: .utf8), + let derData = certificateUtil.pemToDerData(fromPEM: pemData) else { + Issue.record("Expected derData to not be nil") + return + } + + let result = certificateUtil.getNotValidAfterDate(cert: derData) + + guard let date = result else { + Issue.record("Expected date to not be nil") + return + } + #expect(date < Date()) + } + + @Test + func getNotValidAfterDate_returnsNilOnInvalidDer() { + let invalidDer = Data([0x00, 0x01, 0x02]) + + let result = certificateUtil.getNotValidAfterDate(cert: invalidDer) + + #expect(result == nil) + } + + @Test + func getNotValidAfterDate_returnsNilOnEmptyData() { + let result = certificateUtil.getNotValidAfterDate(cert: Data()) + + #expect(result == nil) + } + @Test func getNotValidDate_success() throws { let pemString = """ diff --git a/RIADigiDocTests/ViewModel/LTASettingsViewModelTests.swift b/RIADigiDocTests/ViewModel/LTASettingsViewModelTests.swift new file mode 100644 index 00000000..75074a0f --- /dev/null +++ b/RIADigiDocTests/ViewModel/LTASettingsViewModelTests.swift @@ -0,0 +1,81 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import Testing + +@MainActor +final class LTASettingsViewModelTests { + + private let mockDataStore: DataStoreProtocolMock + + init() { + mockDataStore = DataStoreProtocolMock() + mockDataStore.getIsDefaultLTAEnabledHandler = { false } + } + + @Test + func isDefaultLTAEnabled_defaultsToFalse() async { + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + + #expect(!viewModel.isDefaultLTAEnabled) + } + + @Test + func loadSettings_setsIsDefaultLTAEnabledToTrue() async { + mockDataStore.getIsDefaultLTAEnabledHandler = { true } + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + + await viewModel.loadSettings() + + #expect(viewModel.isDefaultLTAEnabled) + } + + @Test + func loadSettings_setsIsDefaultLTAEnabledToFalse() async { + mockDataStore.getIsDefaultLTAEnabledHandler = { false } + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + + await viewModel.loadSettings() + + #expect(!viewModel.isDefaultLTAEnabled) + } + + @Test + func saveSettings_savesEnabledStateToDataStore() async { + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + viewModel.isDefaultLTAEnabled = true + + await viewModel.saveSettings() + + #expect(mockDataStore.setIsDefaultLTAEnabledCallCount == 1) + #expect(mockDataStore.setIsDefaultLTAEnabledArgValues.first == true) + } + + @Test + func saveSettings_savesDisabledStateToDataStore() async { + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + viewModel.isDefaultLTAEnabled = false + + await viewModel.saveSettings() + + #expect(mockDataStore.setIsDefaultLTAEnabledCallCount == 1) + #expect(mockDataStore.setIsDefaultLTAEnabledArgValues.first == false) + } +} diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index c6a2de2b..619895b0 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -1472,6 +1472,74 @@ struct SigningViewModelTests: Loggable { #expect(viewModel.dataFiles.count == 2) } + @Test + func extendSignatures_setsSuccessMessageWhenExtensionSucceeds() async { + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.extendSignaturesHandler = { mockSignedContainer } + mockSignedContainer.getDataFilesHandler = { [] } + mockSignedContainer.getSignaturesHandler = { [] } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.successMessage == ToastMessage(key: "Signatures extended", args: [])) + #expect(viewModel.errorMessage == nil) + } + + @Test + func extendSignatures_setsErrorMessageWhenContainerIsNil() async { + await viewModel.extendSignatures() + + #expect(viewModel.errorMessage == ToastMessage(key: "Extending signatures failed", args: [])) + #expect(viewModel.successMessage == nil) + } + + @Test + func extendSignatures_setsErrorMessageWhenContainerIsDdoc() async { + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Ddoc } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.errorMessage == ToastMessage(key: "Extending signatures failed", args: [])) + #expect(viewModel.successMessage == nil) + #expect(mockSignedContainer.extendSignaturesCallCount == 0) + } + + @Test + func extendSignatures_setsErrorMessageWhenExtensionThrows() async { + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.extendSignaturesHandler = { + throw DigiDocError.signatureExtensionFailed(ErrorDetail(message: "Extension failed")) + } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.errorMessage == ToastMessage(key: "Extending signatures failed", args: [])) + #expect(viewModel.successMessage == nil) + } + + @Test + func extendSignatures_loadsContainerDataAfterSuccessfulExtension() async { + let extendedContainer = SignedContainerProtocolMock() + let signature = MockSignatureWrapper.mockSignatureWrapper(signatureId: "S-extended") + extendedContainer.getDataFilesHandler = { [] } + extendedContainer.getSignaturesHandler = { [signature] } + + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.extendSignaturesHandler = { extendedContainer } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.signatures.first?.signatureId == "S-extended") + } + @Test func convertToCryptoContainer_successWithExistingContainer() async throws { let mockSignedContainer = SignedContainerProtocolMock()