Remove armor checksum check

The crypto refresh says that we MUST NOT reject messages where the
CRC24 checksum is incorrect. So, we remove the check for it.

Also, remove the checksumRequired config.
This commit is contained in:
Daniel Huigens 2022-12-13 15:04:07 +01:00 committed by larabr
parent 6f1eb06119
commit 939622e827
6 changed files with 47 additions and 238 deletions

1
openpgp.d.ts vendored
View File

@ -317,7 +317,6 @@ interface Config {
aeadProtect: boolean;
allowUnauthenticatedMessages: boolean;
allowUnauthenticatedStream: boolean;
checksumRequired: boolean;
minRSABits: number;
passwordCollisionCheck: boolean;
revocationsExpire: boolean;

View File

@ -130,11 +130,6 @@ export default {
* @property {Boolean} allowUnauthenticatedStream
*/
allowUnauthenticatedStream: false,
/**
* @memberof module:config
* @property {Boolean} checksumRequired Do not throw error when armor is missing a checksum
*/
checksumRequired: false,
/**
* Minimum RSA key size allowed for key generation and message signing, verification and encryption.
* The default is 2047 since due to a bug, previous versions of OpenPGP.js could generate 2047-bit keys instead of 2048-bit ones.

View File

@ -108,79 +108,6 @@ function addheader(customComment, config) {
}
/**
* Calculates a checksum over the given data and returns it base64 encoded
* @param {String | ReadableStream<String>} data - Data to create a CRC-24 checksum for
* @returns {String | ReadableStream<String>} Base64 encoded checksum.
* @private
*/
function getCheckSum(data) {
const crc = createcrc24(data);
return base64.encode(crc);
}
// https://create.stephan-brumme.com/crc32/#slicing-by-8-overview
const crc_table = [
new Array(0xFF),
new Array(0xFF),
new Array(0xFF),
new Array(0xFF)
];
for (let i = 0; i <= 0xFF; i++) {
let crc = i << 16;
for (let j = 0; j < 8; j++) {
crc = (crc << 1) ^ ((crc & 0x800000) !== 0 ? 0x864CFB : 0);
}
crc_table[0][i] =
((crc & 0xFF0000) >> 16) |
(crc & 0x00FF00) |
((crc & 0x0000FF) << 16);
}
for (let i = 0; i <= 0xFF; i++) {
crc_table[1][i] = (crc_table[0][i] >> 8) ^ crc_table[0][crc_table[0][i] & 0xFF];
}
for (let i = 0; i <= 0xFF; i++) {
crc_table[2][i] = (crc_table[1][i] >> 8) ^ crc_table[0][crc_table[1][i] & 0xFF];
}
for (let i = 0; i <= 0xFF; i++) {
crc_table[3][i] = (crc_table[2][i] >> 8) ^ crc_table[0][crc_table[2][i] & 0xFF];
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView#Endianness
const isLittleEndian = (function() {
const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 0xFF, true /* littleEndian */);
// Int16Array uses the platform's endianness.
return new Int16Array(buffer)[0] === 0xFF;
}());
/**
* Internal function to calculate a CRC-24 checksum over a given string (data)
* @param {String | ReadableStream<String>} input - Data to create a CRC-24 checksum for
* @returns {Uint8Array | ReadableStream<Uint8Array>} The CRC-24 checksum.
* @private
*/
function createcrc24(input) {
let crc = 0xCE04B7;
return stream.transform(input, value => {
const len32 = isLittleEndian ? Math.floor(value.length / 4) : 0;
const arr32 = new Uint32Array(value.buffer, value.byteOffset, len32);
for (let i = 0; i < len32; i++) {
crc ^= arr32[i];
crc =
crc_table[0][(crc >> 24) & 0xFF] ^
crc_table[1][(crc >> 16) & 0xFF] ^
crc_table[2][(crc >> 8) & 0xFF] ^
crc_table[3][(crc >> 0) & 0xFF];
}
for (let i = len32 * 4; i < value.length; i++) {
crc = (crc >> 8) ^ crc_table[0][(crc & 0xFF) ^ value[i]];
}
}, () => new Uint8Array([crc, crc >> 8, crc >> 16]));
}
/**
* Verify armored headers. crypto-refresh-06, section 6.2:
* "An OpenPGP implementation may consider improperly formatted Armor
@ -321,24 +248,6 @@ export function unarmor(input, config = defaultConfig) {
await writer.abort(e);
}
}));
data = stream.transformPair(data, async (readable, writable) => {
const checksumVerified = stream.readToEnd(getCheckSum(stream.passiveClone(readable)));
checksumVerified.catch(() => {});
await stream.pipe(readable, writable, {
preventClose: true
});
const writer = stream.getWriter(writable);
try {
const checksumVerifiedString = (await checksumVerified).replace('\n', '');
if (checksum !== checksumVerifiedString && (checksum || config.checksumRequired)) {
throw new Error('Ascii armor integrity check failed');
}
await writer.ready;
await writer.close();
} catch (e) {
await writer.abort(e);
}
});
} catch (e) {
reject(e);
}

View File

@ -171,15 +171,7 @@ export default () => describe('ASCII armor', function() {
'-----END PGP PRIVATE KEY BLOCK-----'
].join('\n');
// try with default config
await expect(openpgp.readKey({ armoredKey: privKey })).to.be.rejectedWith(/Ascii armor integrity check failed/);
// try opposite config
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
await expect(openpgp.readKey({ armoredKey: privKey })).to.be.rejectedWith(/Ascii armor integrity check failed/);
// back to default
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
await openpgp.readKey({ armoredKey: privKey });
});
it('Armor checksum validation - valid', async function () {
@ -203,15 +195,7 @@ export default () => describe('ASCII armor', function() {
'=wJNM',
'-----END PGP PRIVATE KEY BLOCK-----'].join('\n');
// try with default config
await openpgp.readKey({ armoredKey: privKey });
// try opposite config
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
await openpgp.readKey({ armoredKey: privKey });
// back to default
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
});
it('Armor checksum validation - missing', async function () {
@ -234,23 +218,7 @@ export default () => describe('ASCII armor', function() {
'71vlXMJNXvoCeuejiRw=',
'-----END PGP PRIVATE KEY BLOCK-----'].join('\n');
// try with default config
if (openpgp.config.checksumRequired) {
await expect(openpgp.readKey({ armoredKey: privKeyNoCheckSum })).to.be.rejectedWith(/Ascii armor integrity check failed/);
} else {
await openpgp.readKey({ armoredKey: privKeyNoCheckSum });
}
// try opposite config
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
if (openpgp.config.checksumRequired) {
await expect(openpgp.readKey({ armoredKey: privKeyNoCheckSum })).to.be.rejectedWith(/Ascii armor integrity check failed/);
} else {
await openpgp.readKey({ armoredKey: privKeyNoCheckSum });
}
// back to default
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
await openpgp.readKey({ armoredKey: privKeyNoCheckSum });
});
it('Armor checksum validation - missing - trailing newline', async function () {
@ -274,23 +242,7 @@ export default () => describe('ASCII armor', function() {
'-----END PGP PRIVATE KEY BLOCK-----',
''].join('\n');
// try with default config
if (openpgp.config.checksumRequired) {
await expect(openpgp.readKey({ armoredKey: privKeyNoCheckSumWithTrailingNewline })).to.be.rejectedWith(/Ascii armor integrity check failed/);
} else {
await openpgp.readKey({ armoredKey: privKeyNoCheckSumWithTrailingNewline });
}
// try opposite config
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
if (openpgp.config.checksumRequired) {
await expect(openpgp.readKey({ armoredKey: privKeyNoCheckSumWithTrailingNewline })).to.be.rejectedWith(/Ascii armor integrity check failed/);
} else {
await openpgp.readKey({ armoredKey: privKeyNoCheckSumWithTrailingNewline });
}
// back to default
openpgp.config.checksumRequired = !openpgp.config.checksumRequired;
await openpgp.readKey({ armoredKey: privKeyNoCheckSumWithTrailingNewline });
});
it('Accept header with trailing whitespace', async function () {

View File

@ -3057,60 +3057,51 @@ XfA3pqV4mTzF
expect(await isAEADSupported([key])).to.equal(openpgp.config.aeadProtect);
const data = await openpgp.encrypt({ message: await openpgp.createMessage({ binary: new Uint8Array(500) }), encryptionKeys: [key.toPublic()] });
let badSumEncrypted = data.replace(/\n=[a-zA-Z0-9/+]{4}/, '\n=aaaa');
if (badSumEncrypted === data) { // checksum was already =aaaa
badSumEncrypted = data.replace(/\n=[a-zA-Z0-9/+]{4}/, '\n=bbbb');
}
if (badSumEncrypted === data) {
throw new Error('Was not able to successfully modify checksum');
}
const badBodyEncrypted = data.replace(/\n=([a-zA-Z0-9/+]{4})/, 'aaa\n=$1');
const encrypted = data.substr(0, 500) + (data[500] === 'a' ? 'b' : 'a') + data.substr(501);
await loadStreamsPolyfill();
try {
for (const allowStreaming of [true, false]) {
openpgp.config.allowUnauthenticatedStream = allowStreaming;
await Promise.all([badSumEncrypted, badBodyEncrypted].map(async (encrypted, i) => {
await Promise.all([
encrypted,
new ReadableStream({
start(controller) {
controller.enqueue(encrypted);
for (const [i, encryptedData] of [
encrypted,
new ReadableStream({
start(controller) {
controller.enqueue(encrypted);
controller.close();
}
}),
new ReadableStream({
start() {
this.remaining = encrypted.split('\n');
},
async pull(controller) {
if (this.remaining.length) {
await new Promise(res => setTimeout(res));
controller.enqueue(this.remaining.shift() + '\n');
} else {
controller.close();
}
}),
new ReadableStream({
start() {
this.remaining = encrypted.split('\n');
},
async pull(controller) {
if (this.remaining.length) {
await new Promise(res => { setTimeout(res); });
controller.enqueue(this.remaining.shift() + '\n');
} else {
controller.close();
}
}
})
].map(async (encrypted, j) => {
let stepReached = 0;
try {
const message = await openpgp.readMessage({ armoredMessage: encrypted });
stepReached = 1;
const { data: decrypted } = await openpgp.decrypt({ message: message, decryptionKeys: [key] });
stepReached = 2;
await stream.readToEnd(decrypted);
} catch (e) {
expect(e.message).to.match(/Ascii armor integrity check failed/);
expect(stepReached).to.equal(
j === 0 ? 0 :
(openpgp.config.aeadChunkSizeByte === 0 && (j === 2 || detectNode() || util.getHardwareConcurrency() < 8)) || (!openpgp.config.aeadProtect && openpgp.config.allowUnauthenticatedStream) ? 2 :
1
);
return;
}
throw new Error(`Expected "Ascii armor integrity check failed" error in subtest ${i}.${j}`);
}));
}));
})
].entries()) {
let stepReached = 0;
try {
const message = await openpgp.readMessage({ armoredMessage: encryptedData });
stepReached = 1;
const { data: decrypted } = await openpgp.decrypt({ message: message, decryptionKeys: [key] });
stepReached = 2;
await stream.readToEnd(decrypted);
} catch (e) {
expect(e.message).to.match(/Modification detected|Authentication tag mismatch|Unsupported state or unable to authenticate data/);
expect(stepReached).to.equal(
i === 0 ? 1 :
(openpgp.config.aeadChunkSizeByte === 0 && (i === 2 || detectNode() || util.getHardwareConcurrency() < 8)) || (!openpgp.config.aeadProtect && openpgp.config.allowUnauthenticatedStream) ? 2 :
1
);
continue;
}
throw new Error(`Expected "Modification detected" error in subtest ${i}`);
}
}
} finally {
openpgp.config.allowUnauthenticatedStream = allowUnauthenticatedStream;

View File

@ -403,7 +403,7 @@ function tests() {
}
});
it('Detect MDC modifications (allowUnauthenticatedStream=true)', async function() {
it('Detect modification (allowUnauthenticatedStream=true)', async function() {
const aeadProtectValue = openpgp.config.aeadProtect;
openpgp.config.aeadProtect = false;
const allowUnauthenticatedStreamValue = openpgp.config.allowUnauthenticatedStream;
@ -418,7 +418,6 @@ function tests() {
const message = await openpgp.readMessage({
armoredMessage: stream[expectedType === 'node' ? 'webToNode' : 'toStream'](stream.transform(encrypted, value => {
value += '';
if (value === '=' || value.length === 5) return; // Remove checksum
const newlineIndex = value.indexOf('\n', 500);
if (value.length > 1000) return value.slice(0, newlineIndex - 1) + (value[newlineIndex - 1] === 'a' ? 'b' : 'a') + value.slice(newlineIndex);
return value;
@ -441,44 +440,7 @@ function tests() {
}
});
it('Detect armor checksum error (allowUnauthenticatedStream=true)', async function() {
const allowUnauthenticatedStreamValue = openpgp.config.allowUnauthenticatedStream;
openpgp.config.allowUnauthenticatedStream = true;
try {
const encrypted = await openpgp.encrypt({
message: await openpgp.createMessage({ binary: data }),
encryptionKeys: pubKey,
signingKeys: privKey,
config: { minRSABits: 1024 }
});
expect(stream.isStream(encrypted)).to.equal(expectedType);
const message = await openpgp.readMessage({
armoredMessage: stream[expectedType === 'node' ? 'webToNode' : 'toStream'](stream.transform(encrypted, value => {
value += '';
const newlineIndex = value.indexOf('\n', 500);
if (value.length > 1000) return value.slice(0, newlineIndex - 1) + (value[newlineIndex - 1] === 'a' ? 'b' : 'a') + value.slice(newlineIndex);
return value;
}), { encoding: 'utf8' })
});
const decrypted = await openpgp.decrypt({
verificationKeys: pubKey,
decryptionKeys: privKey,
message,
format: 'binary'
});
expect(stream.isStream(decrypted.data)).to.equal(expectedType);
const reader = stream.getReader(decrypted.data);
expect(await reader.peekBytes(1024)).not.to.deep.equal(plaintext[0]);
dataArrived();
await expect(reader.readToEnd()).to.be.rejectedWith('Ascii armor integrity check failed');
expect(decrypted.signatures).to.exist.and.have.length(1);
} finally {
openpgp.config.allowUnauthenticatedStream = allowUnauthenticatedStreamValue;
}
});
it('Detect armor checksum error when not passing public keys (allowUnauthenticatedStream=true)', async function() {
it('Detect modification when not passing public keys (allowUnauthenticatedStream=true)', async function() {
const allowUnauthenticatedStreamValue = openpgp.config.allowUnauthenticatedStream;
openpgp.config.allowUnauthenticatedStream = true;
try {
@ -507,7 +469,7 @@ function tests() {
const reader = stream.getReader(decrypted.data);
expect(await reader.peekBytes(1024)).not.to.deep.equal(plaintext[0]);
dataArrived();
await expect(reader.readToEnd()).to.be.rejectedWith('Ascii armor integrity check failed');
await expect(reader.readToEnd()).to.be.rejectedWith('Modification detected.');
expect(decrypted.signatures).to.exist.and.have.length(1);
await expect(decrypted.signatures[0].verified).to.be.eventually.rejectedWith(/Could not find signing key/);
} finally {
@ -515,7 +477,7 @@ function tests() {
}
});
it('Sign/verify: Detect armor checksum error', async function() {
it('Sign/verify: Detect modification', async function() {
const signed = await openpgp.sign({
message: await openpgp.createMessage({ binary: data }),
signingKeys: privKey,
@ -541,8 +503,9 @@ function tests() {
const reader = stream.getReader(verified.data);
expect(await reader.peekBytes(1024)).not.to.deep.equal(plaintext[0]);
dataArrived();
await expect(reader.readToEnd()).to.be.rejectedWith('Ascii armor integrity check failed');
expect(verified.signatures).to.exist.and.have.length(1);
await reader.readToEnd();
await expect(verified.signatures[0].verified).to.be.rejectedWith('Signed digest did not match');
});
it('stream.transformPair()', async function() {