Skip to content

Commit

Permalink
Improve autocrypt parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
mmso committed Mar 12, 2021
1 parent 5ea7404 commit 6687fbb
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 36 deletions.
54 changes: 18 additions & 36 deletions src/app/message/services/attachedPublicKey.js
@@ -1,21 +1,14 @@
import _ from 'lodash';
import vCard from 'vcf';
import {
arrayToBinaryString,
binaryStringToArray,
decodeBase64,
decryptMessageLegacy,
getFingerprint,
getMatchingKey,
keyInfo
} from 'pmcrypto';
import { arrayToBinaryString, decryptMessageLegacy, getFingerprint, getMatchingKey, keyInfo } from 'pmcrypto';

import { VERIFICATION_STATUS, EMAIL_FORMATING, KEY_FLAGS, LARGE_KEY_SIZE, SEND_TYPES } from '../../constants';
import { toList } from '../../../helpers/arrayHelper';
import { getGroup } from '../../../helpers/vcard';
import { normalizeEmail } from '../../../helpers/string';
import { addGetKeys, getKeyAsUri } from '../../../helpers/key';
import { isInternal, getDate } from '../../../helpers/message';
import { getParsedAutocryptHeader } from './autocryptHelper';

const { OPEN_TAG_AUTOCOMPLETE_RAW, CLOSE_TAG_AUTOCOMPLETE_RAW } = EMAIL_FORMATING;
const { SIGNED_AND_INVALID } = VERIFICATION_STATUS;
Expand Down Expand Up @@ -138,33 +131,21 @@ function attachedPublicKey(
return publicKey.armor();
};

