مقاله فنی

نمی‌توانید بی‌سروصدا یک PDF رمزنگاری‌شده را در Delphi وصله کنید

یک فاکتور PDF که از پیش رمزنگاری AES-256 حمل می‌کند را بردارید و از PDFium Component برای Delphi و C++Builder (PDFiumPas) بخواهید آن را برای نگه‌داری آرشیوی PDF/A مهر بزند، یا آن را با PAdES امضا کند، از طریق یک به‌روزرسانی افزایشی به‌جای یک بازنویسی کامل. کتابخانه با وصله‌کردن مستقیم بایت‌های رمزنگاری‌شده به آنجا نمی‌رسد: شش تزریق‌کننده‌ی نشانگر مطابقتش یک ورودی موجود /Encrypt را تشخیص می‌دهند و منبع را بایت‌به‌بایت بدون تغییر عبور می‌دهند، و امضاکننده‌ی PAdES آن به‌جای منتشرکردن امضایی که هیچ اعتبارسنجی آن را نمی‌پذیرد یک استثنا raise می‌کند

این سؤال متفاوتی از ممیزی یک PDFای که خودتان نساخته‌اید برای ریسک پنهان است، که خودش یک تمرین فقط-خواندنی است. این مقاله درباره‌ی سمت نوشتن همان مرز اعتماد است: کد خودتان مجاز است چه کاری با فایلی انجام دهد که بایت‌هایش از پیش پشت رمز عبور کس دیگری قفل شده، همان لحظه‌ای که آن کد تلاش می‌کند بعداً چیزی به آن اضافه کند

ISO 32000-1 چه چیزی را وقتی یک PDF رمزنگاری‌شده را به‌روزرسانی می‌کنید الزامی می‌کند؟

ISO 32000-1 §7.5.6 الزام می‌کند که trailer یک به‌روزرسانی افزایشی هر ورودی از trailer قبلی را جز /Prev تکرار کند، و جدول ۱۵ /Encrypt را در میان ورودی‌هایی که یک trailer می‌تواند حمل کند فهرست می‌کند. آن را از trailer جدید حذف کنید و یک خواننده‌ی مطابق هیچ دلیلی برای شک‌کردن به آن حذف ندارد: جدیدترین trailer معتبر است، پس خواننده‌ای که هیچ /Encryptای آنجا پیدا نمی‌کند تصمیم می‌گیرد کل فایل رمزنگاری‌نشده است و تلاش می‌کند بدنه‌ی قدیمی‌تر و همچنان رمزنگاری‌شده را به‌عنوان بایت ساده تجزیه کند. /Encrypt را در trailer جدید نگه دارید اما اشیاء خودِ به‌روزرسانی را به‌عنوان متن‌ساده بنویسید، و شکست فقط یک قدم دیرتر جابه‌جا می‌شود: خواننده به‌درستی رمزنگاری را تشخیص می‌دهد، هر شیئی که لمس می‌کند را از طریق رمز فایل عبور می‌دهد، از جمله شیءهای جدید که هرگز اصلاً رمزنگاری نشده بودند، و برای محتوایی که پیش از اینکه رمزگشایی آن را لمس کند کاملاً خوانا بود، نویز پس می‌گیرد. هرکدام از این اشتباه‌ها فایلی تولید می‌کند که در سطح بایت مثل یک به‌روزرسانی افزایشی معمولی و درست‌شکل به‌نظر می‌رسد، درست تا زمانی که یک خواننده‌ی مطابق آن را باز کند

شش تزریق‌کننده‌ی نشانگر، یک دروازه‌ی رمزنگاری در v2.14.2

PDFiumPas شش تزریق‌کننده‌ی نشانگر در سطح بایت عرضه می‌کند، یکی برای هر زیرمجموعه‌ی ISO از PDF که می‌تواند برچسب بزند: PDF/A (‏ISO 19005)، PDF/X (‏ISO 15930)، PDF/UA (‏ISO 14289-1)، PDF/E-1 (‏ISO 24517-1)، PDF/R-1 (‏ISO 23504-1)، و PDF/VT-1 (‏ISO 16612-2). هرکدام بایت‌هایی را که خودِ FPDF_SaveAsCopy در PDFium از پیش نوشته می‌گیرد و یک به‌روزرسانی افزایشی دوم و کوچک‌تر رویش لایه‌بندی می‌کند: یک جریان فراداده‌ی XMP جدید، یک ویرایش دیکشنری کاتالوگ که به آن اشاره می‌کند، و برای زیرمجموعه‌های چاپ-محور یک OutputIntent و پروفایل ICC. تا v2.14.2، هر یک از InjectPdfAMarkers، InjectPdfXMarkers، InjectPdfUaMarkers، InjectPdfEMarkers، InjectPdfRMarkers، و InjectPdfVTMarkers ابتدا trailer منبع را می‌خواند، و اگر یک ورودی موجود /Encrypt گزارش دهد، منبع را بدون تغییر به جریان مقصد کپی می‌کند و بلافاصله برمی‌گردد. هیچ XMP، هیچ OutputIntent، هیچ ویرایش کاتالوگ — فراخواننده فایل اصلی را بایت‌به‌بایت پس می‌گیرد

