Skip to content

Commit 9578306

Browse files
Merge pull request #172 from iExecBlockchainComputing/feature/pre-send-email-verification
Feature/pre send email verification
2 parents 9499bd5 + aebcb91 commit 9578306

File tree

9 files changed

+459
-261
lines changed

9 files changed

+459
-261
lines changed

dapp/CHANGELOG.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## Next
6+
7+
### Changed
8+
9+
- Check email address syntax before trying to send the email.
10+
511
## [0.7.0]
612

713
### Changed
814

9-
- use `@iexec/dataprotector-deserializer` for handling dataprotector v2 protected data
15+
- use `@iexec/dataprotector-deserializer` for handling dataprotector v2 protected data.
1016

1117
## [0.6.0]
1218

@@ -25,19 +31,19 @@ All notable changes to this project will be documented in this file.
2531
### Changed
2632

2733
- Allow developer to specify the sender Name associated to the sender:
28-
- **senderName** _(optional)_: the email sender name, it must be between 3 and 20 characters long. It will be displayed as `"<senderName> via Web3mail"` in the `"From"` email header.
34+
- **senderName** _(optional)_: the email sender name, it must be between 3 and 20 characters long. It will be displayed as `"<senderName> via Web3mail"` in the `"From"` email header.
2935

3036
## [0.3.0]
3137

3238
### Changed
3339

34-
- Migrate from sconify 5.7.5-v9 to sconify 5.7.5-v12
40+
- Migrate from sconify 5.7.5-v9 to sconify 5.7.5-v12.
3541

3642
## [0.2.0]
3743

3844
### Changed
3945

40-
- add support for `options` in `IEXEC_REQUESTER_SECRET_3` (optional)
41-
- add support for html content via `"contentType": "text/html"` in `options`
46+
- add support for `options` in `IEXEC_REQUESTER_SECRET_3` (optional).
47+
- add support for html content via `"contentType": "text/html"` in `options`.
4248

4349
## [0.1.0] Initial release

