mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-25 08:27:19 +02:00
Support SASLprep for AES-256 revision 6 passwords
This commit is contained in:
parent
e909f1f64f
commit
2ea9c26d77
@ -34,6 +34,7 @@ import { Dict, isDict, isName, Name } from "./primitives.js";
|
|||||||
import { calculateMD5 } from "./calculate_md5.js";
|
import { calculateMD5 } from "./calculate_md5.js";
|
||||||
import { calculateSHA256 } from "./calculate_sha256.js";
|
import { calculateSHA256 } from "./calculate_sha256.js";
|
||||||
import { DecryptStream } from "./decrypt_stream.js";
|
import { DecryptStream } from "./decrypt_stream.js";
|
||||||
|
import { saslPrep } from "./sasl_prep.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {typeof AES128Cipher | typeof AES256Cipher | typeof ARCFourCipher
|
* @typedef {typeof AES128Cipher | typeof AES256Cipher | typeof ARCFourCipher
|
||||||
@ -853,6 +854,15 @@ class CipherTransform {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function utf8PasswordToBytes(password) {
|
||||||
|
try {
|
||||||
|
password = utf8StringToString(password);
|
||||||
|
} catch {
|
||||||
|
warn("CipherTransformFactory: Unable to convert UTF8 encoded password.");
|
||||||
|
}
|
||||||
|
return stringToBytes(password);
|
||||||
|
}
|
||||||
|
|
||||||
class CipherTransformFactory {
|
class CipherTransformFactory {
|
||||||
#fileId;
|
#fileId;
|
||||||
|
|
||||||
@ -1128,19 +1138,20 @@ class CipherTransformFactory {
|
|||||||
this.encryptMetadata = encryptMetadata;
|
this.encryptMetadata = encryptMetadata;
|
||||||
|
|
||||||
const fileIdBytes = stringToBytes(fileId);
|
const fileIdBytes = stringToBytes(fileId);
|
||||||
let passwordBytes;
|
let passwordBytes, rawPasswordBytes;
|
||||||
if (password) {
|
if (password) {
|
||||||
if (algorithm === 5) {
|
if (revision === 6) {
|
||||||
try {
|
const preppedPassword = saslPrep(password);
|
||||||
password = utf8StringToString(password);
|
passwordBytes = utf8PasswordToBytes(preppedPassword);
|
||||||
} catch {
|
if (preppedPassword !== password) {
|
||||||
warn(
|
rawPasswordBytes = utf8PasswordToBytes(password);
|
||||||
"CipherTransformFactory: Unable to convert UTF8 encoded password."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else if (algorithm === 5) {
|
||||||
|
passwordBytes = utf8PasswordToBytes(password);
|
||||||
|
} else {
|
||||||
passwordBytes = stringToBytes(password);
|
passwordBytes = stringToBytes(password);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let encryptionKey;
|
let encryptionKey;
|
||||||
if (algorithm !== 5) {
|
if (algorithm !== 5) {
|
||||||
@ -1163,9 +1174,12 @@ class CipherTransformFactory {
|
|||||||
const ownerEncryption = stringToBytes(dict.get("OE"));
|
const ownerEncryption = stringToBytes(dict.get("OE"));
|
||||||
const userEncryption = stringToBytes(dict.get("UE"));
|
const userEncryption = stringToBytes(dict.get("UE"));
|
||||||
const perms = stringToBytes(dict.get("Perms"));
|
const perms = stringToBytes(dict.get("Perms"));
|
||||||
|
for (const candidate of rawPasswordBytes
|
||||||
|
? [passwordBytes, rawPasswordBytes]
|
||||||
|
: [passwordBytes]) {
|
||||||
encryptionKey = this.#createEncryptionKey20(
|
encryptionKey = this.#createEncryptionKey20(
|
||||||
revision,
|
revision,
|
||||||
passwordBytes,
|
candidate,
|
||||||
ownerPassword,
|
ownerPassword,
|
||||||
ownerValidationSalt,
|
ownerValidationSalt,
|
||||||
ownerKeySalt,
|
ownerKeySalt,
|
||||||
@ -1177,6 +1191,10 @@ class CipherTransformFactory {
|
|||||||
userEncryption,
|
userEncryption,
|
||||||
perms
|
perms
|
||||||
);
|
);
|
||||||
|
if (encryptionKey) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!encryptionKey) {
|
if (!encryptionKey) {
|
||||||
if (!password) {
|
if (!password) {
|
||||||
|
|||||||
51
src/core/sasl_prep.js
Normal file
51
src/core/sasl_prep.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/* Copyright 2026 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NON_ASCII_SPACES = new Set([
|
||||||
|
0x00a0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
|
||||||
|
0x2007, 0x2008, 0x2009, 0x200a, 0x200b, 0x202f, 0x205f, 0x3000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const COMMONLY_MAPPED_TO_NOTHING = new Set([
|
||||||
|
0x00ad, 0x034f, 0x1806, 0x180b, 0x180c, 0x180d, 0x200b, 0x200c, 0x200d,
|
||||||
|
0x2060, 0xfe00, 0xfe01, 0xfe02, 0xfe03, 0xfe04, 0xfe05, 0xfe06, 0xfe07,
|
||||||
|
0xfe08, 0xfe09, 0xfe0a, 0xfe0b, 0xfe0c, 0xfe0d, 0xfe0e, 0xfe0f, 0xfeff,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function saslPrep(str) {
|
||||||
|
let mapped = "";
|
||||||
|
for (const char of str) {
|
||||||
|
const code = char.codePointAt(0);
|
||||||
|
if (NON_ASCII_SPACES.has(code)) {
|
||||||
|
mapped += " ";
|
||||||
|
} else if (!COMMONLY_MAPPED_TO_NOTHING.has(code)) {
|
||||||
|
mapped += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SASLprep also specifies prohibited-output and bidirectional-text checks.
|
||||||
|
// They are intentionally omitted here since this function is only used to
|
||||||
|
// derive password candidates when opening PDFs. Being permissive lets us
|
||||||
|
// handle non-conforming files whose producers omitted those checks too; the
|
||||||
|
// derived candidate must still pass the PDF password verification.
|
||||||
|
|
||||||
|
// TODO: SASLprep is based on Unicode 3.2, whereas String.prototype.normalize
|
||||||
|
// uses the Unicode version provided by the JavaScript runtime. Use frozen
|
||||||
|
// Unicode 3.2 normalization data if this difference causes interoperability
|
||||||
|
// problems in practice.
|
||||||
|
return mapped.normalize("NFKC");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { saslPrep };
|
||||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -944,3 +944,4 @@
|
|||||||
!issue20504.pdf
|
!issue20504.pdf
|
||||||
!issue20504_skia.pdf
|
!issue20504_skia.pdf
|
||||||
!issue21579.pdf
|
!issue21579.pdf
|
||||||
|
!saslprep-r6.pdf
|
||||||
|
|||||||
36
test/pdfs/saslprep-r6.pdf
Normal file
36
test/pdfs/saslprep-r6.pdf
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
%PDF-2.0
|
||||||
|
%¿÷¢þ
|
||||||
|
1 0 obj
|
||||||
|
<< /Pages 2 0 R /Type /Catalog >>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<< /Count 1 /Kids [ 3 0 R ] /MediaBox [ 0 0 400 200 ] /Type /Pages >>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<< /Contents 4 0 R /Parent 2 0 R /Resources << /Font << /F1 5 0 R >> >> /Type /Page >>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<< /Length 80 /Filter /FlateDecode >>
|
||||||
|
stream
|
||||||
|
èÊØ9%‹¥p~•±Ý;wVÛÐÉ™ aÇ1ðTg
õîáÓ†;vž€±)üôzík—8p/Ö·XÜNrã<08>º8ä0
|
||||||
|
âk$ ,ïÓpendstream
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<< /BaseFont /Helvetica /Subtype /Type1 /Type /Font >>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<< /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <2114dff5892a5f97b6b7ebf850027d59fd9582cabd1ca6545e8b7101fc9dd200847af489e83e58df71cfcef246a6b295> /OE <41f1bf91aa8bf033d86dcf0b1419d6bf8b2a4ffa6c6eaa6285f253427ccffb58> /P -4 /Perms <0efe0222fb3566a638740c6111174a66> /R 6 /StmF /StdCF /StrF /StdCF /U <981e8012145993440b792f07715ecc7bd1cd672c5b32ddb7c07dc7d51f355123bdca31fd6d7bb7d160297691d233e1b4> /UE <772076efadec02adc61b06de50debe38dda5dbc7378e03354c1dfddd1383ac9b> /V 5 >>
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 7
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000015 00000 n
|
||||||
|
0000000064 00000 n
|
||||||
|
0000000149 00000 n
|
||||||
|
0000000251 00000 n
|
||||||
|
0000000401 00000 n
|
||||||
|
0000000471 00000 n
|
||||||
|
trailer << /Root 1 0 R /Size 7 /ID [<20ecf0586687a9f2d48b7fa8bcafe19e><20ecf0586687a9f2d48b7fa8bcafe19e>] /Encrypt 6 0 R >>
|
||||||
|
startxref
|
||||||
|
1018
|
||||||
|
%%EOF
|
||||||
@ -14471,5 +14471,13 @@
|
|||||||
"rounds": 1,
|
"rounds": 1,
|
||||||
"type": "eq",
|
"type": "eq",
|
||||||
"password": "pässwört"
|
"password": "pässwört"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "saslprep-r6",
|
||||||
|
"file": "pdfs/saslprep-r6.pdf",
|
||||||
|
"md5": "2aee2434f164a00a52b98f29b6f9758c",
|
||||||
|
"rounds": 1,
|
||||||
|
"type": "eq",
|
||||||
|
"password": "S\u00AASL\u00ADprep"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -33,6 +33,7 @@ import {
|
|||||||
} from "../../src/shared/util.js";
|
} from "../../src/shared/util.js";
|
||||||
import { calculateMD5 } from "../../src/core/calculate_md5.js";
|
import { calculateMD5 } from "../../src/core/calculate_md5.js";
|
||||||
import { calculateSHA256 } from "../../src/core/calculate_sha256.js";
|
import { calculateSHA256 } from "../../src/core/calculate_sha256.js";
|
||||||
|
import { saslPrep } from "../../src/core/sasl_prep.js";
|
||||||
|
|
||||||
describe("crypto", function () {
|
describe("crypto", function () {
|
||||||
// RFC 1321, A.5 Test suite
|
// RFC 1321, A.5 Test suite
|
||||||
@ -519,6 +520,47 @@ describe("crypto", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("saslPrep", function () {
|
||||||
|
it("should leave ASCII strings unchanged", function () {
|
||||||
|
expect(saslPrep("")).toEqual("");
|
||||||
|
expect(saslPrep("password")).toEqual("password");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should map non-ASCII space characters to U+0020", function () {
|
||||||
|
expect(saslPrep("a\u00A0b")).toEqual("a b");
|
||||||
|
expect(saslPrep("a\u2003b")).toEqual("a b");
|
||||||
|
expect(saslPrep("a\u3000b")).toEqual("a b");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove characters commonly mapped to nothing", function () {
|
||||||
|
expect(saslPrep("pass\u00ADword")).toEqual("password");
|
||||||
|
expect(saslPrep("a\u200Db")).toEqual("ab");
|
||||||
|
expect(saslPrep("a\uFEFFb")).toEqual("ab");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply NFKC normalization", function () {
|
||||||
|
expect(saslPrep("\u00AA")).toEqual("a");
|
||||||
|
expect(saslPrep("\u2168")).toEqual("IX");
|
||||||
|
expect(saslPrep("\uFB01")).toEqual("fi");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should tolerate bidirectional text rejected by SASLprep", function () {
|
||||||
|
// A RandALCat character followed by a non-RandALCat character would fail
|
||||||
|
// the SASLprep bidirectional-text check, which we intentionally omit.
|
||||||
|
expect(saslPrep("\u0627\u0031")).toEqual("\u0627\u0031");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize using the runtime Unicode version", function () {
|
||||||
|
// U+1D2C was unassigned in Unicode 3.2, on which SASLprep is based, but
|
||||||
|
// current Unicode normalization maps it to LATIN CAPITAL LETTER A.
|
||||||
|
expect(saslPrep("\u1D2C")).toEqual("A");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prepare the password `SªSLprep`", function () {
|
||||||
|
expect(saslPrep("SªSLprep")).toEqual("SaSLprep");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("CipherTransformFactory", function () {
|
describe("CipherTransformFactory", function () {
|
||||||
function buildDict(map) {
|
function buildDict(map) {
|
||||||
const dict = new Dict();
|
const dict = new Dict();
|
||||||
@ -617,6 +659,7 @@ describe("CipherTransformFactory", function () {
|
|||||||
let fileId1, fileId2, dict1, dict2, dict3;
|
let fileId1, fileId2, dict1, dict2, dict3;
|
||||||
let aes256Dict, aes256IsoDict, aes256BlankDict, aes256IsoBlankDict;
|
let aes256Dict, aes256IsoDict, aes256BlankDict, aes256IsoBlankDict;
|
||||||
let aes256UnicodeDict;
|
let aes256UnicodeDict;
|
||||||
|
let aes256SaslPrepDict, aes256SaslPrepFallbackDict;
|
||||||
|
|
||||||
beforeAll(function () {
|
beforeAll(function () {
|
||||||
fileId1 = unescape("%F6%C6%AF%17%F3rR%8DRM%9A%80%D1%EF%DF%18");
|
fileId1 = unescape("%F6%C6%AF%17%F3rR%8DRM%9A%80%D1%EF%DF%18");
|
||||||
@ -780,12 +823,63 @@ describe("CipherTransformFactory", function () {
|
|||||||
P: -1084,
|
P: -1084,
|
||||||
R: 5,
|
R: 5,
|
||||||
});
|
});
|
||||||
|
aes256SaslPrepDict = buildDict({
|
||||||
|
Filter: Name.get("Standard"),
|
||||||
|
V: 5,
|
||||||
|
Length: 256,
|
||||||
|
O: unescape(
|
||||||
|
"%21%14%DF%F5%89%2A%5F%97%B6%B7%EB%F8P%02%7DY%FD%95%82%CA%BD" +
|
||||||
|
"%1C%A6T%5E%8Bq%01%FC%9D%D2%00%84z%F4%89%E8%3EX%DFq%CF%CE%F2F" +
|
||||||
|
"%A6%B2%95"
|
||||||
|
),
|
||||||
|
U: unescape(
|
||||||
|
"%98%1E%80%12%14Y%93D%0By%2F%07q%5E%CC%7B%D1%CDg%2C%5B2%DD%B7" +
|
||||||
|
"%C0%7D%C7%D5%1F5Q%23%BD%CA1%FDm%7B%B7%D1%60%29v%91%D23%E1%B4"
|
||||||
|
),
|
||||||
|
OE: unescape(
|
||||||
|
"A%F1%BF%91%AA%8B%F03%D8m%CF%0B%14%19%D6%BF%8B%2AO%FAln%AAb%85" +
|
||||||
|
"%F2SB%7C%CF%FBX"
|
||||||
|
),
|
||||||
|
UE: unescape(
|
||||||
|
"w%20v%EF%AD%EC%02%AD%C6%1B%06%DEP%DE%BE8%DD%A5%DB%C77%8E%035L" +
|
||||||
|
"%1D%FD%DD%13%83%AC%9B"
|
||||||
|
),
|
||||||
|
Perms: unescape("%0E%FE%02%22%FB5f%A68t%0Ca%11%17Jf"),
|
||||||
|
P: -4,
|
||||||
|
R: 6,
|
||||||
|
});
|
||||||
|
aes256SaslPrepFallbackDict = buildDict({
|
||||||
|
Filter: Name.get("Standard"),
|
||||||
|
V: 5,
|
||||||
|
Length: 256,
|
||||||
|
O: unescape(
|
||||||
|
"%B1O%A6A%BD%E9%19%09%EC%FFuG%3C%BEWZ%2CN%F6%C7Cz%9A%E1%84%15" +
|
||||||
|
"%0C66%AE%B4%E6%C0%C1%C2%C3%C4%C5%C6%C7%D0%D1%D2%D3%D4%D5%D6%D7"
|
||||||
|
),
|
||||||
|
U: unescape(
|
||||||
|
"i%0C%B0%B9%B7%90%AAXN%C916E%2E%C2%CF%BEG%DE%1Dqf%8F%3A%BF%92" +
|
||||||
|
"%FB%FA%C8o%23%11%A0%A1%A2%A3%A4%A5%A6%A7%B0%B1%B2%B3%B4%B5%B6" +
|
||||||
|
"%B7"
|
||||||
|
),
|
||||||
|
OE: unescape(
|
||||||
|
"%0BE%22%A4%AAX%DA%C5%D4%DAL%A0%91Q%3E%3AE%EE%B7%97W%CFm%B8%AC" +
|
||||||
|
"%8Ak%80%B4%5D%EB%0F"
|
||||||
|
),
|
||||||
|
UE: unescape(
|
||||||
|
"%D0%C0%E4rM%F7%E7%D2%FA%A0%90%7B%BC%BE%86p5%B5%27%A1%10%8A%86" +
|
||||||
|
"%86%95%2B%92%B4%E1%F4%A5%9A"
|
||||||
|
),
|
||||||
|
Perms: unescape("%ED%D4%5FM%ED%9E%DE%F9%82%C8%A9%7F%C1%CC%C4%B4"),
|
||||||
|
P: -4,
|
||||||
|
R: 6,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(function () {
|
afterAll(function () {
|
||||||
fileId1 = fileId2 = dict1 = dict2 = dict3 = null;
|
fileId1 = fileId2 = dict1 = dict2 = dict3 = null;
|
||||||
aes256Dict = aes256IsoDict = aes256BlankDict = aes256IsoBlankDict = null;
|
aes256Dict = aes256IsoDict = aes256BlankDict = aes256IsoBlankDict = null;
|
||||||
aes256UnicodeDict = null;
|
aes256UnicodeDict = null;
|
||||||
|
aes256SaslPrepDict = aes256SaslPrepFallbackDict = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#ctor", function () {
|
describe("#ctor", function () {
|
||||||
@ -844,6 +938,29 @@ describe("CipherTransformFactory", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("AES256 Revision 6 with SASLprep", function () {
|
||||||
|
it("should accept a password requiring SASLprep", function () {
|
||||||
|
ensurePasswordCorrect(
|
||||||
|
aes256SaslPrepDict,
|
||||||
|
fileId1,
|
||||||
|
"S\u00AASL\u00ADprep"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("should accept an already prepared password", function () {
|
||||||
|
ensurePasswordCorrect(aes256SaslPrepDict, fileId1, "SaSLprep");
|
||||||
|
});
|
||||||
|
it("should not accept wrong password", function () {
|
||||||
|
ensurePasswordIncorrect(aes256SaslPrepDict, fileId1, "wrong");
|
||||||
|
});
|
||||||
|
it("should fall back to the raw password", function () {
|
||||||
|
ensurePasswordCorrect(
|
||||||
|
aes256SaslPrepFallbackDict,
|
||||||
|
fileId1,
|
||||||
|
"pass\u00ADword"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should accept user password", function () {
|
it("should accept user password", function () {
|
||||||
ensurePasswordCorrect(dict1, fileId1, "123456");
|
ensurePasswordCorrect(dict1, fileId1, "123456");
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user