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

metadata encryption v2 (confirm-less labeling) #9130

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
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
40 changes: 33 additions & 7 deletions packages/e2e-utils/src/mocks/dropbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const port = 30002;
export class DropboxMock {
files: Record<string, any> = {};
nextResponse: Record<string, any>[] = [];
uploadSessionFiles: Record<string, Buffer> = {};
// store requests for assertions in tests
requests: string[] = [];
app?: Express;
Expand Down Expand Up @@ -119,8 +120,8 @@ export class DropboxMock {
metadata: {
'.tag': 'file',
name: query,
path_lower: `/apps/trezor/${query}`,
path_display: `/Apps/TREZOR/${query}`,
path_lower: `/${query}`,
path_display: `/${query}`,
id: 'id:foo-id',
client_modified: '2020-10-07T09:52:45Z',
server_modified: '2020-10-07T09:52:45Z',
Expand All @@ -141,25 +142,39 @@ export class DropboxMock {
res.end();
});

// https://api.dropboxapi.com/2/files/list_folder
app.post('/2/files/list_folder', (_req, res) => {
const entries = Object.keys(this.files).map(name =>
// name is without leading slash /
({ name: name.replace('/', '') }),
);

res.write(
JSON.stringify({
entries,
}),
);

return res.send();
});

// https://content.dropboxapi.com/2/files/download
app.post('/2/files/download', (req, res) => {
// @ts-expect-error
const dropboxApiArgs = JSON.parse(req.headers['dropbox-api-arg']);
const { path } = dropboxApiArgs;
const name = path.replace('/apps/trezor', '');

const file = this.files[name];

const file = this.files[path];
if (file) {
// @ts-expect-error
res.writeHeader(200, {
'Content-Type': 'application/octet-stream',
'Dropbox-Api-Result': `{"name": "${name}", "path_lower": "${path}", "path_display": "/Apps/TREZOR/${name}", "id": "id:foo-bar", "client_modified": "2020-10-07T09:52:45Z", "server_modified": "2020-10-07T09:52:45Z", "rev": "foo-bar", "size": 666, "is_downloadable": true, "content_hash": "foo-bar"}`,
'Dropbox-Api-Result': `{"name": "${path}", "path_lower": "${path}", "path_display": "/${path}", "id": "id:foo-bar", "client_modified": "2020-10-07T09:52:45Z", "server_modified": "2020-10-07T09:52:45Z", "rev": "foo-bar", "size": 666, "is_downloadable": true, "content_hash": "foo-bar"}`,
});

res.write(file, 'binary');
} else {
console.error('[dropboxMock]: no such file found', file);
console.error('[dropboxMock]: no such file found', path);
}

return res.end();
Expand All @@ -179,6 +194,17 @@ export class DropboxMock {
},
);

// https://content.dropboxapi.com/2/files/upload
app.post(
'/2/files/move_v2',
// express.raw({ type: 'application/octet-stream' }),
(req, res) => {
this.files[req.body.to_path] = this.files[req.body.from_path];
delete this.files[req.body.from_path];
res.send();
},
);

this.app = app;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/e2e-utils/src/mocks/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ export class GoogleMock {
});
});

app.get('/drive/api/v3/reference/files/list', express.json(), (_req, res) => {
res.json({
files: Object.keys(this.files),
});
});