const extractPublicKeysFromAutocrypt = (message) => {
const extractPublicKeysFromAutocrypt = async (message, keyParser) => {
if (!_.has(message.ParsedHeaders, 'Autocrypt')) {
return [];
}
const autocrypt = toList(message.ParsedHeaders.Autocrypt);
return _.filter(
autocrypt.map((header) => {
const match = header.match(
/^(\s*(_[^;\s]*|addr|prefer-encrypt)\s*=\s*[^;\s]*\s*;)*\s*keydata\s*=([^;]*)$/
);
if (!match) {
return null;
}
const preferEncryptMutual = header.match(
/^(\s*(_[^;\s]*|addr)\s*=\s*[^;\s]*\s*;)*\s*prefer-encrypt\s*=\s*mutual\s*;/
);
if (!preferEncryptMutual) {
return null;
}
const keydata = header.match(/^(?:\s*(?:[^;\s]*)\s*=\s*[^;\s]*\s*;)*\s*keydata\s*=([^;]*)$/);
try {
return binaryStringToArray(decodeBase64(keydata[1]));
} catch (e) {
// not encoded correctly
return null;
}
})
await Promise.all(
autocrypt.map(async (header) => {
const result = getParsedAutocryptHeader(header, message.Sender.Address);
if (!result) {
return;
}
return keyParser(result.keydata);
})
)
);
};

Expand Down Expand Up @@ -229,21 +210,21 @@ function attachedPublicKey(
return false;
}

const isKey = (key) =>
keyInfo(key).catch(() => {
const keyParser = (key) => {
return keyInfo(key).catch(() => {
return false;
});
};

const buffers = (
await Promise.all(candidates.map((c) => AttachmentLoader.get(c, message).catch(() => false)))
).filter(Boolean);
const armoredFiles = buffers.map(arrayToBinaryString);
const keyInfos = _.filter(await Promise.all(armoredFiles.map(isKey)));
const keyInfos = _.filter(await Promise.all(armoredFiles.map(keyParser)));

if (keyInfos.length === 0) {
// try to get them from the autocrypt headers
const autocryptdata = extractPublicKeysFromAutocrypt(message);
keyInfos.push(..._.filter(await Promise.all(autocryptdata.map(isKey))));
keyInfos.push(...(await extractPublicKeysFromAutocrypt(message, keyParser)));
}

const keyInfoObject = await getMatchingKeyInfo(keyInfos, message);
Expand Down Expand Up @@ -343,4 +324,5 @@ function attachedPublicKey(
attachPublicKey
};
}

export default attachedPublicKey;
62 changes: 62 additions & 0 deletions src/app/message/services/autocryptHelper.js
@@ -0,0 +1,62 @@
import { binaryStringToArray, decodeBase64 } from 'pmcrypto';

const MANDATORY_FIELDS = ['keydata', 'addr'];
const OPTIONAL_FIELDS = ['prefer-encrypt'];
const CRITICAL_FIELDS = OPTIONAL_FIELDS.concat(MANDATORY_FIELDS);

// Parse according to https://autocrypt.org/level1.html#the-autocrypt-header
export const getParsedAutocryptHeader = (header = '', sender = '') => {
let invalid = false;

const result = Object.fromEntries(
header
.split(';')
.map((keyValue) => {
const trimmedKeyValue = keyValue.trim();

// For ease of parsing, the keydata attribute MUST be the last attribute in this header. Avoid splitting by = since it's base64
if (trimmedKeyValue.startsWith('keydata=')) {
try {
const keydataStringValue = trimmedKeyValue.slice('keydata='.length);
const keydataValue = binaryStringToArray(decodeBase64(keydataStringValue));
return ['keydata', keydataValue];
} catch (e) {
return ['', ''];
}
}

const [parsedKey = '', parsedValue = ''] = keyValue.split('=');

const key = parsedKey.trim();

// It MUST treat the entire Autocrypt header as invalid if it encounters a “critical” attribute that it doesn’t support.
if (!CRITICAL_FIELDS.includes(key) && !key.startsWith('_')) {
invalid = true;
return ['', ''];
}

return [key, parsedValue.trim()];
})
.filter(([key, value]) => {
return key && value;
})
);

// The mandatory fields must be present.
if (MANDATORY_FIELDS.some((field) => !result[field]) || invalid) {
return;
}

// If addr differs from the addr in the From header, the entire Autocrypt header MUST be treated as invalid.
if (result.addr.toLowerCase() !== sender.toLowerCase()) {
return;
}

// The prefer-encrypt attribute is optional and can only occur with the value mutual.
// Its presence in the Autocrypt header indicates an agreement to enable encryption by default.
if (result['prefer-encrypt'] !== 'mutual') {
return;
}

return result;
};
47 changes: 47 additions & 0 deletions test/specs/message/services/autocryptHelper.spec.js
@@ -0,0 +1,47 @@
import { getParsedAutocryptHeader } from '../../../../src/app/message/services/autocryptHelper';

const validKeyData = {
base64: ` xjMEYAffqRYJKwYBBAHaRw8BAQdAFKaMT9wf5w6gMeW6X+6CSKCwTw5ohqESZQXzNfHG+CrN FDxwcm90b25xYUB5YWhvby5jb20+wncEEBYKAB8FAmAH36kGCwkHCAMCBBUICgIDFgIBAhkB AhsDAh4BAAoJEHhS0KVKBiDsdGgBAMzShBIHmMcpfR9wfXQdJZJBsO/3NTqJfpR6lQM7b0Yh AQDmVdb5v9XofabzXILvXFCsY6G8m2enwfCZw00YK/C0Dc44BGAH36kSCisGAQQBl1UBBQEB B0CnB3qq73mdvkEfyixD+hAk3+5vW/Gg5rZaPoV0gixGOwMBCAfCYQQYFggACQUCYAffqQIb DAAKCRB4UtClSgYg7On/AQDAh4kb5SvbWpxvAj2XJjSD3VnoTq4mXiYVX+5porb2XgEAijZr EgjyGGjkRTwRZ7+ufgn+Qfvk/6+uc7/3efwlngA=`,
uint8array: new Uint8Array([198, 51, 4, 96, 7, 223, 169, 22, 9, 43, 6, 1, 4, 1, 218, 71, 15, 1, 1, 7, 64, 20, 166, 140, 79, 220, 31, 231, 14, 160, 49, 229, 186, 95, 238, 130, 72, 160, 176, 79, 14, 104, 134, 161, 18, 101, 5, 243, 53, 241, 198, 248, 42, 205, 20, 60, 112, 114, 111, 116, 111, 110, 113, 97, 64, 121, 97, 104, 111, 111, 46, 99, 111, 109, 62, 194, 119, 4, 16, 22, 10, 0, 31, 5, 2, 96, 7, 223, 169, 6, 11, 9, 7, 8, 3, 2, 4, 21, 8, 10, 2, 3, 22, 2, 1, 2, 25, 1, 2, 27, 3, 2, 30, 1, 0, 10, 9, 16, 120, 82, 208, 165, 74, 6, 32, 236, 116, 104, 1, 0, 204, 210, 132, 18, 7, 152, 199, 41, 125, 31, 112, 125, 116, 29, 37, 146, 65, 176, 239, 247, 53, 58, 137, 126, 148, 122, 149, 3, 59, 111, 70, 33, 1, 0, 230, 85, 214, 249, 191, 213, 232, 125, 166, 243, 92, 130, 239, 92, 80, 172, 99, 161, 188, 155, 103, 167, 193, 240, 153, 195, 77, 24, 43, 240, 180, 13, 206, 56, 4, 96, 7, 223, 169, 18, 10, 43, 6, 1, 4, 1, 151, 85, 1, 5, 1, 1, 7, 64, 167, 7, 122, 170, 239, 121, 157, 190, 65, 31, 202, 44, 67, 250, 16, 36, 223, 238, 111, 91, 241, 160, 230, 182, 90, 62, 133, 116, 130, 44, 70, 59, 3, 1, 8, 7, 194, 97, 4, 24, 22, 8, 0, 9, 5, 2, 96, 7, 223, 169, 2, 27, 12, 0, 10, 9, 16, 120, 82, 208, 165, 74, 6, 32, 236, 233, 255, 1, 0, 192, 135, 137, 27, 229, 43, 219, 90, 156, 111, 2, 61, 151, 38, 52, 131, 221, 89, 232, 78, 174, 38, 94, 38, 21, 95, 238, 105, 162, 182, 246, 94, 1, 0, 138, 54, 107, 18, 8, 242, 24, 104, 228, 69, 60, 17, 103, 191, 174, 126, 9, 254, 65, 251, 228, 255, 175, 174, 115, 191, 247, 121, 252, 37, 158, 0])
};

describe('autocrypt helper', () => {
it('should parse a valid string', () => {
const result = `addr=test@yahoo.com; prefer-encrypt=mutual; keydata=${validKeyData.base64}`;
expect(getParsedAutocryptHeader(result, 'test@yahoo.com')).toEqual({
addr: 'test@yahoo.com',
'prefer-encrypt': 'mutual',
keydata: validKeyData.uint8array
});
});

it('should not parse a valid string that does not contain prefer-encrypt', () => {
const result = `addr=test@yahoo.com; keydata=${validKeyData.base64}`;
expect(getParsedAutocryptHeader(result, 'test@yahoo.com')).toEqual(undefined);
});

it('should not parse a valid string that contains an invalid prefer-encrypt', () => {
const result = `addr=test@yahoo.com; _other=test; prefer-encrypt=none; keydata=${validKeyData.base64}`;
expect(getParsedAutocryptHeader(result, 'test@yahoo.com')).toEqual(undefined);
});

it('should not parse an invalid string that contains critical unknown attributes', () => {
const result = `addr=test@yahoo.com; other=test; prefer-encrypt=mutual; keydata=${validKeyData.base64}`;
expect(getParsedAutocryptHeader(result, 'test@yahoo.com')).toEqual(undefined);
});

it('should not parse an invalid string that does not contain addr', () => {
const result = `other=test; prefer-encrypt=mutual; keydata=${validKeyData.base64}`;
expect(getParsedAutocryptHeader(result, 'test@yahoo.com')).toEqual(undefined);
});

it('should not parse an invalid string', () => {
const result = `addr=test@yahoo.com; prefer-encrypt=none; keydata=${validKeyData.base64}`;
expect(getParsedAutocryptHeader(result, 'test@yahoo.com')).toEqual(undefined);
});

it('should not parse an unknown sender', () => {
const result = `addr=unknown@yahoo.com; prefer-encrypt=none; keydata=${validKeyData.base64}`;
expect(getParsedAutocryptHeader(result, 'test@yahoo.com')).toEqual(undefined);
});
});

2 comments on commit 6687fbb

@Hanna4156
Copy link

Choose a reason for hiding this comment

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

src/app/message/services/attachedPublicKey.js

@Hanna4156
Copy link

Choose a reason for hiding this comment

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

Please sign in to comment.