var
  Src, Dst: TFileStream;
  Opts: TPdfXSaveOptions;
begin
  Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
  Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
  try
    Opts.Conformance := pxc4;
    InjectPdfXMarkers(Src, Dst, Opts);
    // pdfx-attempt.pdf is byte-identical to the source: still encrypted,
    // no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
    // nothing was corrupted either
  finally
    Dst.Free;
    Src.Free;
  end;
end;

مجاز-به-رمزنگاری‌شدن همان چیزی نیست که امن-برای-تزریق

هم PDF/E-1 و هم PDF/R-1 صراحتاً اجازه می‌دهند سند میزبانشان در سطح مشخصات رمزنگاری‌شده باشد، که شبیه یک معافیت به‌نظر می‌رسد تا زمانی که به آنچه واقعاً باید روی دیسک اتفاق بیفتد نگاه کنید. ISO 24517-1 §6.3 رمزنگاری را برای PDF/E-1 مجاز می‌کند، و ISO 23504-1 §6.2.3 آن را برای PDF/R-1 مشروط به اینکه هدر %PDF-2.0 را اعلان کند مجاز می‌کند. هیچ‌کدام از این بندها چیزی درباره‌ی اینکه آیا یک پس‌پردازشگر سطح-بایت می‌تواند ایمن یک شیء متن‌ساده را به آن کانتینر رمزنگاری‌شده اضافه کند نمی‌گویند، و نمی‌تواند، به همان دلایل §7.5.6 که برای هر زیرمجموعه‌ی دیگر اعمال می‌شود. اعتبارسنج‌های مطابقت خودِ PDFiumPas برای این دو پروفایل، ValidatePdfECompliance و ValidatePdfRCompliance، حضور /Encrypt را عمداً بدون پرچم‌گذاری آن به‌عنوان یک نقص ثبت می‌کنند، که برای یک اعتبارسنج فقط-خواندنی که هرگز یک بایت نمی‌نویسد درست است. این هم یک الگویی است که به‌راحتی می‌توان از رویش گذشت و فرض کرد تزریق‌کننده‌ی همتا به یک نگهبان جداگانه نیاز ندارد، درحالی‌که تزریق‌کننده همان یک تابعی است در جفت که واقعاً باید امتناع کند

آیا SaveAsPdfX بی‌سروصدا سند شما را رمزگشایی می‌کند؟

بله، هر زمانی که از طریق متدهای راحتی عمومی بروید به‌جای فراخوانی مستقیم یک تزریق‌کننده. هر یک از TPdf.SaveAsPdfA، SaveAsPdfX، SaveAsPdfUa، SaveAsPdfE، SaveAsPdfR، و SaveAsPdfVT سند فعلی را با SaveAs(Tmp, saRemoveSecurity) به یک جریان موقت رندر می‌کند پیش از سپردن آن بایت‌ها به تزریق‌کننده‌ی متناظرش. saRemoveSecurity به پرچم خودِ FPDF_REMOVE_SECURITY در PDFium نگاشت می‌شود، پس کپی موقتی که تزریق‌کننده دریافت می‌کند در وهله‌ی اول اصلاً رمزنگاری نشده بود، و نگهبان /Encrypt تزریق‌کننده هرگز دلیلی برای ماشه‌کشیدن ندارد. خروجی نشانگرهای PDF/A، PDF/X، PDF/UA، PDF/E-1، PDF/R-1، یا PDF/VT-1 شما را حمل می‌کند، اما دیگر با هر رمز عبوری که منبع را باز کرد محافظت نمی‌شود

آن معامله نامرئی است تا زمانی که کسی پایین‌دست کپی آرشیوی «محافظت‌شده» را بدون رمز عبور باز کند و متوجه شود صرفاً کار می‌کند. رفع اشکال یک فراخوانی متد متفاوت نیست؛ PDFiumPas هیچ همتای saAddSecurityای برای جفت‌شدن با saRemoveSecurity ندارد، چون موتور زیرین PDFium هرگز برای نوشتن رمزنگاری جدید ساخته نشده، فقط برای حذف آن. اگر هر دو ویژگی برای یک فایل اهمیت دارند، رمزنگاری باید یک گام جداگانه باشد که خودتان مالکش هستید، اعمال‌شده پس از نشانگرهای مطابقت، نه تا‌شده در همان فراخوانی SaveAsPdfA

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';           // needed to open the source at all
    Pdf.FileName := 'signed-encrypted-invoice.pdf';
    Pdf.LoadDocument;
    Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
    // invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
    // ran first inside SaveAsPdfA: the output opens without a password
  finally
    Pdf.Free;
  end;
