mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-07-25 16:37:22 +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 { calculateSHA256 } from "./calculate_sha256.js";
|
||||
import { DecryptStream } from "./decrypt_stream.js";
|
||||
import { saslPrep } from "./sasl_prep.js";
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
#fileId;
|
||||
|
||||
@ -1128,18 +1138,19 @@ class CipherTransformFactory {
|
||||
this.encryptMetadata = encryptMetadata;
|
||||
|
||||
const fileIdBytes = stringToBytes(fileId);
|
||||
let passwordBytes;
|
||||
let passwordBytes, rawPasswordBytes;
|
||||
if (password) {
|
||||
if (algorithm === 5) {
|
||||
try {
|
||||
password = utf8StringToString(password);
|
||||
} catch {
|
||||
warn(
|
||||
"CipherTransformFactory: Unable to convert UTF8 encoded password."
|
||||
);
|
||||
if (revision === 6) {
|
||||
const preppedPassword = saslPrep(password);
|
||||
passwordBytes = utf8PasswordToBytes(preppedPassword);
|
||||
if (preppedPassword !== password) {
|
||||
rawPasswordBytes = utf8PasswordToBytes(password);
|
||||
}
|
||||
} else if (algorithm === 5) {
|
||||
passwordBytes = utf8PasswordToBytes(password);
|
||||
} else {
|
||||
passwordBytes = stringToBytes(password);
|
||||
}
|
||||
passwordBytes = stringToBytes(password);
|
||||
}
|
||||
|
||||
let encryptionKey;
|
||||
@ -1163,20 +1174,27 @@ class CipherTransformFactory {
|
||||
const ownerEncryption = stringToBytes(dict.get("OE"));
|
||||
const userEncryption = stringToBytes(dict.get("UE"));
|
||||
const perms = stringToBytes(dict.get("Perms"));
|
||||
encryptionKey = this.#createEncryptionKey20(
|
||||
revision,
|
||||
passwordBytes,
|
||||
ownerPassword,
|
||||
ownerValidationSalt,
|
||||
ownerKeySalt,
|
||||
uBytes,
|
||||
userPassword,
|
||||
userValidationSalt,
|
||||
userKeySalt,
|
||||
ownerEncryption,
|
||||
userEncryption,
|
||||
perms
|
||||
);
|
||||
for (const candidate of rawPasswordBytes
|
||||
? [passwordBytes, rawPasswordBytes]
|
||||
: [passwordBytes]) {
|
||||
encryptionKey = this.#createEncryptionKey20(
|
||||
revision,
|
||||
candidate,
|
||||
ownerPassword,
|
||||
ownerValidationSalt,
|
||||
ownerKeySalt,
|
||||
uBytes,
|
||||
userPassword,
|
||||
userValidationSalt,
|
||||
userKeySalt,
|
||||
ownerEncryption,
|
||||
userEncryption,
|
||||
perms
|
||||
);
|
||||
if (encryptionKey) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!encryptionKey) {
|
||||
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_skia.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,
|
||||
"type": "eq",
|
||||
"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";
|
||||
import { calculateMD5 } from "../../src/core/calculate_md5.js";
|
||||
import { calculateSHA256 } from "../../src/core/calculate_sha256.js";
|
||||
import { saslPrep } from "../../src/core/sasl_prep.js";
|
||||
|
||||
describe("crypto", function () {
|
||||
// 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 () {
|
||||
function buildDict(map) {
|
||||
const dict = new Dict();
|
||||
@ -617,6 +659,7 @@ describe("CipherTransformFactory", function () {
|
||||
let fileId1, fileId2, dict1, dict2, dict3;
|
||||
let aes256Dict, aes256IsoDict, aes256BlankDict, aes256IsoBlankDict;
|
||||
let aes256UnicodeDict;
|
||||
let aes256SaslPrepDict, aes256SaslPrepFallbackDict;
|
||||
|
||||
beforeAll(function () {
|
||||
fileId1 = unescape("%F6%C6%AF%17%F3rR%8DRM%9A%80%D1%EF%DF%18");
|
||||
@ -780,12 +823,63 @@ describe("CipherTransformFactory", function () {
|
||||
P: -1084,
|
||||
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 () {
|
||||
fileId1 = fileId2 = dict1 = dict2 = dict3 = null;
|
||||
aes256Dict = aes256IsoDict = aes256BlankDict = aes256IsoBlankDict = null;
|
||||
aes256UnicodeDict = null;
|
||||
aes256SaslPrepDict = aes256SaslPrepFallbackDict = null;
|
||||
});
|
||||
|
||||
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 () {
|
||||
ensurePasswordCorrect(dict1, fileId1, "123456");
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user