app.get('/drive/v3/about', express.json(), (_req, res) => {
console.log('[mockGoogleDrive]: about');
res.send({
Expand Down
11 changes: 6 additions & 5 deletions packages/suite-desktop-core/e2e/support/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export const launchSuite = async (params: LaunchSuiteParams = {}) => {
// recordVideo: { dir: 'test-results' },
});

const localDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'));

if (options.rmUserData) {
fse.removeSync(localDataDir);
}

electronApp.process().stdout?.on('data', data => console.log(data.toString()));
electronApp.process().stderr?.on('data', data => console.error(data.toString()));

Expand All @@ -48,11 +54,6 @@ export const launchSuite = async (params: LaunchSuiteParams = {}) => {
);

const window = await electronApp.firstWindow();
const localDataDir = await electronApp.evaluate(({ app }) => app.getPath('userData'));

if (options.rmUserData) {
fse.removeSync(localDataDir);
}

return { electronApp, window, localDataDir };
};
Expand Down
14 changes: 13 additions & 1 deletion packages/suite-web/e2e/cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export default defineConfig({
addMatchImageSnapshotPlugin(on, config);
});
on('task', {
metadataStartProvider: async provider => {
metadataStartProvider: async (provider: 'dropbox' | 'google') => {
switch (provider) {
case 'dropbox':
await mocked.dropbox.start();
Expand Down Expand Up @@ -148,6 +148,18 @@ export default defineConfig({
throw new Error('not a valid case');
}
},
metadataGetFilesList: ({ provider }) => {
switch (provider) {
case 'dropbox':
return Object.keys(mocked.dropbox.files).map(name =>
name.replace('/apps/trezor/', ''),
);
case 'google':
return Object.keys(mocked.google.files);
default:
throw new Error('not a valid case');
}
},
startMockedBridge: async har => {
await mocked.bridge.start(har);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @group_metadata
// @retry=2
// @retry=0

import { rerouteMetadataToMockProvider, stubOpen } from '../../stubs/metadata';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @group_metadata
// @retry=2
// @retry=0

import { rerouteMetadataToMockProvider, stubOpen } from '../../stubs/metadata';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @group_metadata
// @retry=2
// @retry=0

import { rerouteMetadataToMockProvider, stubOpen } from '../../stubs/metadata';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @group_metadata
// @retry=2
// @retry=0

import { rerouteMetadataToMockProvider, stubOpen } from '../../stubs/metadata';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @group_metadata
// @retry=2
// @retry=0

import * as METADATA_LABELING from '@trezor/suite/src/actions/suite/constants/metadataLabelingConstants';

Expand Down
17 changes: 8 additions & 9 deletions packages/suite-web/e2e/tests/metadata/metadata-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @group_metadata
// @retry=2
// @retry=0

const mnemonic = 'all all all all all all all all all all all all';

Expand Down Expand Up @@ -35,17 +35,19 @@ describe('Metadata - cancel metadata on device', () => {
cy.hoverTestElement("@metadata/accountLabel/m/84'/0'/0'/hover-container");

// try to init metadata...
cy.getTestElement("@metadata/accountLabel/m/84'/0'/0'/add-label-button").click();
cy.getTestElement("@metadata/accountLabel/m/84'/0'/0'/add-label-button")
// todo: there is some bug on how this component is rendered, cypress sometimes detects 2 instances of this component!!!
.first()
.click();
// ...but user cancels dialogue on device
cy.getConfirmActionOnDeviceModal();
cy.task('pressNo');
cy.wait(501);

// cancelling labeling on device actually enables labeling globally so when user reloads app,
// metadata dialogue will be prompted. now user cancels dialogue on device again and remembers device

cy.safeReload();
// todo: this may timeout
cy.discoveryShouldFinish();

cy.getConfirmActionOnDeviceModal();
cy.task('pressNo');
Expand All @@ -55,7 +57,7 @@ describe('Metadata - cancel metadata on device', () => {
cy.getTestElement('@viewOnlyStatus/disabled').click();
cy.getTestElement('@viewOnly/radios/enabled').click();
cy.safeReload();
cy.discoveryShouldFinish(); // no dialogue, metadata keys survive together with remembered wallet!
// no dialogue, metadata keys survive together with remembered wallet!

// but when user tries to add another wallet, there is enable labeling dialogue again
cy.getTestElement('@menu/switch-device').click();
Expand Down Expand Up @@ -85,10 +87,6 @@ describe('Metadata - cancel metadata on device', () => {
// note: since recently, the first dialogue that appeared was "enable labeling on device" are we ok with this change of order?
cy.getTestElement('@modal/close-button').click();

// cy.getConfirmActionOnDeviceModal();
// cy.task('pressNo');
// cy.wait(501);

cy.getTestElement('@accounts/empty-account/receive');

// forget device and reload -> enable labeling dialogue appears
Expand All @@ -102,6 +100,7 @@ describe('Metadata - cancel metadata on device', () => {
cy.getTestElement('@switch-device/eject').click();

cy.safeReload();
cy.discoveryShouldFinish();

cy.getConfirmActionOnDeviceModal(); // enable labeling dialogue;
cy.task('pressNo');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// @group_metadata

import { rerouteMetadataToMockProvider, stubOpen } from '../../stubs/metadata';

const originalFileName = '/f7acc942eeb83921892a95085e409b3e6b5325db6400ae5d8de523a305291dca.mtdt';
const mnemonic = 'all all all all all all all all all all all all';

const provider = 'dropbox' as const;

describe('Metadata - metadata files are properly migrated from ENCRYPTION_VERSION v1 to v2', () => {
beforeEach(() => {
cy.viewport(1080, 1440).resetDb();
});

it('user cancels metadata on device during migration, this choice is respected for remembered wallet. migration is finished later', () => {
// prepare test
cy.task('startEmu', { wipe: true, version: '2.7.0' });
cy.task('setupEmu', { mnemonic });
cy.task('startBridge');
cy.task('metadataStartProvider', provider);
cy.task('metadataSetFileContent', {
provider,
file: originalFileName,
content: {
version: '1.0.0',
accountLabel: 'some account label',
outputLabels: {},
addressLabels: {},
},
aesKey: 'c785ef250807166bffc141960c525df97647fcc1bca57f6892ca3742ba86ed8d',
});

// first go to settings, see that metadata is disabled by default.
cy.prefixedVisit('/settings', {
onBeforeLoad: (win: Window) => {
cy.stub(win, 'open').callsFake(stubOpen(win));
cy.stub(win, 'fetch').callsFake(rerouteMetadataToMockProvider);
},
});
cy.getTestElement('@analytics/continue-button', { timeout: 30_000 }).click();
cy.getTestElement('@onboarding/exit-app-button').click();

// enable encryption v2 through experimental features
cy.enableDebugMode();
cy.getTestElement('@settings/experimental-switch').click({ force: true });
cy.getTestElement('@experimental-feature/confirm-less-labeling/checkbox').click();

// metadata is off
cy.getTestElement('@settings/metadata-switch').within(() => {
cy.get('input').should('not.be.checked');
});

// now go to accounts. application does not try to initiate metadata
cy.getTestElement('@suite/menu/suite-index').click();
cy.getTestElement('@onbarding/viewOnly/skip').click();
cy.getTestElement('@viewOnlyTooltip/gotIt').click();
cy.getTestElement('@account-menu/btc/normal/0').click();
cy.discoveryShouldFinish();

// but even though metadata is disabled, on hover "add label" button appears
cy.hoverTestElement("@metadata/accountLabel/m/84'/0'/0'/hover-container");
cy.getTestElement("@metadata/accountLabel/m/84'/0'/0'/add-label-button").click();

cy.getTestElement(`@modal/metadata-provider/${provider}-button`).click();
cy.getTestElement('@modal/metadata-provider').should('not.exist');

// now user cancels dialogue on device
cy.getConfirmActionOnDeviceModal();
cy.task('pressNo');
cy.wait(501);

// cancelling labeling on device actually enables labeling globally so when user reloads app,
// metadata dialogue will be prompted. this is because metadata settings is remembered, but anything related
// to device which was not remembered (in this case previously cancelled dialogue) is not remembered.
// now user cancels dialogue on device again and remembers device
cy.prefixedVisit('/accounts', {
onBeforeLoad: (win: Window) => {
cy.stub(win, 'open').callsFake(stubOpen(win));
cy.stub(win, 'fetch').callsFake(rerouteMetadataToMockProvider);
},
});
cy.discoveryShouldFinish();

cy.getConfirmActionOnDeviceModal(); // <-- enable labeling dialogue
cy.task('pressNo');

// set device to remembered
cy.getTestElement('@menu/switch-device').click();
cy.getTestElement('@viewOnlyStatus/disabled').click();
cy.getTestElement('@viewOnly/radios/enabled').click();
cy.wait(200); // wait for db write to finish :( sad

// after reload, no metadata dialogue (cancel choice from previous run is now remembered)
cy.prefixedVisit('/accounts', {
onBeforeLoad: (win: Window) => {
cy.stub(win, 'open').callsFake(stubOpen(win));
cy.stub(win, 'fetch').callsFake(rerouteMetadataToMockProvider);
},
});
cy.getTestElement('@deviceStatus-connected');

// now user manually triggers enable metadata. migration should run and finish successfully
cy.hoverTestElement("@metadata/accountLabel/m/84'/0'/0'/hover-container");
cy.getTestElement("@metadata/accountLabel/m/84'/0'/0'/add-label-button").click();
cy.getConfirmActionOnDeviceModal();
cy.task('pressYes');

// now v1 encryption file is migrated, and its value displays in metadata input, yupi
cy.getTestElement('@metadata/input').should('have.value', 'some account label');
});
});
Loading
Loading