Skip to content

Commit

Permalink
#2644 Password protected message compliance (#2645)
Browse files Browse the repository at this point in the history
* feat: password protected message compliance

* fix: pr reviews
  • Loading branch information
ioanmo226 authored Dec 13, 2024
1 parent 52edb18 commit 010e051
Show file tree
Hide file tree
Showing 20 changed files with 425 additions and 67 deletions.
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
2 changes: 1 addition & 1 deletion FlowCrypt/Controllers/Compose/ComposeViewController.swift
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?

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
33 changes: 33 additions & 0 deletions FlowCryptAppTests/Functionality/PGP/PasswordProtectedMsgTest.swift
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

0 comments on commit 010e051

Please sign in to comment.