From 9082a435f5822ed4a646a14ba9ba186c9f6a5407 Mon Sep 17 00:00:00 2001 From: tegaryas Date: Fri, 5 Jun 2026 15:08:31 +0700 Subject: [PATCH 1/4] fix: fix false-positive password detection for PDFs whose /ID starts with UTF-16 BOM bytes --- .../implementation/primitives/pdf_string.dart | 8 +++---- .../lib/src/pdfviewer.dart | 21 ++++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart b/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart index ee428257..92ca571e 100644 --- a/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart +++ b/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart @@ -21,10 +21,10 @@ class PdfString implements IPdfPrimitive { if (data![0] == 0xfe && data![1] == 0xff) { this.value = decodeBigEndian(data, 2, data!.length - 2); isHex = false; - data = []; - for (int i = 0; i < this.value!.length; i++) { - data!.add(this.value!.codeUnitAt(i).toUnsigned(8)); - } + // data keeps the original hex-decoded bytes — hex strings are binary, + // not text, even when they happen to start with the UTF-16 BOM bytes. + // Overwriting data with low-bytes of the decoded chars corrupts binary + // fields like /ID (used in encryption key derivation). } else { this.value = byteToString(data!); } diff --git a/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart b/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart index f4ba520c..10f305d2 100644 --- a/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart +++ b/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart @@ -2314,7 +2314,7 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { } } final int pageCount = await _plugin.initializePdfRenderer( - _renderDigitalSignatures() ?? _pdfBytes, + (_document != null ? _renderDigitalSignatures() : null) ?? _pdfBytes, _password, ); _pdfViewerController._pageCount = pageCount; @@ -2344,7 +2344,9 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { ), ); } - } else if (errorMessage.contains('Cannot open an encrypted document.')) { + } else if (errorMessage.contains('Cannot open an encrypted document.') || + errorMessage.contains('PASSWORD_ERROR') || + errorMessage.contains('PDF_UNLOCK_FAILED')) { if (!_isPasswordUsed) { try { _decryptedProtectedDocument(_pdfBytes, widget.password); @@ -3444,7 +3446,20 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { /// Get the file of the Pdf. Future _getPdfFile(Uint8List? value) async { if (value != null) { - return PdfDocument(inputBytes: value, password: _password); + try { + return PdfDocument(inputBytes: value, password: _password); + } on ArgumentError catch (e) { + // When no password is provided and the Dart PDF auth fails (false positive — + // the PDF has an Encrypt dict with empty user password but our authentication + // algorithm can't verify it), return null so the native renderer can handle it. + // Truly password-protected PDFs will fail in the native renderer instead and + // show the password dialog via the PASSWORD_ERROR / PDF_UNLOCK_FAILED path. + if ((_password == null || _password!.isEmpty) && + e.toString().contains('Cannot open an encrypted document.')) { + return null; + } + rethrow; + } } return null; } From e820c7d842c2001cfee0e8527df7a31dbaeeba18 Mon Sep 17 00:00:00 2001 From: tegaryas Date: Fri, 5 Jun 2026 17:52:15 +0700 Subject: [PATCH 2/4] fix: fix password pdf can't view --- .../implementation/primitives/pdf_string.dart | 7 ++- .../lib/src/pdfviewer.dart | 54 ++++++++++--------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart b/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart index 92ca571e..d8b26c51 100644 --- a/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart +++ b/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart @@ -21,10 +21,9 @@ class PdfString implements IPdfPrimitive { if (data![0] == 0xfe && data![1] == 0xff) { this.value = decodeBigEndian(data, 2, data!.length - 2); isHex = false; - // data keeps the original hex-decoded bytes — hex strings are binary, - // not text, even when they happen to start with the UTF-16 BOM bytes. - // Overwriting data with low-bytes of the decoded chars corrupts binary - // fields like /ID (used in encryption key derivation). + for (int i = 0; i < this.value!.length; i++) { + data!.add(this.value!.codeUnitAt(i).toUnsigned(8)); + } } else { this.value = byteToString(data!); } diff --git a/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart b/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart index 10f305d2..304a59d4 100644 --- a/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart +++ b/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart @@ -2313,8 +2313,17 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { _performTextExtraction(); } } + + Uint8List? signatureBytes; + if (_document != null) { + try { + signatureBytes = _renderDigitalSignatures(); + } catch (_) { + // Digital-signature flattening is best-effort; fall back to raw bytes. + } + } final int pageCount = await _plugin.initializePdfRenderer( - (_document != null ? _renderDigitalSignatures() : null) ?? _pdfBytes, + signatureBytes ?? _pdfBytes, _password, ); _pdfViewerController._pageCount = pageCount; @@ -2344,9 +2353,7 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { ), ); } - } else if (errorMessage.contains('Cannot open an encrypted document.') || - errorMessage.contains('PASSWORD_ERROR') || - errorMessage.contains('PDF_UNLOCK_FAILED')) { + } else if (errorMessage.contains('Cannot open an encrypted document.')) { if (!_isPasswordUsed) { try { _decryptedProtectedDocument(_pdfBytes, widget.password); @@ -2354,12 +2361,17 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { } catch (e) { if (widget.onDocumentLoadFailed != null) { if (widget.password == '' || widget.password == null) { - widget.onDocumentLoadFailed!( - PdfDocumentLoadFailedDetails( - 'Empty Password Error', - 'The provided `password` property is empty so unable to load the encrypted document.', - ), - ); + // Only report "no password" when the dialog won't be shown. + // When canShowPasswordDialog is true the UI handles it, + // so firing onDocumentLoadFailed here is confusing noise. + if (!widget.canShowPasswordDialog) { + widget.onDocumentLoadFailed!( + PdfDocumentLoadFailedDetails( + 'Empty Password Error', + 'The provided `password` property is empty so unable to load the encrypted document.', + ), + ); + } } else { widget.onDocumentLoadFailed!( PdfDocumentLoadFailedDetails( @@ -2394,6 +2406,8 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { ); } } else { + // ignore: avoid_print + print('[SfPdfViewer] unhandled error: $e'); if (widget.onDocumentLoadFailed != null) { widget.onDocumentLoadFailed!( PdfDocumentLoadFailedDetails( @@ -2411,7 +2425,10 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { /// Render and flatten digital signatures in the PDF document. Uint8List? _renderDigitalSignatures() { if (_document!.form.fields.count != 0) { - final PdfDocument pdfDocument = PdfDocument(inputBytes: _pdfBytes); + final PdfDocument pdfDocument = PdfDocument( + inputBytes: _pdfBytes, + password: _password, + ); bool isDigitalSignature = false; Uint8List? digitalSignatureBytes; @@ -3446,20 +3463,7 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { /// Get the file of the Pdf. Future _getPdfFile(Uint8List? value) async { if (value != null) { - try { - return PdfDocument(inputBytes: value, password: _password); - } on ArgumentError catch (e) { - // When no password is provided and the Dart PDF auth fails (false positive — - // the PDF has an Encrypt dict with empty user password but our authentication - // algorithm can't verify it), return null so the native renderer can handle it. - // Truly password-protected PDFs will fail in the native renderer instead and - // show the password dialog via the PASSWORD_ERROR / PDF_UNLOCK_FAILED path. - if ((_password == null || _password!.isEmpty) && - e.toString().contains('Cannot open an encrypted document.')) { - return null; - } - rethrow; - } + return PdfDocument(inputBytes: value, password: _password); } return null; } From c074a477ef913d7b13997dabf51288a94900e23f Mon Sep 17 00:00:00 2001 From: tegaryas Date: Fri, 5 Jun 2026 17:54:29 +0700 Subject: [PATCH 3/4] chore: remove print --- packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart b/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart index 304a59d4..b2c64879 100644 --- a/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart +++ b/packages/syncfusion_flutter_pdfviewer/lib/src/pdfviewer.dart @@ -2406,8 +2406,6 @@ class SfPdfViewerState extends State with WidgetsBindingObserver { ); } } else { - // ignore: avoid_print - print('[SfPdfViewer] unhandled error: $e'); if (widget.onDocumentLoadFailed != null) { widget.onDocumentLoadFailed!( PdfDocumentLoadFailedDetails( From 42d658caddd453b32c7d8a631d631f99f4d23764 Mon Sep 17 00:00:00 2001 From: tegaryas Date: Fri, 5 Jun 2026 18:27:37 +0700 Subject: [PATCH 4/4] fix: add condition for data length = 16 --- .../lib/src/pdf/implementation/primitives/pdf_string.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart b/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart index d8b26c51..54821bc8 100644 --- a/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart +++ b/packages/syncfusion_flutter_pdf/lib/src/pdf/implementation/primitives/pdf_string.dart @@ -21,8 +21,12 @@ class PdfString implements IPdfPrimitive { if (data![0] == 0xfe && data![1] == 0xff) { this.value = decodeBigEndian(data, 2, data!.length - 2); isHex = false; - for (int i = 0; i < this.value!.length; i++) { - data!.add(this.value!.codeUnitAt(i).toUnsigned(8)); + // 16-byte fields (e.g. /ID) are raw binary identifiers — never + // append decoded character bytes to them. + if (data!.length != 16) { + for (int i = 0; i < this.value!.length; i++) { + data!.add(this.value!.codeUnitAt(i).toUnsigned(8)); + } } } else { this.value = byteToString(data!);