Skip to content
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
4 changes: 2 additions & 2 deletions .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ agent:
type: a1-standard-4
os_image: macos-xcode13
execution_time_limit:
minutes: 90
minutes: 120
auto_cancel:
running:
when: branch != 'master'
Expand All @@ -15,7 +15,7 @@ blocks:
run:
when: 'change_in(''/'', {exclude: [''/Core/package.json'', ''/Core/package-lock.json'']})'
execution_time_limit:
minutes: 85
minutes: 115
task:
secrets:
- name: flowcrypt-ios-ci-secrets
Expand Down
8 changes: 4 additions & 4 deletions FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 17 additions & 6 deletions FlowCrypt/Controllers/Compose/ComposeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ final class ComposeViewController: TableNodeViewController {
self.composeMessageService = composeMessageService ?? ComposeMessageService(
clientConfiguration: clientConfiguration,
encryptedStorage: appContext.encryptedStorage,
messageGateway: appContext.getRequiredMailProvider().messageSender
messageGateway: appContext.getRequiredMailProvider().messageSender,
passPhraseService: appContext.passPhraseService,
sender: email
)
self.filesManager = filesManager
self.photosManager = photosManager
Expand Down Expand Up @@ -271,7 +273,6 @@ extension ComposeViewController {
let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg(
input: input,
contextToSend: contextToSend,
email: email,
includeAttachments: false,
signingPrv: signingPrv
)
Expand Down Expand Up @@ -507,7 +508,6 @@ extension ComposeViewController {
let sendableMsg = try await self.composeMessageService.validateAndProduceSendableMsg(
input: self.input,
contextToSend: self.contextToSend,
email: self.email,
signingPrv: signingKey
)
UIApplication.shared.isIdleTimerDisabled = true
Expand All @@ -527,8 +527,17 @@ extension ComposeViewController {
DispatchQueue.main.asyncAfter(deadline: .now() + hideSpinnerAnimationDuration) { [weak self] in
guard let self = self else { return }

if case MessageValidationError.noPubRecipients = error, self.isMessagePasswordSupported {
self.setMessagePassword()
if self.isMessagePasswordSupported {
switch error {
case MessageValidationError.noPubRecipients:
self.setMessagePassword()
case MessageValidationError.notUniquePassword,
MessageValidationError.subjectContainsPassword,
MessageValidationError.weakPassword:
self.showAlert(message: error.errorMessage)
default:
self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage)
}
} else {
self.showAlert(message: "compose_error".localized + "\n\n" + error.errorMessage)
}
Expand Down Expand Up @@ -1107,7 +1116,9 @@ extension ComposeViewController {
}

@objc private func messagePasswordTextFieldDidChange(_ sender: UITextField) {
messagePasswordAlertController?.actions[1].isEnabled = (sender.text ?? "").isNotEmpty
let password = sender.text ?? ""
let isPasswordStrong = composeMessageService.isMessagePasswordStrong(pwd: password)
messagePasswordAlertController?.actions[1].isEnabled = isPasswordStrong
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private struct ComposedErrorHandler: ErrorHandler {
static let shared: ComposedErrorHandler = ComposedErrorHandler(
handlers: [
KeyServiceErrorHandler(),
BackupServiceErrorHandler(),
BackupServiceErrorHandler()
]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable {
case emptyRecipient
case emptySubject
case emptyMessage
case missedPublicKey
case weakPassword
case subjectContainsPassword
case notUniquePassword
case missingPublicKey
case noPubRecipients
case revokedKeyRecipients
case expiredKeyRecipients
Expand All @@ -27,7 +30,13 @@ enum MessageValidationError: Error, CustomStringConvertible, Equatable {
return "compose_enter_subject".localized
case .emptyMessage:
return "compose_enter_secure".localized
case .missedPublicKey:
case .weakPassword:
return "compose_password_weak".localized
case .subjectContainsPassword:
return "compose_password_subject_error".localized
case .notUniquePassword:
return "compose_password_passphrase_error".localized
case .missingPublicKey:
return "compose_no_pub_sender".localized
case .noPubRecipients:
return "compose_recipient_no_pub".localized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ protocol CoreComposeMessageType {
final class ComposeMessageService {

private let messageGateway: MessageGateway
private let passPhraseService: PassPhraseServiceType
private let storage: EncryptedStorageType
private let contactsService: ContactsServiceType
private let core: CoreComposeMessageType & KeyParser
private let enterpriseServer: EnterpriseServerApiType
private let draftGateway: DraftGateway?
private lazy var logger: Logger = Logger.nested(Self.self)

private let sender: String

private struct ReplyInfo: Encodable {
let sender: String
let recipient: [String]
Expand All @@ -40,12 +43,15 @@ final class ComposeMessageService {
clientConfiguration: ClientConfiguration,
encryptedStorage: EncryptedStorageType,
messageGateway: MessageGateway,
passPhraseService: PassPhraseServiceType,
draftGateway: DraftGateway? = nil,
contactsService: ContactsServiceType? = nil,
sender: String,
core: CoreComposeMessageType & KeyParser = Core.shared,
enterpriseServer: EnterpriseServerApiType = EnterpriseServerApi()
) {
self.messageGateway = messageGateway
self.passPhraseService = passPhraseService
self.draftGateway = draftGateway
self.storage = encryptedStorage
self.contactsService = contactsService ?? ContactsService(
Expand All @@ -54,6 +60,7 @@ final class ComposeMessageService {
)
self.core = core
self.enterpriseServer = enterpriseServer
self.sender = sender
self.logger = Logger.nested(in: Self.self, with: "ComposeMessageService")
}

Expand All @@ -66,7 +73,6 @@ final class ComposeMessageService {
func validateAndProduceSendableMsg(
input: ComposeMessageInput,
contextToSend: ComposeMessageContext,
email: String,
includeAttachments: Bool = true,
signingPrv: PrvKeyInfo?
) async throws -> SendableMsg {
Expand Down Expand Up @@ -98,8 +104,8 @@ final class ComposeMessageService {

let subject = contextToSend.subject ?? "(no subject)"

guard let myPubKey = storage.getKeypairs(by: email).map(\.public).first else {
throw MessageValidationError.missedPublicKey
guard let myPubKey = storage.getKeypairs(by: sender).map(\.public).first else {
throw MessageValidationError.missingPublicKey
}

let sendableAttachments: [SendableMsg.Attachment] = includeAttachments
Expand All @@ -114,13 +120,24 @@ final class ComposeMessageService {
let replyToMimeMsg = input.replyToMime
.flatMap { String(data: $0, encoding: .utf8) }

if let password = contextToSend.messagePassword, password.isNotEmpty {
if subject.lowercased().contains(password.lowercased()) {
throw MessageValidationError.subjectContainsPassword
}

let allAvailablePassPhrases = passPhraseService.getPassPhrases().map(\.value)
if allAvailablePassPhrases.contains(password) {
throw MessageValidationError.notUniquePassword
}
}

return SendableMsg(
text: text,
html: nil,
to: recipients.map(\.email),
cc: [],
bcc: [],
from: email,
from: sender,
subject: subject,
replyToMimeMsg: replyToMimeMsg,
atts: sendableAttachments,
Expand Down Expand Up @@ -338,4 +355,27 @@ extension ComposeMessageService {

return SendableMsgBody(text: text, html: html)
}

func isMessagePasswordStrong(pwd: String) -> Bool {
let minLength = 8

// currently password-protected messages are supported only with FES on iOS
// guard enterpriseServer.isFesUsed else {
// // consumers - just 8 chars requirement
// return pwd.count >= minLength
// }

// enterprise FES - use common corporate password rules
let predicate = NSPredicate(
format: "SELF MATCHES %@ ", [
"(?=.*[a-z])", // 1 lowercase character
"(?=.*[A-Z])", // 1 uppercase character
"(?=.*[0-9])", // 1 number
"(?=.*[\\-@$#!%*?&_,;:'()\"])", // 1 special symbol
".{\(minLength),}$" // minimum 8 characters
].joined()
)

return predicate.evaluate(with: pwd)
}
}
5 changes: 4 additions & 1 deletion FlowCrypt/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,13 @@
"compose_recipient_revoked" = "One or more of your recipients have revoked public keys (marked in red).\n\nPlease ask them to send you a new public key. If this is an enterprise installation, please ask your systems admin.";
"compose_recipient_expired" = "One or more of your recipients have expired public keys (marked in orange).\n\nPlease ask them to send you updated public key. If this is an enterprise installation, please ask your systems admin.";
"compose_recipient_invalid_email" = "One or more of your recipients have invalid email address (marked in red)";
"compose_password_weak" = "Password didn't comply with company policy, which requires at least:\n\n- one uppercase\n- one lowercase\n- one number\n- one special character eg &/#\"-'_%-@,;:!*()\n- 8 characters length\n\nPlease update the password and re-send.";
"compose_password_passphrase_error" = "Please do not use your private key pass phrase as a password for this message.\n\nYou should come up with some other unique password that you can share with recipient.";
"compose_password_subject_error" = "Please do not include the password in the email subject. Sharing password over email undermines password based encryption.\n\nYou can ask the recipient to also install FlowCrypt, messages between FlowCrypt users don't need a password.";
"compose_password_placeholder" = "Tap to add password for recipients who don't have encryption set up.";
"compose_password_set_message" = "Web portal password added";
"compose_password_modal_title" = "Set web portal password";
"compose_password_modal_message" = "The recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)";
"compose_password_modal_message" = "The recipients will receive a link to read your message on a web portal, where they will need to enter this password.\n\nYou are responsible for sharing this password with recipients (use other medium to share the password - not email)\n\nPassword should include:\n- one uppercase\n- one lowercase\n- one number\n- one special character eg &/#\"-'_%-@,;:!*()\n- min 8 characters length";
"compose_error" = "Could not compose message";
"compose_reply_successful" = "Reply successfully sent";
"compose_quote_from" = "On %@ at %@ %@ wrote:"; // Date, time, sender
Expand Down
5 changes: 3 additions & 2 deletions FlowCryptAppTests/ExtensionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ extension ExtensionTests {
dateFormatter.dateFormat = "HH:mm"

let today = Date()
let year = Calendar.current.dateComponents([.year], from: today).year
let components = Calendar.current.dateComponents([.year, .day], from: today)

let sameYearDate = try XCTUnwrap(DateComponents(
calendar: .current,
timeZone: .current,
year: year,
year: components.year,
month: 1,
day: 24,
hour: 18,
Expand Down
Loading