dapp/src/sendEmail.js

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ const {
33
IExecDataProtectorDeserializer,
44
} = require('@iexec/dataprotector-deserializer');
55
const sendEmail = require('./emailService');
6-
const validateInputs = require('./validateInputs');
6+
const {
7+
validateWorkerEnv,
8+
validateAppSecret,
9+
validateRequesterSecret,
10+
validateProtectedData,
11+
} = require('./validation');
712
const {
813
downloadEncryptedContent,
914
decryptContent,
@@ -20,64 +25,74 @@ async function writeTaskOutput(path, message) {
2025
}
2126

2227
async function start() {
23-
// Parse the developer secret environment variable
24-
let developerSecret;
28+
const { IEXEC_OUT, IEXEC_APP_DEVELOPER_SECRET, IEXEC_REQUESTER_SECRET_1 } =
29+
process.env;
30+
31+
// Check worker env
32+
const workerEnv = validateWorkerEnv({ IEXEC_OUT });
33+
34+
// Parse the app developer secret environment variable
35+
let appDeveloperSecret;
2536
try {
26-
developerSecret = JSON.parse(process.env.IEXEC_APP_DEVELOPER_SECRET);
37+
appDeveloperSecret = JSON.parse(IEXEC_APP_DEVELOPER_SECRET);
2738
} catch {
2839
throw Error('Failed to parse the developer secret');
2940
}
41+
appDeveloperSecret = validateAppSecret(appDeveloperSecret);
42+
43+
// Parse the requester secret environment variable
3044
let requesterSecret;
3145
try {
32-
requesterSecret = process.env.IEXEC_REQUESTER_SECRET_1
33-
? JSON.parse(process.env.IEXEC_REQUESTER_SECRET_1)
46+
requesterSecret = IEXEC_REQUESTER_SECRET_1
47+
? JSON.parse(IEXEC_REQUESTER_SECRET_1)
3448
: {};
3549
} catch {
3650
throw Error('Failed to parse requester secret');
3751
}
52+
requesterSecret = validateRequesterSecret(requesterSecret);
3853

39-
const deserializer = new IExecDataProtectorDeserializer();
40-
const email = await deserializer.getValue('email', 'string');
54+
// Get the secret email address from the protected data
55+
let protectedData;
56+
try {
57+
const deserializer = new IExecDataProtectorDeserializer();
58+
protectedData = {
59+
email: await deserializer.getValue('email', 'string'),
60+
};
61+
} catch (e) {
62+
throw Error(`Failed to parse ProtectedData: ${e.message}`);
63+
}
64+
validateProtectedData(protectedData);
4165

42-
const unsafeEnvVars = {
43-
iexecOut: process.env.IEXEC_OUT,
44-
mailJetApiKeyPublic: developerSecret.MJ_APIKEY_PUBLIC,
45-
mailJetApiKeyPrivate: developerSecret.MJ_APIKEY_PRIVATE,
46-
mailJetSender: developerSecret.MJ_SENDER,
47-
emailSubject: requesterSecret.emailSubject,
48-
emailContentMultiAddr: requesterSecret.emailContentMultiAddr,
49-
contentType: requesterSecret.contentType,
50-
senderName: requesterSecret.senderName,
51-
emailContentEncryptionKey: requesterSecret.emailContentEncryptionKey,
52-
};
53-
const envVars = validateInputs(unsafeEnvVars);
5466
const encryptedEmailContent = await downloadEncryptedContent(
55-
envVars.emailContentMultiAddr
67+
requesterSecret.emailContentMultiAddr
5668
);
57-
const emailContent = decryptContent(
69+
const requesterEmailContent = decryptContent(
5870
encryptedEmailContent,
59-
envVars.emailContentEncryptionKey
71+
requesterSecret.emailContentEncryptionKey
6072
);
6173

6274
const response = await sendEmail({
63-
email,
64-
mailJetApiKeyPublic: envVars.mailJetApiKeyPublic,
65-
mailJetApiKeyPrivate: envVars.mailJetApiKeyPrivate,
66-
emailSubject: envVars.emailSubject,
67-
emailContent,
68-
mailJetSender: envVars.mailJetSender,
69-
contentType: envVars.contentType,
70-
senderName: envVars.senderName,
75+
// from protected data
76+
email: protectedData.email,
77+
// from app developer secrets
78+
mailJetApiKeyPublic: appDeveloperSecret.MJ_APIKEY_PUBLIC,
79+
mailJetApiKeyPrivate: appDeveloperSecret.MJ_APIKEY_PRIVATE,
80+
mailJetSender: appDeveloperSecret.MJ_SENDER,
81+
// from requester secret
82+
emailContent: requesterEmailContent,
83+
emailSubject: requesterSecret.emailSubject,
84+
contentType: requesterSecret.contentType,
85+
senderName: requesterSecret.senderName,
7186
});
7287

7388
await writeTaskOutput(
74-
`${envVars.iexecOut}/result.txt`,
89+
`${workerEnv.IEXEC_OUT}/result.txt`,
7590
JSON.stringify(response, null, 2)
7691
);
7792
await writeTaskOutput(
78-
`${envVars.iexecOut}/computed.json`,
93+
`${workerEnv.IEXEC_OUT}/computed.json`,
7994
JSON.stringify({
80-
'deterministic-output-path': `${envVars.iexecOut}/result.txt`,
95+
'deterministic-output-path': `${workerEnv.IEXEC_OUT}/result.txt`,
8196
})
8297
);
8398
}

dapp/src/validateInputs.js

Lines changed: 0 additions & 27 deletions
This file was deleted.

dapp/src/validation.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const Joi = require('joi');
2+
3+
const workerEnvSchema = Joi.object({
4+
IEXEC_OUT: Joi.string().required(),
5+
});
6+
7+
function validateWorkerEnv(envVars) {
8+
const { error, value } = workerEnvSchema.validate(envVars, {
9+
abortEarly: false,
10+
});
11+
if (error) {
12+
const validationErrors = error.details.map((detail) => detail.message);
13+
throw new Error(`Worker environment error: ${validationErrors.join('; ')}`);
14+
}
15+
return value;
16+
}
17+
18+
const appSecretSchema = Joi.object({
19+
MJ_APIKEY_PUBLIC: Joi.string().required(),
20+
MJ_APIKEY_PRIVATE: Joi.string().required(),
21+
MJ_SENDER: Joi.string().email().required(),
22+
});
23+
24+
function validateAppSecret(obj) {
25+
const { error, value } = appSecretSchema.validate(obj, {
26+
abortEarly: false,
27+
});
28+
if (error) {
29+
const validationErrors = error.details.map((detail) => detail.message);
30+
throw new Error(`App secret error: ${validationErrors.join('; ')}`);
31+
}
32+
return value;
33+
}
34+
35+
const protectedDataEmailSchema = Joi.object({
36+
email: Joi.string().email().required(),
37+
});
38+
39+
function validateProtectedData(obj) {
40+
const { error, value } = protectedDataEmailSchema.validate(obj, {
41+
abortEarly: false,
42+
});
43+
if (error) {
44+
const validationErrors = error.details.map((detail) => detail.message);
45+
throw new Error(`ProtectedData error: ${validationErrors.join('; ')}`);
46+
}
47+
return value;
48+
}
49+
50+
const requesterSecretSchema = Joi.object({
51+
emailSubject: Joi.string().required(),
52+
emailContentMultiAddr: Joi.string()
53+
.pattern(/^\/(ipfs|p2p)\//)
54+
.message('"emailContentMultiAddr" must be a multiAddr')
55+
.required(),
56+
emailContentEncryptionKey: Joi.string().base64(),
57+
contentType: Joi.string().valid('text/plain', 'text/html'),
58+
senderName: Joi.string().min(3).max(20),
59+
});
60+
61+
function validateRequesterSecret(obj) {
62+
const { error, value } = requesterSecretSchema.validate(obj, {
63+
abortEarly: false,
64+
});
65+
if (error) {
66+
const validationErrors = error.details.map((detail) => detail.message);
67+
throw new Error(`Requester secret error: ${validationErrors.join('; ')}`);
68+
}
69+
return value;
70+
}
71+
72+
module.exports = {
73+
validateWorkerEnv,
74+
validateAppSecret,
75+
validateRequesterSecret,
76+
validateProtectedData,
77+
};
92.3 KB
Binary file not shown.
92 KB
Binary file not shown.

0 commit comments

Comments
 (0)