end;

وقتی یک PDF رمزنگاری‌شده را با PAdES امضا کنید چه اتفاقی می‌افتد؟

PDFiumPas کاملاً امتناع می‌کند، به‌جای اینکه بی‌سروصدا درخواست را رها کند همان‌طور که یک تزریق‌کننده‌ی نشانگر انجام می‌دهد. هم TPdf.SignPades و هم SignPadesToStream از طریق یک SignPadesBytes داخلی مسیر می‌یابند، و اولین کاری که پس از تجزیه‌ی trailer منبع انجام می‌دهد این است که /Encrypt را بررسی کند. اگر آن ورودی حاضر باشد، EPadesCrypto را با پیام «SignPadesBytes: the source document is encrypted; remove encryption before signing» raise می‌کند به‌جای اینکه هرگونه بیشتر پیش برود. InjectPadesDssMarkers، تابعی که گواهی‌ها، پاسخ‌های OCSP، و CRLها را برای اعتبارسنجی بلندمدت جاسازی می‌کند، همان بررسی را به همان دلیل اعمال می‌کند، با پیام خودش: «InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material»

استدلال اینجا سخت‌گیرانه‌تر از عبور-ساده‌ی تزریق‌کننده‌های نشانگر است، و عمداً چنین است. یک عبور خاموش برای یک مهر PDF/A ایمن است چون رد‌کردنش شما را با همان PDF معتبری که با آن شروع کردید رها می‌کند، فقط بدون‌برچسب. امضاکردن نمی‌تواند به همین آرامی شکست بخورد: امضایی که بی‌سروصدا هرگز اضافه نشده، برای هر کد فراخواننده‌ای که فقط یک نتیجه‌ی بولی را بررسی می‌کند، دقیقاً شبیه امضایی است که با موفقیت اضافه شده. EPadesCrypto از کلاس معمولی Exception ارث می‌برد، پس گرفتنش یک exception handling معمولی است، نه یک قرارداد جریان-کنترل خاص که باید یاد بگیرید

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';
    Pdf.FileName := 'encrypted-contract.pdf';
    Pdf.LoadDocument;
    try
      Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
    except
      on E: EPadesCrypto do
        // E.Message: 'SignPadesBytes: the source document is encrypted;
        // remove encryption before signing'
        raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
    end;
  finally
    Pdf.Free;
  end;
end;

ترتیب‌بندی مهرهای مطابقت، امضاها، و رمزنگاری

رفع اشکال عملی ترتیب‌بندی است، نه یک کتابخانه‌ی متفاوت. ابتدا نشانگرهای PDF/A، PDF/X، PDF/UA، PDF/E-1، PDF/R-1، یا PDF/VT-1 را اعمال کنید، سپس هر امضای PAdES را اضافه کنید، و فقط پس از آن هر گامی در خط لوله‌ی شما که واقعاً مالک رمزنگاری است اجرا کنید، خواه آن یک نویسنده‌ی PDF اختصاصی باشد، یک دستگاه امضاکننده، یا پیاده‌سازی AES خودتان. لایه‌ی به‌روزرسانی افزایشی PDFiumPas به‌طور طبیعی در میانه‌ی آن توالی جا می‌گیرد، اشیاء کوچک و هدفمندی را روی فایلی که در غیر این‌صورت تمام شده پیوست می‌کند، و رمزنگاری دقیقاً به این دلیل در انتها تعلق دارد که تنها عملیاتی در زنجیره است که خودِ PDFiumPas نمی‌تواند انجام دهد یا معکوس کند

هیچ‌کدام از این‌ها اینکه PDFiumPas چطور trailer و داده‌ی cross-reference را می‌خواند که هر به‌روزرسانی افزایشی به آن‌ها بستگی دارد تغییر نمی‌دهد، که خودش منبع ظرافتی است به‌محض اینکه جریان‌های xref به تصویر می‌آیند؛ اعتبارسنجی جریان‌های شیء و xref یک PDF پوشش می‌دهد که همان مسیر خواندن trailer چطور ساختارهای فشرده‌ی PDF 1.5+ را مدیریت می‌کند. و به‌محض اینکه یک سند آماده‌ی چیزی قوی‌تر از یک مهر مطابقت باشد، امضای یک PDF با یک امضای PAdES B-B در Delphi جایی است که SignPades دقیقاً از همان نقطه‌ای که این مقاله رها می‌کند تحویل می‌گیرد

تزریق‌کننده‌های نشانگر و متدهای SignPades که در اینجا توصیف شد بخشی از کامپوننت PDFium برای Delphi و C++Builder عرضه می‌شوند، در کنار رندر و بازرسی فقط-خواندنی‌ای که خودِ PDFium بومی ارائه می‌دهد