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

#2644 Password protected message compliance #2645

Merged
merged 2 commits into from
Dec 13, 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
12 changes: 12 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -420,9 +420,12 @@
D2FD0F692453245E00259FF0 /* Either.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FD0F682453245E00259FF0 /* Either.swift */; };
D2FF6966243115EC007182F0 /* SetupImapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6965243115EC007182F0 /* SetupImapViewController.swift */; };
D2FF6968243115F9007182F0 /* SetupImapViewDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FF6967243115F9007182F0 /* SetupImapViewDecorator.swift */; };
D717EC262D06B37E00AE7BFF /* CustomAlertNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717EC252D06B37900AE7BFF /* CustomAlertNode.swift */; };
D73F7D9D2CD46AE900955806 /* PgpOnlySwitchNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73F7D9C2CD46AE700955806 /* PgpOnlySwitchNode.swift */; };
D73F7D9F2CD4922500955806 /* NotificationExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73F7D9E2CD4922100955806 /* NotificationExtension.swift */; };
D741F9B22CA5661C00E1CAFF /* SecurityWarningNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D741F9B12CA5661400E1CAFF /* SecurityWarningNode.swift */; };
D7478BDE2D09113100D42659 /* PasswordProtectedMsgTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7478BDD2D09112500D42659 /* PasswordProtectedMsgTest.swift */; };
D7478BE02D0912E600D42659 /* CoreAlertNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7478BDF2D0912E200D42659 /* CoreAlertNode.swift */; };
F191F621272511790053833E /* BlurViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F191F620272511790053833E /* BlurViewController.swift */; };
F8678DCC2722143300BB1710 /* GmailService+draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8678DCB2722143300BB1710 /* GmailService+draft.swift */; };
F8A72FA12729F82800E4BCAB /* DraftGatewayMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A72FA02729F82800E4BCAB /* DraftGatewayMock.swift */; };
Expand Down Expand Up @@ -889,9 +892,12 @@
D2FD0F682453245E00259FF0 /* Either.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Either.swift; sourceTree = "<group>"; };
D2FF6965243115EC007182F0 /* SetupImapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupImapViewController.swift; sourceTree = "<group>"; };
D2FF6967243115F9007182F0 /* SetupImapViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupImapViewDecorator.swift; sourceTree = "<group>"; };
D717EC252D06B37900AE7BFF /* CustomAlertNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertNode.swift; sourceTree = "<group>"; };
D73F7D9C2CD46AE700955806 /* PgpOnlySwitchNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PgpOnlySwitchNode.swift; sourceTree = "<group>"; };
D73F7D9E2CD4922100955806 /* NotificationExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationExtension.swift; sourceTree = "<group>"; };
D741F9B12CA5661400E1CAFF /* SecurityWarningNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWarningNode.swift; sourceTree = "<group>"; };
D7478BDD2D09112500D42659 /* PasswordProtectedMsgTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordProtectedMsgTest.swift; sourceTree = "<group>"; };
D7478BDF2D0912E200D42659 /* CoreAlertNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreAlertNode.swift; sourceTree = "<group>"; };
E26D5E20275AA417007B8802 /* BundleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtensions.swift; sourceTree = "<group>"; };
F191F620272511790053833E /* BlurViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurViewController.swift; sourceTree = "<group>"; };
F8678DCB2722143300BB1710 /* GmailService+draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GmailService+draft.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1502,6 +1508,7 @@
9F4164162665757700106194 /* PGP */ = {
isa = PBXGroup;
children = (
D7478BDD2D09112500D42659 /* PasswordProtectedMsgTest.swift */,
9FA0157926565B7800CBBA05 /* KeyMethodsTest.swift */,
);
path = PGP;
Expand Down Expand Up @@ -2261,6 +2268,7 @@
D2A1D3D223FD9AE600D626D6 /* Nodes */ = {
isa = PBXGroup;
children = (
D7478BDF2D0912E200D42659 /* CoreAlertNode.swift */,
D73F7D9C2CD46AE700955806 /* PgpOnlySwitchNode.swift */,
9F7ECCA6272C3FB4008A1770 /* TextImageNode.swift */,
51EBC56F2746A06600178DE8 /* TextWithIconNode.swift */,
Expand All @@ -2282,6 +2290,7 @@
51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */,
51B9EE6E27567B520080B2D5 /* MessageRecipientsNode.swift */,
9547EF202A5F106E00A048FF /* PassPhraseAlertNode.swift */,
D717EC252D06B37900AE7BFF /* CustomAlertNode.swift */,
95E014CE2A8BF27C00D4B4F5 /* AvatarCheckboxNode.swift */,
9577CEDC2AA7A4A40084AC62 /* PublicKeyDetailNode.swift */,
955475FA2B0650AC00F52076 /* WebNode.swift */,
Expand Down Expand Up @@ -2618,6 +2627,7 @@
9F5F504A26FA6C8F00294FA2 /* ClientConfigurationProviderMock.swift in Sources */,
9FC4117D268118AE004C0A69 /* PassPhraseStorageMock.swift in Sources */,
9F97650E267E16620058419D /* WKDURLsConstructorTests.swift in Sources */,
D7478BDE2D09113100D42659 /* PasswordProtectedMsgTest.swift in Sources */,
9F976585267E194F0058419D /* FlowCryptCoreTests.swift in Sources */,
9F6F3C3C26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift in Sources */,
9FC4116B2681186D004C0A69 /* KeyMethodsTest.swift in Sources */,
Expand Down Expand Up @@ -2899,11 +2909,13 @@
D211CE7123FC35AC00D1CE38 /* TextFieldCellNode.swift in Sources */,
D73F7D9D2CD46AE900955806 /* PgpOnlySwitchNode.swift in Sources */,
9FA19890253C841F008C9CF2 /* TableViewController.swift in Sources */,
D717EC262D06B37E00AE7BFF /* CustomAlertNode.swift in Sources */,
51DAD9BD273E7DD20076CBA7 /* BadgeNode.swift in Sources */,
51DE2FEE2714DA0400916222 /* ContactKeyCellNode.swift in Sources */,
9547EF242A5FBA2B00A048FF /* MenuSeparatorCellNode.swift in Sources */,
D2A9CA432426210200E1D898 /* SetupTitleNode.swift in Sources */,
D2F6D12F24324ACC00DB4065 /* SwitchCellNode.swift in Sources */,
D7478BE02D0912E600D42659 /* CoreAlertNode.swift in Sources */,
51EBC5702746A06600178DE8 /* TextWithIconNode.swift in Sources */,
5180CB97273724E9001FC7EF /* ThreadMessageInfoCellNode.swift in Sources */,
D2E26F6824F169E300612AF1 /* ContactCellNode.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ final class ComposeViewController: TableNodeViewController {
let photosManager: PhotosManagerType
let router: GlobalRouterType

private let clientConfiguration: ClientConfiguration
let clientConfiguration: ClientConfiguration
var isMessagePasswordSupported: Bool { clientConfiguration.isUsingFes }

let search = PassthroughSubject<String, Never>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ extension ComposeViewController {
contextToSend.hasRecipientsWithActivePubKey

let sendableMsg = try await composeMessageHelper.createSendableMsg(
clientConfiguration: clientConfiguration,
input: draft.input,
contextToSend: draft.contextToSend,
shouldValidate: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ extension ComposeViewController {
} else {
refreshEKMAndProceed(error: error)
}
case let MessageValidationError.messagePasswordDisallowed(error):
alertsFactory.makeCustomAlert(viewController: self, message: error)
case MessageValidationError.notUniquePassword,
MessageValidationError.subjectContainsPassword,
MessageValidationError.weakPassword:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ extension ComposeViewController {

private func sendMessage(isPlain: Bool) async throws -> MessageIdentifier {
let sendableMsg = try await composeMessageHelper.createSendableMsg(
clientConfiguration: clientConfiguration,
input: input,
contextToSend: contextToSend,
shouldSendPlainMessage: isPlain,
shouldSign: !isPlain,
withPubKeys: !isPlain
)
Expand Down
19 changes: 19 additions & 0 deletions FlowCrypt/Controllers/Threads/Models/AlertsFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ class AlertsFactory {

viewController.present(alertViewController, animated: true, completion: nil)
}

func makeCustomAlert(
viewController: UIViewController,
title: String = "error".localized,
message: String
) {
let alertNode = CustomAlertNode(
title: title,
message: message
)
alertNode.onOkay = {
viewController.dismiss(animated: true)
}
let alertViewController = ASDKViewController(node: alertNode)
alertViewController.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
alertViewController.modalTransitionStyle = UIModalTransitionStyle.crossDissolve

viewController.present(alertViewController, animated: true, completion: nil)
}
}

class SubmitOnPasteTextFieldDelegate: NSObject, UITextFieldDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ final class EncryptedStorage: EncryptedStorageType {
case version15
case version16
case version17
case version18

var version: SchemaVersion {
switch self {
Expand Down Expand Up @@ -84,14 +85,16 @@ final class EncryptedStorage: EncryptedStorageType {
return SchemaVersion(appVersion: "1.2.3", dbSchemaVersion: 16)
case .version17:
return SchemaVersion(appVersion: "1.3.0", dbSchemaVersion: 17)
case .version18:
return SchemaVersion(appVersion: "1.3.0", dbSchemaVersion: 18)
}
}
}

private lazy var migrationLogger = Logger.nested(in: Self.self, with: .migration)
private lazy var logger = Logger.nested(Self.self)

private let currentSchema: EncryptedStorageSchema = .version17
private let currentSchema: EncryptedStorageSchema = .version18
private let supportedSchemas = EncryptedStorageSchema.allCases

private let storageEncryptionKey: Data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ class ClientConfiguration {
raw.enforceKeygenAlgo
}

/// An array of strings to check against the subject of the composed password-protected message. If any string in this array is found in the subject, an error alert must be displayed
var disallowPasswordMessagesForTerms: [String]? {
raw.disallowPasswordMessagesForTerms
}

/// The text to be displayed in the password protected message compliance error alert.
var disallowPasswordMessagesErrorText: String? {
raw.disallowPasswordMessagesErrorText
}

/// Some orgs want to have newly generated keys include self-signatures that expire some time in the future.
var getEnforcedKeygenExpirationMonths: Int? {
raw.enforceKeygenExpireMonths
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable {
case expiredKeyRecipients
case notUsableForEncryptionKeyRecipients
case invalidEmailRecipient
case messagePasswordDisallowed(String)
case internalError(String)

var description: String {
Expand Down Expand Up @@ -49,6 +50,8 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable {
return "compose_recipient_unusuable_for_encryption".localized
case .invalidEmailRecipient:
return "compose_recipient_invalid_email".localized
case let .messagePasswordDisallowed(message):
return message
case let .internalError(message):
return message
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ final class ComposeMessageHelper {
// MARK: - Validation
// swiftlint:disable:next function_body_length
func createSendableMsg(
clientConfiguration: ClientConfiguration,
input: ComposeMessageInput,
contextToSend: ComposeMessageContext,
shouldSendPlainMessage: Bool = true,
shouldValidate: Bool = true,
shouldSign: Bool = true,
withPubKeys: Bool = true
Expand Down Expand Up @@ -136,6 +138,14 @@ final class ComposeMessageHelper {
throw MessageValidationError.emptyMessage
}

if !shouldSendPlainMessage,
contextToSend.hasRecipientsWithoutPubKey,
let termsToDisallow = clientConfiguration.disallowPasswordMessagesForTerms,
let errorText = clientConfiguration.disallowPasswordMessagesErrorText,
!subject.isPasswordMessageEnabled(disallowTerms: termsToDisallow) {
throw MessageValidationError.messagePasswordDisallowed(errorText)
}

if let password = contextToSend.messagePassword, password.isNotEmpty {
if subject.lowercased().contains(password.lowercased()) {
throw MessageValidationError.subjectContainsPassword
Expand Down
14 changes: 12 additions & 2 deletions FlowCrypt/Models/Common/RawClientConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ struct RawClientConfiguration: Codable, Equatable {
let disallowAttesterSearchForDomains: [String]?
let enforceKeygenAlgo: String?
let enforceKeygenExpireMonths: Int?
let disallowPasswordMessagesForTerms: [String]?
let disallowPasswordMessagesErrorText: String?

init(
flags: [ClientConfigurationFlag]? = nil,
Expand All @@ -50,7 +52,9 @@ struct RawClientConfiguration: Codable, Equatable {
allowAttesterSearchOnlyForDomains: [String]? = nil,
disallowAttesterSearchForDomains: [String]? = nil,
enforceKeygenAlgo: String? = nil,
enforceKeygenExpireMonths: Int? = nil
enforceKeygenExpireMonths: Int? = nil,
disallowPasswordMessagesForTerms: [String]? = nil,
disallowPasswordMessagesErrorText: String? = nil
) {
self.flags = flags
self.customKeyserverUrl = customKeyserverUrl
Expand All @@ -61,6 +65,8 @@ struct RawClientConfiguration: Codable, Equatable {
self.disallowAttesterSearchForDomains = disallowAttesterSearchForDomains
self.enforceKeygenAlgo = enforceKeygenAlgo
self.enforceKeygenExpireMonths = enforceKeygenExpireMonths
self.disallowPasswordMessagesForTerms = disallowPasswordMessagesForTerms
self.disallowPasswordMessagesErrorText = disallowPasswordMessagesErrorText
}
}

Expand Down Expand Up @@ -96,7 +102,11 @@ extension RawClientConfiguration {
try JSONDecoder().decode([String].self, from: $0)
},
enforceKeygenAlgo: unwrappedObject.enforceKeygenAlgo,
enforceKeygenExpireMonths: unwrappedObject.enforceKeygenExpireMonths
enforceKeygenExpireMonths: unwrappedObject.enforceKeygenExpireMonths,
disallowPasswordMessagesForTerms: try? object?.disallowPasswordMessagesForTerms.ifNotNil {
try JSONDecoder().decode([String].self, from: $0)
},
disallowPasswordMessagesErrorText: unwrappedObject.disallowPasswordMessagesErrorText
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ final class ClientConfigurationRealmObject: Object {
@Persisted var disallowAttesterSearchForDomains: Data?
@Persisted var enforceKeygenAlgo: String?
@Persisted var enforceKeygenExpireMonths: Int
@Persisted var disallowPasswordMessagesForTerms: Data?
@Persisted var disallowPasswordMessagesErrorText: String?
ioanmo226 marked this conversation as resolved.
Show resolved Hide resolved

convenience init(
flags: [String]?,
Expand All @@ -30,6 +32,8 @@ final class ClientConfigurationRealmObject: Object {
disallowAttesterSearchForDomains: [String]?,
enforceKeygenAlgo: String?,
enforceKeygenExpireMonths: Int?,
disallowPasswordMessagesForTerms: [String]?,
disallowPasswordMessagesErrorText: String?,
email: String
) {
self.init()
Expand All @@ -44,6 +48,8 @@ final class ClientConfigurationRealmObject: Object {
self.disallowAttesterSearchForDomains = try? disallowAttesterSearchForDomains.ifNotNil { try JSONEncoder().encode($0) }
self.enforceKeygenAlgo = enforceKeygenAlgo
self.enforceKeygenExpireMonths = enforceKeygenExpireMonths ?? -1
self.disallowPasswordMessagesForTerms = try? disallowPasswordMessagesForTerms.ifNotNil { try JSONEncoder().encode($0) }
self.disallowPasswordMessagesErrorText = disallowPasswordMessagesErrorText
self.userEmail = email
}
}
Expand All @@ -60,6 +66,8 @@ extension ClientConfigurationRealmObject {
disallowAttesterSearchForDomains: configuration.disallowAttesterSearchForDomains,
enforceKeygenAlgo: configuration.enforceKeygenAlgo,
enforceKeygenExpireMonths: configuration.enforceKeygenExpireMonths,
disallowPasswordMessagesForTerms: configuration.disallowPasswordMessagesForTerms,
disallowPasswordMessagesErrorText: configuration.disallowPasswordMessagesErrorText,
email: email
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// PasswordProtectedMsgTest.swift
// FlowCryptTests
//
// Created by Ioan Moldovan on 20.05.2021.
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

@testable import FlowCrypt
import XCTest

class PasswordProtectedMsgTest: XCTestCase {

func testPasswordProtectedMessageCompliance() {
let disallowTerms = ["[Classification: Data Control: Internal Data Control]", "droid", "forbidden data"]

let subjectsToTest: [String: Bool] = [
"[Classification: Data Control: Internal Data Control] Quarter results": false,
"Conference information [Classification: Data Control: Internal Data Control]": false,
"Classification: Data Control: Internal Data Control - Tomorrow meeting": true,
"Internal Data Control - Finance monitoring": true,
"Android phone update": true,
"droid phone": false,
"DROiD phone": false,
"[forbidden data] year results": false,
]

for (subject, expectedValue) in subjectsToTest {
let result = subject.isPasswordMessageEnabled(disallowTerms: disallowTerms)
XCTAssertEqual(result, expectedValue, "Failed for subject: \(subject)")
}
}
}
28 changes: 28 additions & 0 deletions FlowCryptCommon/Extensions/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,32 @@ public extension String {
guard parts.count == 2 else { return nil }
return (String(parts[0]), String(parts[1]))
}

func isPasswordMessageEnabled(disallowTerms: [String]) -> Bool {
// Allow by default if subject is nil, disallowTerms is empty, or no terms are specified
guard disallowTerms.isNotEmpty else {
return true
}

let lowerSubject = self.lowercased()

for term in disallowTerms {
// Escape special regex characters
let escapedTerm = NSRegularExpression.escapedPattern(for: term)

// Create regex pattern to ensure term appears as a separate token
let pattern = #"(^|\W)\#(escapedTerm)(\W|$)"#

guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
continue
}

let range = NSRange(lowerSubject.startIndex..., in: lowerSubject)
if regex.firstMatch(in: lowerSubject, range: range) != nil {
// Found a disallowed term as a separate token
return false
}
}
return true // Allow if no matches are found
}
}
Loading
Loading