Skip to content

Commit 677656b

Browse files
committed
Add tool to clean existing keys for a certain upload year in the database with the new purify method. Usage example: npm run clean 2023.
1 parent eefd6cd commit 677656b

3 files changed

Lines changed: 106 additions & 3 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
"test:integration": "mocha --exit --require ./test/setup.js --recursive ./test/integration",
2121
"release": "npm run release:install && npm run release:archive",
2222
"release:install": "rm -rf node_modules/ && npm ci --production",
23-
"release:archive": "zip -rq release.zip package.json package-lock.json node_modules/ *.js src/ config/ locales/"
23+
"release:archive": "zip -rq release.zip package.json package-lock.json node_modules/ *.js src/ config/ locales/",
24+
"clean": "node src/tools/clean"
2425
},
2526
"dependencies": {
2627
"@hapi/boom": "^10.0.1",

src/modules/mongo.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
'use strict';
77

88
const log = require('../lib/log');
9-
const MongoClient = require('mongodb').MongoClient;
9+
const {MongoClient} = require('mongodb');
1010

1111
/**
1212
* A simple wrapper around the official MongoDB client.
@@ -22,8 +22,9 @@ class Mongo {
2222
async init({uri, user, pass}) {
2323
log.info('Connecting to MongoDB ...');
2424
const url = `mongodb://${user}:${pass}@${uri}`;
25-
this._client = await MongoClient.connect(url);
25+
this._client = new MongoClient(url);
2626
this._client.on('commandFailed', event => log.error('MongoDB command failed\n%s', event));
27+
await this._client.connect();
2728
this._db = this._client.db();
2829
}
2930

@@ -134,6 +135,29 @@ class Mongo {
134135
const col = this._db.collection(type);
135136
return col.deleteMany({});
136137
}
138+
139+
/**
140+
* Aggregate documents from a collection
141+
* @param {Array} pipeline The aggregation pipeline
142+
* @param {String} type The collection to use e.g. 'publickey'
143+
* @return {Promise<AggregationCursor<T>>} The operation result
144+
*/
145+
aggregate(pipeline, type) {
146+
const col = this._db.collection(type);
147+
return col.aggregate(pipeline);
148+
}
149+
150+
/**
151+
* Replace one document
152+
* @param {Object} filter The filter used to select the document to replace
153+
* @param {Object} replacement The Document that replaces the matching document
154+
* @param {String} type The collection to use e.g. 'publickey'
155+
* @return {Promise<Document>}
156+
*/
157+
replace(filter, replacement, type) {
158+
const col = this._db.collection(type);
159+
return col.replaceOne(filter, replacement);
160+
}
137161
}
138162

139163
module.exports = Mongo;

src/tools/clean.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Copyright (C) 2024 Mailvelope GmbH
3+
* Licensed under the GNU Affero General Public License version 3
4+
*/
5+
6+
'use strict';
7+
8+
const config = require('../../config/config');
9+
const Mongo = require('../modules/mongo');
10+
const PurifyKey = require('../modules/purify-key');
11+
const PGP = require('../modules/pgp');
12+
13+
const DB_TYPE = 'publickey';
14+
const KEY_SIZE = 1; // divided by 4/3 gives binary size of key
15+
const MAX_UPLOAD_DATE = new Date(new Date().setDate(new Date().getDate() - config.publicKey.purgeTimeInDays)); // now - purgeTimeInDays
16+
const YEAR = parseInt(process.argv[2] ?? MAX_UPLOAD_DATE.getFullYear());
17+
18+
let mongo;
19+
let pgp;
20+
21+
async function init() {
22+
mongo = new Mongo();
23+
await mongo.init(config.mongo);
24+
const purify = new PurifyKey({...config.purify, maxNumUserEmail: 60, maxNumSubkey: 25, maxSizeKey: 64 * 1024});
25+
pgp = new PGP(purify);
26+
}
27+
28+
function aggregate() {
29+
return mongo.aggregate([
30+
{$match: {uploaded: {$gte: new Date(YEAR, 0, 1), $lt: new Date(YEAR + 1, 0, 1)}}},
31+
{$match: {uploaded: {$lt: MAX_UPLOAD_DATE}}},
32+
{$project: {keySize: {$binarySize: '$publicKeyArmored'}}},
33+
{$match: {keySize: {$gt: KEY_SIZE}}}
34+
], DB_TYPE);
35+
}
36+
37+
async function clean() {
38+
try {
39+
console.log(`Start cleaning year ${YEAR}...`);
40+
await init();
41+
const result = await aggregate();
42+
let count = 0;
43+
for await (const document of result) {
44+
await cleanKey(document);
45+
count++;
46+
}
47+
console.log('Number of keys processed:', count);
48+
} catch (e) {
49+
console.log('Error while traversing keys:', e);
50+
} finally {
51+
await mongo.disconnect();
52+
}
53+
}
54+
55+
async function cleanKey({_id}) {
56+
const key = await mongo.get({_id}, DB_TYPE);
57+
if (!key.publicKeyArmored) {
58+
console.log('No armored key. Key is not yet verified. Skip');
59+
return;
60+
}
61+
try {
62+
const purified = await pgp.parseKey(key.publicKeyArmored);
63+
// filter out all unverified user ID and those that are not in the purified set
64+
key.userIds = key.userIds.filter(userId => userId.verified && purified.userIds.some(id => id.email === userId.email));
65+
if (!key.userIds.length) {
66+
throw new Error('No user ID after comparing with purified key.');
67+
}
68+
const publicKeyArmored = await pgp.filterKeyByUserIds(key.userIds, purified.publicKeyArmored);
69+
key.publicKeyArmored = publicKeyArmored;
70+
await mongo.replace({_id}, key, DB_TYPE);
71+
} catch (e) {
72+
console.log('Parsing of key failed:', e.message);
73+
await mongo.remove({_id}, DB_TYPE);
74+
console.log(`Key ${key.fingerprint} removed.`);
75+
}
76+
}
77+
78+
clean();

0 commit comments

Comments
 (0)