diff --git a/src/cleartext.js b/src/cleartext.js index 1a21cebc..9ab0f342 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -37,8 +37,8 @@ export class CleartextMessage { * @param {Signature} signature - The detached signature or an empty signature for unsigned messages */ constructor(text, signature) { - // remove trailing whitespace and normalize EOL to canonical form - this.text = util.removeTrailingSpaces(text).replace(/\r?\n/g, '\r\n'); + // remove trailing whitespace from each line and normalize line endings to LF + this.text = util.removeTrailingSpaces(text); if (signature && !(signature instanceof Signature)) { throw new Error('Invalid signature input'); } @@ -74,7 +74,8 @@ export class CleartextMessage { */ async sign(signingKeys, recipientKeys = [], signature = null, signingKeyIDs = [], date = new Date(), signingUserIDs = [], recipientUserIDs = [], notations = [], config = defaultConfig) { const literalDataPacket = new LiteralDataPacket(); - literalDataPacket.setText(this.text); + // normalize EOL to canonical form + literalDataPacket.setText(this.text.replace(/\n/g, '\r\n')); const newSignature = new Signature(await createSignaturePackets(literalDataPacket, signingKeys, recipientKeys, signature, signingKeyIDs, date, signingUserIDs, recipientUserIDs, notations, true, config)); return new CleartextMessage(this.text, newSignature); } @@ -94,8 +95,8 @@ export class CleartextMessage { verify(keys, date = new Date(), config = defaultConfig) { const signatureList = this.signature.packets.filterByTag(enums.packet.signature); // drop UnparsablePackets const literalDataPacket = new LiteralDataPacket(); - // we assume that cleartext signature is generated based on UTF8 cleartext - literalDataPacket.setText(this.text); + // cleartext signatures should be generated over UTF8 cleartext with canonical newlines + literalDataPacket.setText(this.text.replace(/\n/g, '\r\n')); return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true, config); } @@ -104,8 +105,7 @@ export class CleartextMessage { * @returns {String} Cleartext of message. */ getText() { - // normalize end of line to \n - return this.text.replace(/\r\n/g, '\n'); + return this.text; } /** diff --git a/src/encoding/armor.js b/src/encoding/armor.js index ee6c79f0..915a751a 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -273,7 +273,7 @@ export function unarmor(input) { // Reverse dash-escaping for msg text.push(line.replace(/^- /, '')); } else { - text = text.join('\r\n'); + text = text.join('\n'); textDone = true; verifyHeaders(lastHeaders); lastHeaders = []; diff --git a/test/general/armor.js b/test/general/armor.js index 3fe80d56..331fa617 100644 --- a/test/general/armor.js +++ b/test/general/armor.js @@ -252,7 +252,7 @@ export default () => describe('ASCII armor', function() { it('Do not filter blank lines after header', async function () { let msg = getArmor(['Hash: SHA1', '']); msg = await openpgp.readCleartextMessage({ cleartextMessage: msg }); - expect(msg.text).to.equal('\r\nsign this'); + expect(msg.getText()).to.equal('\nsign this'); }); it('Selectively output CRC checksum', async function () { @@ -353,9 +353,39 @@ NJCB6+LWtabSoVIjNVgKwyKqyTLaESNwC2ogZwkdE8qPGiDFEHo4Gg9zuRof ).to.equal( pubKey .replace('\n-', '-') - .replace(/\n\r/g, '\n') ); }); + it('Armored messages use LF newlines', async function () { + const usesLF = armoredData => { + return !armoredData.includes('\r'); + }; + + const { privateKey: v4Key } = await openpgp.generateKey({ userIDs: { email: 'v4@armor.test' }, format: 'object' }); + expect(usesLF(v4Key.armor())).to.be.true; + const { privateKey: v6Key } = await openpgp.generateKey({ userIDs: { email: 'v6@armor.test' }, config: { v6Keys: true, aeadProtect: true }, format: 'object' }); + expect(usesLF(v6Key.armor())).to.be.true; + + const messageWithSEIPDv1 = await openpgp.encrypt({ message: await openpgp.createMessage({ text: 'test' }), encryptionKeys: v4Key }); + expect(usesLF(messageWithSEIPDv1)).to.be.true; + const messageWithSEIPDv2 = await openpgp.encrypt({ message: await openpgp.createMessage({ text: 'test' }), encryptionKeys: v6Key }); + expect(usesLF(messageWithSEIPDv2)).to.be.true; + + const signatureV4V6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: [v4Key, v6Key] }); + expect(usesLF(signatureV4V6)).to.be.true; + const signatureV6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: v6Key }); + expect(usesLF(signatureV6)).to.be.true; + + const detachedSignatureV4V6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: [v4Key, v6Key], detached: true }); + expect(usesLF(detachedSignatureV4V6)).to.be.true; + const detachedSignatureV6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: v6Key, detached: true }); + expect(usesLF(detachedSignatureV6)).to.be.true; + + const cleartextSignatureV4V6 = await openpgp.sign({ message: await openpgp.createCleartextMessage({ text: 'test' }), signingKeys: [v4Key, v6Key] }); + expect(usesLF(cleartextSignatureV4V6)).to.be.true; + const cleartextSignatureV6 = await openpgp.sign({ message: await openpgp.createCleartextMessage({ text: 'test' }), signingKeys: v6Key }); + expect(usesLF(cleartextSignatureV6)).to.be.true; + }); + });