Skip to content
Open
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
2 changes: 1 addition & 1 deletion .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ blocks:
fi
done
- open -Fn "$(xcode-select -p)/Applications/Simulator.app"
- xcrun simctl boot "iPhone 16"
- xcrun simctl boot "iPhone 17"
- xcrun simctl install booted ./WebDriverAgentRunner-Runner.app
epilogue:
always:
Expand Down
39 changes: 25 additions & 14 deletions Core/source/mobile-interface/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,30 @@ export class Endpoints {
} else if (req.format === 'encryptInline') {
const encryptedAtts: Att[] = [];
for (const att of req.atts || []) {
const encryptedAtt = (await PgpMsg.encrypt({
pubkeys: req.pubKeys,
data: Buf.fromBase64Str(att.base64),
filename: att.name,
armor: false,
})) as Uint8Array;
encryptedAtts.push(
new Att({
name: `${att.name}.pgp`,
type: 'application/pgp-encrypted',
data: encryptedAtt,
}),
);
// Skip encryption for public key attachments
if (att.type === 'application/pgp-keys') {
encryptedAtts.push(
new Att({
name: att.name,
type: att.type,
data: Buf.fromBase64Str(att.base64),
}),
);
} else {
const encryptedAtt = (await PgpMsg.encrypt({
pubkeys: req.pubKeys,
data: Buf.fromBase64Str(att.base64),
filename: att.name,
armor: false,
})) as Uint8Array;
encryptedAtts.push(
new Att({
name: `${att.name}.pgp`,
type: 'application/pgp-encrypted',
data: encryptedAtt,
}),
);
}
}

const signingPrv = await getSigningPrv(req);
Expand Down Expand Up @@ -199,7 +210,7 @@ export class Endpoints {
type: 'decryptErr',
content:
decryptRes.error.type === DecryptErrTypes.noMdc
? decryptRes.content?.toUtfStr() ?? ''
? (decryptRes.content?.toUtfStr() ?? '')
: rawBlock.content.toString(),
decryptErr: decryptRes,
complete: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ extension ComposeViewController {
handler: { [weak self] _ in self?.selectFromFilesApp() }
)
)
let publicKeyAction = UIAlertAction(
title: "files_picking_public_key".localized,
style: .default,
handler: { [weak self] _ in self?.attachPublicKey() }
)
publicKeyAction.accessibilityIdentifier = "aid-attach-public-key"
alert.addAction(publicKeyAction)
alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel))
present(alert, animated: true, completion: nil)
}
Expand Down Expand Up @@ -74,4 +81,45 @@ extension ComposeViewController {
}
)
}

private func attachPublicKey() {
Task {
do {
// Get the current user's keypair
let keypair = try await getUserKeypair()

// Get the public key data
let publicKeyArmored = keypair.public
guard let publicKeyData = publicKeyArmored.data(using: String.Encoding.utf8) else {
throw AppErr.general("Failed to convert public key to data")
}

// Create a MessageAttachment from the public key data with longid-based filename
let attachment = MessageAttachment(
name: "0x\(keypair.primaryLongid).asc",
data: publicKeyData,
mimeType: "application/pgp-keys"
)
appendAttachmentIfAllowed(attachment)
reload(sections: [.attachments])
} catch {
showAlert(message: "Failed to retrieve public key: \(error.localizedDescription)")
}
}
}

private func getUserKeypair() async throws -> Keypair {
// Get the user's email to retrieve their key
let userEmail = contextToSend.sender

// Get all keypairs for the user
let keypairs = try await appContext.keyAndPassPhraseStorage.getKeypairsWithPassPhrases(email: userEmail)

// Get the first available keypair
guard let firstKeypair = keypairs.first else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should take the newest valid public key, not the first one. as I have multiple keys in iOS app and currently it takes the oldest one which is expired.

throw AppErr.general("No keypair found for \(userEmail)")
}

return firstKeypair
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate, UINavigationCo
reload(sections: [.attachments])
}

