From 2fbf0279ee5850025bbbc392315e20eca6f234d4 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 24 Feb 2026 15:49:13 +0100 Subject: [PATCH] Add support for `AuthEvent` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This solves GH-20049, it’s an alternative to GH-20140. The analysis is a bit different. It builds on [my comment in the other issue](https://github.com/mozilla/pdf.js/issues/20139#issuecomment-3952462166), and the fact that they are *separate* things. To recap, it is possible to have a document plain text but have attachments encrypted. In that case, instead of prompting upfront, PDFs can prompt later with `/AuthEvent /EFOpen`. The default is `/AuthEvent /DocOpen`. Which is typical. So `/AuthEvent` is uncommon. So, separate things: * encrypted attachments (regardless of `/AuthEvent`) (GH-20139) * `/AuthEvent /EFOpen` (regardless of whether there are attachments) (this issue/PR) This PR stops prompting for a password on doc open if there is an `/AuthEvent /EFOpen`. It also does not list delayed encrypted attachments in the sidebar. It does that to prevent an infinite loading screen in a known case but also so that there is a place marked in the code where future logic, after GH-20139, can support lazily decrypting attachments. --- src/core/catalog.js | 11 ++++++++++- src/core/xref.js | 33 +++++++++++++++++++++------------ test/pdfs/.gitignore | 1 + test/pdfs/issue20049.pdf | Bin 0 -> 11722 bytes test/test_manifest.json | 7 +++++++ test/unit/api_spec.js | 14 ++++++++++++++ 6 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 test/pdfs/issue20049.pdf diff --git a/src/core/catalog.js b/src/core/catalog.js index 9ab10f281a0f5..490a1fe3b54fe 100644 --- a/src/core/catalog.js +++ b/src/core/catalog.js @@ -1054,7 +1054,16 @@ class Catalog { const obj = this.#catDict.get("Names"); let attachments = null; - if (obj instanceof Dict && obj.has("EmbeddedFiles")) { + if ( + obj instanceof Dict && + obj.has("EmbeddedFiles") && + // Note: decrypting attachments is not supported regardless. + // If it was, then `decryptOnAttachmentOpen` would signal whether to do + // so lazily. + // As it stands, we can at least avoid users getting to an infinite loader + // in the case of `decryptOnAttachmentOpen`. + !this.xref.decryptOnAttachmentOpen + ) { const nameTree = new NameTree(obj.getRaw("EmbeddedFiles"), this.xref); for (const [key, value] of nameTree.getAll()) { const fs = new FileSpec(value); diff --git a/src/core/xref.js b/src/core/xref.js index 6ff5a1e52e059..7763219b487f8 100644 --- a/src/core/xref.js +++ b/src/core/xref.js @@ -43,6 +43,7 @@ class XRef { this._newPersistentRefNum = null; this._newTemporaryRefNum = null; this._persistentRefsCache = null; + this.decryptOnAttachmentOpen = false; } getNewPersistentRef(obj) { @@ -117,18 +118,26 @@ class XRef { warn(`XRef.parse - Invalid "Encrypt" reference: "${ex}".`); } if (encrypt instanceof Dict) { - const ids = trailerDict.get("ID"); - const fileId = ids?.length ? ids[0] : ""; - // The 'Encrypt' dictionary itself should not be encrypted, and by - // setting `suppressEncryption` we can prevent an infinite loop inside - // of `XRef_fetchUncompressed` if the dictionary contains indirect - // objects (fixes issue7665.pdf). - encrypt.suppressEncryption = true; - this.encrypt = new CipherTransformFactory( - encrypt, - fileId, - this.pdfManager.password - ); + // Note: decrypting attachments is not supported regardless. + // But it is at least possible to honour `/AuthEvent /EFOpen` by not + // asking for a password on document open. + this.decryptOnAttachmentOpen = + encrypt.get("CF")?.get("StdCF")?.get("AuthEvent")?.name === "EFOpen"; + + if (!this.decryptOnAttachmentOpen) { + const ids = trailerDict.get("ID"); + const fileId = ids?.length ? ids[0] : ""; + // The 'Encrypt' dictionary itself should not be encrypted, and by + // setting `suppressEncryption` we can prevent an infinite loop inside + // of `XRef_fetchUncompressed` if the dictionary contains indirect + // objects (fixes issue7665.pdf). + encrypt.suppressEncryption = true; + this.encrypt = new CipherTransformFactory( + encrypt, + fileId, + this.pdfManager.password + ); + } } // Get the root dictionary (catalog) object, and do some basic validation. diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 73665e3be4df2..e2aaf1047e624 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -385,6 +385,7 @@ !bug1020226.pdf !issue9534_reduced.pdf !attachment.pdf +!issue20049.pdf !basicapi.pdf !issue15590.pdf !issue15594_reduced.pdf diff --git a/test/pdfs/issue20049.pdf b/test/pdfs/issue20049.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ec0ed82e0f42152781823759c7ca4c1a59be8698 GIT binary patch literal 11722 zcmeHNc|4Te+qXs8B5PWVH3~Ds3`X{4?Ac{!#*E#}G{&ARS+XS}TP2C8Y*DtzR*_{8 zBH2k~Pj>Z=Ry|Me?|J|D{qg?s+#f#9eeQE@-*cVIIp?|r^i|=aU@<8Mfw6JoD+XFH z7ytrbkuD6fvOpbY3>txV_CuopV4w~F43-iX2kHU95TF`B3Idh@o+0%cllmbbX`lwc z5&)3`1He#_wY)q78iU&90{Fwj0EYdNp^CQ0qR{#Xyt@ZUq6*f{(;edhfB^OJXq2-( z0gDHK#pUIJY8ZRGFOEQZA(0va;m&RZG#&_dLlDs4H~=+OEoI?w2pkLo!KLAn%4(`= zU?pWWDM=|+aV5AKTt!VuLS9x`Sz260RU85VNkJfRC{#%e3|9gxOGBlU!O|e8io7*Y z6N5tgSdwr6Lv~*rumCXVt56*@#*yFzfJ*Hu=k?WW7|;-lC21w`b$}5GDu6f?XzYta z1I-N44#2aPV1PIPOajD%fJY4bW;x`IMpv0#FA% zpz-8B+75|IVKA((y`;MBD7(Y~s3m(jMzgw$=yxXHvGpJz@B|+`+JOPYARwTo2mggO zgE-_D#3}$O@E;Kaz$Ad*jgpiC8UZAvf78>>&JJy7r->q?SPG=i4Pw4z&MD1x<(XM# zN^oX!a71utJYTeDC7E$?>)G6#2Hd0wJ^h=K%-h3c6!c{J`80R`uJPB|`|eySaMFAm z5l~14-tQ5U$vPY-~e zNsg*$Ue~yX6(l}IiFB9qwPDHryN@tXPfdu}S~0_UfT!m|PY?CmAPp{G9M_kw72ei1q;N1{A|x3_R@# z#&|UP>#CIe+WB=cs*pUy4ePi^)ZX=h#Slm+dH}%RC{*xRoHEwOa(C53VNfw~X@E2Y zEG7-JCJ9G5BmS3fq!>ox@?8}}v|M7H46U^rK&(5b)~uhm>85Az6L#wnS5mVw zK+N)ZB^Pu&%4$P>_P^@Z&au=?wR~ZNYxHH2yd;ICg z_?no^#gJ@|GrA4FNt+2#xA!OFB=WdJnF?}is5r?6;vWAYEPwT)?_T_e+58@G#X*w4 zCe4T3I!-RSUXdw$Ydfl}drb0lGscU$u|7pvY0+*ps^O&Se66az+x+n9CCa4~SsBJbA_&a=f(B4Z%zPZ4UiP15y8H5wU>hzK8Bq>r>nVJbJIN;S zMydG^abN0}gDdU+=f;rM0#Ft0gN@1{QZq=OB0N;kOLZTdm08#nWn4?H9>Hs`#!7cF1o`^F zGL!!EIs;X;$-x-c0}J`kk3(rYFU*Jgr^;UUJ1zvgt|Zgn4xy?in2`rTGR;`$Kh#+r zVrgkyqu^vBXEYN`;@A zj8?5jQucx%$0S9fQOUEz;$NsUcFksu0sZ5O|@;E63ldq6jx(C zJRiACLAF1!5%vVn7`*{=x=H4f<2VsT6{(5LVSb(BJo&!KVRNa(Xafm8mDr#=ddWwJ zxKc`g8N?A(h`pvpE@1AS>J&ehe{^ony!V~c#?$`c;}?u}WZ}Cb=n~EpFSo)+;HunjeR1;^O$Pu%y+a-l%A7*+ z3c=7v_Bg1&==9P3>h&!nsy=MjX*g;JMzzY>{Cp2INnJ*Fi8Y&gk&RoigV0PJW%jDAjzB_E81vk7iVW#lw3^NhnPKml<`IF;jyE* z3+0?v;uvTFRoTJUF;zhy8W=_emwcX_9AaGvIauehh&<0zLmPNB@c3{X_w%PGt{kwS zat%4ia}B`n#&68TwSQ{=PAGFd#2`3VkH8zwQ^O_KPHbi=U=@a@0#=>*=x2wB@AG5_;pbNsK-({;u?0ziqF46lY@rEtr%fxoXOi^W|U-_oXr-m81nX6oBTs2&WxlFi{ zA=$ zEirS2jGoev{HIq zZe~>GQ^ZX~)~G$-z4-f`C$lWFPCu8ne|#=#Nq*c?uqg6|W`bsHv#wEJ_mHt&C9#x9 z@A}S35ON<{_2PEXTXXXV@ZMXn2HB?bS-$BTr|9IO&7yCx53<{e%!wF@eh}d{7dGE# z=3Y8ewAt{cG0oN5KHx>T%eZ|>d(PmyNBMc8!(LD40#2t{7Y(vkov9+OGOqG&32uRD zQT$5t?(hxscHwr0_AEMjjzx}cQBHj4 zxzX~Y_;@jtSd6U?F0)0u;M)$_SKDqv#@~u+`wGYOtN54qCdv%J7 zNM~~?x$tcK*}}4fHROWJ!d-b8`AGR#d6pWln)?C3b;J(Ej@Rbk$JdKfo3FR@sHCYL z9C%NAhYC(*N+VAve&EnPPpXDG^17-JZeW)!zg0Ioh4QwFSD5SpD7`h?rU$2(2;_m_ ztZ%uA&jlZEDp91Gs!C&(MqE=@lX`S4yEeNwyjlI3T93M}n!j4Ins&08hIWtQ&X`^*6Zl)c&g;khoPOS$&J>L_L>EnY?|W~dP(ccL2D0LcQWRK9fXnKxm%t=Ji_ zBjjQ3P?d7|v(6`t82)sDi{^ACE)Rzs)zKL*4O*GstxTj%U|Nnfe{9~m0Cj+Rdpg#5 zeGcEQgU-7$=7;qbSPfL1@wTq39Q{=DA;Y+k_9p<^g9n23uD{=c=<; zv$woQy!Ly)9h|QGXqJjJx9RUF7_rWuY+b&+Tvl~^dYF3li~2avp3~j0rE;YbCF^4oo)?dwdx6~<`K02{a(`N1 zBCBGZN}30@H1=Yhd*LR%Y`FlrLKRg}u>c+A5 zW1fX$I^ae?^<4N)Z13@^o7Ze-FE~#>T5zwP#+B?`8_&O7voBz3 z(|QXyv|1T3dHKtYBb3q9oAY8d{$Fg~%1s9h?R3)Q9kfws^^@Ll+OeM#rU1s#j)jy|yEkTyl&HFING6N!lpXYuHE3Bc6hn!e`X;)rm11F@t`^E6)~N zVv=8thdmYk{CF*?^M1w!@2zS_WOL)9Yv+el1+Agn8rWvaFV~WyC6YpFgm@>?7#z@NRf5^yAhacBIn3cBKDdlMa!Bem`}A z0T7AZo2Vc6I6mb0X6|m5XAfT>R(!tnaWt;)vd2fn@R8GAhy2fBtWI;Ev2u)j<&(*? z)8ukjdw^dz{q{9eS0PsceSXz!HPC@fwnr*RiqhZ?!8mS#PX|q%Z`bzS*(T>_U2>%0 zG0xn1uZYyH_b8ZqAUbEPTQ%=Nge}0pY_kRIhzko1 zSw1P{)v}s(EQj8##BE+5;`d5#>NvMKJ86{txZ+w99`Eb?8I`A-aZ5*}LEoiBPKdH) z%G5^aQS2AR{=n63WW_Ln!+oJ*gX87GUER{8!uAZyld)wS`d(Jjf#v-yNLEXo9{@xAx0`G)eOEy-Rg+tcos^_ z`V~pPo4y^!{FE2kMm>>Q?~XWfz39#3VvtpvIP^u(ncd_?JD>;0*R$;196aANA!DB7 zfd#J`Ubr!L-ajD50vnu9*zPmJu^A+`rWRA24yKN)?a00@sFt`C(k*`hG(|k+`qE-` zB2BzNgqR`1`R3E1W7@78xrVn?DkRFe+w;N*HNIt)iNu5TaZ2lTe4I7cA|eVCx9{2R zi`(WrZ^?T#0)H4}u3m#c}-=dYGJBnv(74Y7$8a~|a2&-3JL5&$Q z$Equ(pF0ya6;wK`!(PSd`#id{IK#*4C4Z~vLkXML!ZLL;b&=->TK^hA*pUoX#6~15 z<_&r?sJ?CFEk7%!-$Z)Q>ek?`u=@K4!$jmH!xaiA^4B!m=M7?@fHl!BRz2n^@qJY9 z3lm3~wHnw*Xq*z~?jmA774w_xoAL}7;l1t)u6kcPM&%%S0F2*MIWt4V%w;(&*=6li8T=cnkH_XQ_Dt&^o84U95@8>ZZ^OD(w-#a<7KfE5h3WNph_Ce@ zJA(is=i${h=!MP^DVq)5;)7+xlIVe9G;qng?9i(( zsh{mEH0oq3*$$emTzV`0d1Jv*aIf$lruw*8ea z3w7umV<=8@7k|OiY)s?hEfg&~T#Dl$4SIA?G9>_c3?-4`Zjg4Z$q3gX7mlTzvb!yo zb2=_}n_P6+5k;e zJ$om0X_m~ zYkc~!BMxxq6H&VIIg(@i`jIku2l7DiuD;=t9ec0xQ?CZ_vv%mVwAvS%Jev(No+JMA zR5G=-`>4gbosHrQyYi@%)J4{$7exT=70E&OYoR015kVY{kh(CB*3vzgPxx6N#X~=q z(JhX~W(9J2nl*M~)GZcHvWJStr75kbZKw(;6=*QzPibaU-wrIA31GG-fp39?PPHZ8 z-ux?f(j!(<^mzKTGnaK?3C2Ar)q=zlt<651i&(zUbVc`KacR8|WMH7XJ3SU&VqtXk zS}dao>h$RnI!emu{(^$p+bOKE^ZXk|9fk3{E6-&-?R~jAd4!%}!ExfWY zVHEYfWR{d}$Y+xYyyC3!r=ps2U+Sxd@3u29(W{|^cvYAWM>YNE(r7N>zPZzhzlk?g zhgn2Th)$_sVR+=&#k@;Mge5_+IqsWx_Mh4+1qB`bzB3^o2OsR zC(Sy4lo7^kbHH?NspqUGnqza5U63zxaor1d`>C$(EFJaptv89G>nTiM8p`LU&V*Lo zG*U|wd***k`n8F)sx^Odj+ey$ys z7l(dySub!s$Z0}Z3k@nV%&4SaYE^|r3oMR#O+dh%UWBL}WfadMC1lxjs95Fle`h5FiYI(j#46-cFEh`)TCp@lJ$IdaI zy~^>x0rptQJj)dc1Z|tfZp7|IJ8(yI{0-QkMUKJ7ZvAZrSh@t}HKBzAEnxONg5^ zUyka05vn#v`sRmIZ@9T^k-$d%XKa{FJ8!LGoc+xgIbK8Uq6a?zH)K#77 zs8Ke2%wwcM=V5sbGh*Qh>}rd-W!8*FlYnLH-HN58vw~%TY?mMwg4r{EM~Z?f7ZNM%mvl`|~kQY*@5b zvR4$b2L|_c$&c*Eajnbf3#<;_&$Xglzuq^lb=(h<#pdq{VqFt=Q}Rz`^%#5M;+Fpz z7v1KX@0^cWw%lIGSSSB7mOQ6NRToHMxIE**JPxw z&NuacWKVu=WMvh6a0q)>Gy#A_J33?Jco(Xk@&cSua=d4ux*%Pg3fjq8!w-)(@;hT} z@8@a{L-8ue)5`kD__*WTNy#^WkGmVjL&isr7eUJSqGd?!-EJT+;Oh{As~oS=?gxOG zu0B8oi$?=sVqj5w5C{yAf{95=K~W$FDG^edL|g(0k_3XmqF|VeI9P^sjsE@Nm8T`O zWbr5m8ADb0cX^~YIbJ6M0Ve|ldV70|c}s|4@s2<+3{m^<#LAdH9DS0lyjvF<=01P%xm z0|Ea;gxjOtZ$zXIBqE7F5~1vW3c`8f-M-p`vInBw(C)kGDH0v{k94Go(AE7(`kS~P zI=Z_9e*!?VPnJX?qe4nZld|WeUwIPvP*ISSCRs={lexKITmk> z#k$FVO~UOV_}={&3SBJ9*}?a(0>M%;yFcQmKu{S8&^IP~M*fw@_jK?7iOb%xd-#y% zKt>;rMS0qz@$&8ndp#q7DH`vw>kDA07!)9+?CI==A|>45QV?N)fhQ@;?gt>1NvQ1R z34I8%z~30`VT`hualqo;5d?Vz4(H}L;IcYH>>un_y`r+yBD# zj}Yx0@Go3@(fG&qzi|B{M0*GP3)fyW{;~ZpT>l8s-U0s!F4|uQ9a81u)8O$zt>?sH2!)T^lk^XoiYG*ueIw4UzUWB(^5a1 zi7;lrGeg6lbC-RfpPY2W8_}RE`nxWBO4R<4qWGrEkMsm-=L-2x%2>rz(_RFz!fhup zf~;xjDFC)}xnrg-(?or%kOOK()MIQ~88z21lSkTdj2WtmLbL{ISm*sl?#(4KXLIQt zgw8nETZaDmyay?G32{Lez8C8^6+A%+igGpu4|4;)=Mj;4KEu`0>kBJDTvX#DK_n!RRQ*wa<)}V6w7*NPG_Z2%7*?th& VLm%p6e+TJT=`R>8`Mvn}e*lAoE4Bat literal 0 HcmV?d00001 diff --git a/test/test_manifest.json b/test/test_manifest.json index f7a8f866df448..f053a4de24e91 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -5524,6 +5524,13 @@ "rounds": 1, "type": "eq" }, + { + "id": "issue20049", + "file": "pdfs/issue20049.pdf", + "md5": "1cdfde56be6b070e0c18aafc487d92ff", + "rounds": 1, + "type": "eq" + }, { "id": "issue8117", "file": "pdfs/issue8117.pdf", diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index e9961a16d7bc9..7d336c6d256b4 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -888,6 +888,20 @@ describe("api", function () { await loadingTask.destroy(); }); + it("should not prompt for password if `/AuthEvent /EFOpen`", async function () { + const loadingTask = getDocument(buildGetDocumentParams("issue20049.pdf")); + expect(loadingTask instanceof PDFDocumentLoadingTask).toEqual(true); + let called = false; + + loadingTask.onPassword = function () { + called = true; + }; + + const pdfDocument = await loadingTask.promise; + expect(pdfDocument.numPages).toBeGreaterThan(0); + expect(called).toBe(false); + }); + it("Doesn't iterate over all empty slots in the xref entries (bug 1980958)", async function () { if (isNodeJS) { pending("Worker is not supported in Node.js.");