private func appendAttachmentIfAllowed(_ attachment: MessageAttachment) {
internal func appendAttachmentIfAllowed(_ attachment: MessageAttachment) {
let totalSize = contextToSend.attachments.map(\.size).reduce(0, +) + attachment.size
if totalSize > GeneralConstants.Global.attachmentSizeLimit {
showToast("files_picking_size_error_message".localized)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ extension ComposeViewController {
self?.handleInfoTap()
}
let attachmentButton = NavigationBarItemsView.Input(
image: UIImage(systemName: "paperclip")
image: UIImage(systemName: "paperclip"),
accessibilityId: "aid-compose-attach"
) { [weak self] in
self?.handleAttachTap()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,17 +396,24 @@ extension ComposeMessageHelper {
for attachment in message.atts {
guard let data = Data(base64Encoded: attachment.base64) else { continue }

let encryptedFile = try await core.encrypt(
file: data,
name: attachment.name,
pubKeys: message.pubKeys
)
let encryptedAttachment = SendableMsg.Attachment(
name: "\(attachment.name).pgp",
type: "application/pgp-encrypted",
base64: encryptedFile.base64EncodedString()
)
encryptedAttachments.append(encryptedAttachment)
// Skip encryption for public key attachments
if attachment.type == "application/pgp-keys" {
// Add public key attachment as-is without encryption
encryptedAttachments.append(attachment)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that show public key switch is cropped on iOS 26, can you please fix it in this PR too, thanks!

Image

} else {
// Encrypt all other attachments
let encryptedFile = try await core.encrypt(
file: data,
name: attachment.name,
pubKeys: message.pubKeys
)
let encryptedAttachment = SendableMsg.Attachment(
name: "\(attachment.name).pgp",
type: "application/pgp-encrypted",
base64: encryptedFile.base64EncodedString()
)
encryptedAttachments.append(encryptedAttachment)
}
}

return encryptedAttachments
Expand Down
1 change: 1 addition & 0 deletions FlowCrypt/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ Be careful - avoid clicking links and downloading attachments, or sharing person
"files_picking_camera_input_source" = "Camera";
"files_picking_photo_library_source" = "Photo Library";
"files_picking_files_source" = "Files";
"files_picking_public_key" = "Public Key";
"files_picking_no_library_access_error_title" = "No access to library";
"files_picking_no_library_access_error_message" = "You may open Settings and give the full access to photo library";
"files_picking_no_camera_access_error_title" = "No access to camera";
Expand Down
2 changes: 1 addition & 1 deletion FlowCrypt/Resources/generated/flowcrypt-ios-prod.js.txt

Large diffs are not rendered by default.

28 changes: 24 additions & 4 deletions appium/api-mocks/apis/fes/fes-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,32 @@ export const getMockFesEndpoints = (mockConfig: MockConfig, fesConfig: FesConfig
// body is a mime-multipart string, we're doing a few smoke checks here without parsing it
if (req.method === 'POST') {
expectContains(body, '-----BEGIN PGP MESSAGE-----');
const match = String(body).match(/Content-Type: application\/json\s*\n\s*(\{.*\})/);

if (!match) {
throw new FesHttpErr('Bad request', Status.BAD_REQUEST);
// Extract JSON from request body
const bodyStr = String(body);
let messageData;

// Try different parsing strategies

// 1. Try embedded JSON after Content-Type header (original format)
const embeddedJsonMatch = bodyStr.match(/Content-Type:\s*application\/json\s*\r?\n\s*(\{.*?\})/s);

// 2. Try to find any JSON object containing associateReplyToken
const jsonObjectMatch = bodyStr.match(/\{[^{}]*"associateReplyToken"[^{}]*\}/);

if (embeddedJsonMatch) {
messageData = JSON.parse(embeddedJsonMatch[1]);
} else if (jsonObjectMatch) {
messageData = JSON.parse(jsonObjectMatch[0]);
} else {
// Last resort: try to parse the entire body as JSON
try {
messageData = JSON.parse(bodyStr);
} catch {
throw new FesHttpErr('Bad request - could not extract JSON from request body', Status.BAD_REQUEST);
}
}
const messageData = JSON.parse(match[0]);

const { associateReplyToken, to, cc, bcc } = messageData;

expect(associateReplyToken).toBe('mock-fes-reply-token');
Expand Down
18 changes: 18 additions & 0 deletions appium/tests/screenobjects/new-message.screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const SELECTORS = {
CANCEL_BUTTON: '~aid-cancel-button',
BACK_BUTTON: '~aid-back-button',
DELETE_BUTTON: '~aid-compose-delete',
ATTACH_BUTTON: '~aid-compose-attach',
ATTACH_PUBLIC_KEY_BUTTON: '~aid-attach-public-key',
SEND_BUTTON: '~aid-compose-send',
SEND_PLAIN_MESSAGE_BUTTON: '~aid-compose-send-plain',
SEND_MESSAGE_PASSWORD_BUTTON: '~aid-compose-send-message-password',
Expand Down Expand Up @@ -92,6 +94,14 @@ class NewMessageScreen extends BaseScreen {
return $(SELECTORS.DELETE_BUTTON);
}

get attachButton() {
return $(SELECTORS.ATTACH_BUTTON);
}

get attachPublicKeyButton() {
return $(SELECTORS.ATTACH_PUBLIC_KEY_BUTTON);
}

get sendButton() {
return $(SELECTORS.SEND_BUTTON);
}
Expand Down Expand Up @@ -413,6 +423,14 @@ class NewMessageScreen extends BaseScreen {
await ElementHelper.waitAndClick(await this.sendButton);
};

clickAttachButton = async () => {
await ElementHelper.waitAndClick(await this.attachButton);
};

clickAttachPublicKeyButton = async () => {
await ElementHelper.waitAndClick(await this.attachPublicKeyButton);
};

checkSendPlainMessageButtonNotPresent = async () => {
await ElementHelper.waitElementInvisible(await this.sendPlainMessageButton);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { MockApi } from 'api-mocks/mock';
import { MockApiConfig } from 'api-mocks/mock-config';
import { MockUserList } from 'api-mocks/mock-data';
import {
SplashScreen,
MailFolderScreen,
NewMessageScreen,
SetupKeyScreen,
MenuBarScreen,
EmailScreen,
} from '../../../screenobjects/all-screens';

describe('COMPOSE EMAIL: ', () => {
it('check public key attachment attach', async () => {
const mockApi = new MockApi();
const recipient = MockUserList.dmitry;
const testSubject1 = 'Test public key attachment - PGP';
const testSubject2 = 'Test public key attachment - Password';

mockApi.fesConfig = MockApiConfig.defaultEnterpriseFesConfiguration;
mockApi.ekmConfig = MockApiConfig.defaultEnterpriseEkmConfiguration;
const email = '[email protected]';
mockApi.addGoogleAccount(email);

// Set up attester to serve public key for PGP recipient
mockApi.attesterConfig = {
servedPubkeys: {
[recipient.email]: recipient.pub!,
},
};

await mockApi.withMockedApis(async () => {
await SplashScreen.mockLogin();
await SetupKeyScreen.setPassPhrase();
await MailFolderScreen.checkInboxScreen();

// Test 1: Public key attachment with regular PGP message
await MailFolderScreen.clickCreateEmail();

// Compose email
await NewMessageScreen.setAddRecipient(recipient.email);
await NewMessageScreen.setSubject(testSubject1);
await NewMessageScreen.setComposeSecurityMessage('This message includes my public key');

// Click attach button
await NewMessageScreen.clickAttachButton();

// Wait for action sheet to appear and click "Public key" using accessibility identifier
await browser.pause(1000);
await NewMessageScreen.clickAttachPublicKeyButton();

// Check that public key attachment was added
// The filename should be in format 0x{longid}.asc
await browser.pause(1000);
const attachmentLabel = await NewMessageScreen.attachmentNameLabel;
const attachmentName = await attachmentLabel.getValue();
expect(attachmentName).toMatch(/^0x[A-F0-9]{16}\.asc$/);

// Send the message
await NewMessageScreen.clickSendButton();
await browser.pause(1000);

// Go to sent folder to verify the attachment
await MenuBarScreen.clickMenuBtn();
await MenuBarScreen.clickSentButton();
await MailFolderScreen.checkSentScreen();
await MailFolderScreen.clickOnEmailBySubject(testSubject1);

// Verify attachment in sent email
// TODO: need to uncomment this line when we fix public key render issue
// https://github.com/FlowCrypt/flowcrypt-ios/issues/634
// await EmailScreen.checkPublicKeyImportView(email, recipient.pub!, false);
await EmailScreen.clickBackButton();

// Go back to inbox for test 2
await MenuBarScreen.clickMenuBtn();
await MenuBarScreen.clickInboxButton();
await MailFolderScreen.checkInboxScreen();

// Test 2: Public key attachment with password-protected message (non-PGP recipient)
await MailFolderScreen.clickCreateEmail();

// Compose email to non-PGP recipient
await NewMessageScreen.setAddRecipient('[email protected]');
await NewMessageScreen.setSubject(testSubject2);
await NewMessageScreen.setComposeSecurityMessage('This password-protected message includes my public key');

// Click attach button
await NewMessageScreen.clickAttachButton();

// Wait for action sheet to appear and click "Public key" using accessibility identifier
await browser.pause(1000);
await NewMessageScreen.clickAttachPublicKeyButton();

// Check that public key attachment was added
await browser.pause(1000);
const attachmentLabel2 = await NewMessageScreen.attachmentNameLabel;
const attachmentName2 = await attachmentLabel2.getValue();
expect(attachmentName2).toMatch(/^0x[A-F0-9]{16}\.asc$/);

// Set message password
await NewMessageScreen.clickPasswordCell();
await NewMessageScreen.setMessagePassword('abcABC1*');

// Send the password-protected message
await NewMessageScreen.clickSendButton();
await browser.pause(1000);

// Go to sent folder to verify the attachment
await MenuBarScreen.clickMenuBtn();
await MenuBarScreen.clickSentButton();
await MailFolderScreen.checkSentScreen();
await MailFolderScreen.clickOnEmailBySubject(testSubject2);

// Verify attachment in sent password-protected email
// TODO: need to uncomment this line when we fix public key render issue
// https://github.com/FlowCrypt/flowcrypt-ios/issues/634
// await EmailScreen.checkPublicKeyImportView(email, recipient.pub!, false);
});
});
});