Passwortgeschützte Dokumente laden mit dem jadice web toolkit

Gültig ab jadice web toolkit 5.7.1.0

Dieser Artikel beschreibt, wie passwortgeschützte PDF-Dokumente mit dem jadice web toolkit geladen werden können



Die vorgestellte Lösung ist im Showcase des jadice web toolkit unter http://webtoolkit.jadice.com/showcase/index.html#!EncryptedDocumentExample abrufbar. Das Passwort für das dort verwendete Dokument lautet J4d1c3




Anforderung

Ein PDF-Dokument, zu dessen Anzeige ein Passwort benötigt wird, soll im jadice web toolkit dargestellt werden. Dazu wird beim Öffnen des Dokuments ein Passwort in einem Benutzerdialog abgefragt. Anschließend wird versucht, das Dokument mit diesen Passwort zu öffnen.




Grundlegendes Prinzip

In diesem Abschnitt werden die grundlegenden Prinzipien behandelt, um passwortgeschützte PDF-Dokumente im jadice web toolkit öffnen zu können.


Lesen eines passwortgeschützten Dokuments mit dem DocumentDataProvider

Um ein passwortgeschütztes Dokument mit Mitteln der jadice documentplatform öffnen zu können, muss in den PDFStandardSecurityHandlerSettings des Readers ein CryptoMaterialProvider gesetzt werden. Das vom CryptoMaterialProviders bereitgestellte PasswordMaterial enthält dann einen String,  mit dem versucht wird, das Dokument zu öffnen.

Das Setzen dieser Einstellung kann beispielsweise in der der read()- beziehungsweise der recovery()Methode des verwendeten DocumentDataProvider erfolgen. Es ist allerdings auch möglich, in diese Methoden eine Reader-Instanz zu übergeben, für die die PDFStandardSecurityHandlerSettings bereits zuvor gesetzt wurden.

Das folgende Code-Beispiel enthält einen Auszug der Implementierung eines DocumentDataProvider, bei der stets versucht wird, das DEFAULT_PASSWORD zum Öffnen des Dokuments zu verwenden. In der Praxis wird man das Passwort eher über einen Benutzerdialog abfragen und in Source beziehungsweise Handle hinterlegen.

DocumentDataProvider mit Standard-Passwort
@Override
public void read(Reader reader, final S source) throws JadiceException, IOException {
  // Abfragen der PDFStandardSecurityHandlerSettings beim Reader.
  PDFStandardSecurityHandlerSettings pdfStandardSecurityHandlerSettings = reader.getSettings(
      PDFStandardSecurityHandlerSettings.class);
  // Definition des CryptoMaterialProviders
  CryptoMaterialProvider<PasswordMaterial> cryptoMaterialProvider = new CryptoMaterialProvider<PasswordMaterial>() {
    @Override
    // Der CryptoMaterialProvider wiederum stellt einem CryptoMaterialReceiver das
    // PasswordMaterial zur Verfügung.
    public void provide(CryptoMaterialReceiver<PasswordMaterial> receiver) {
      // Bereistellung des PasswordMaterial mit dem fixen Passwort DEFAULT_PASSWORD
      receiver.receive(new PasswordMaterial(DEFAULT_PASSWORD));
    }
  };
  // setzen des oben definierten cryptoMaterialProvider in den pdfStandardSecurityHandlerSettings
  // des Readers
  pdfStandardSecurityHandlerSettings.setCryptoMaterialProvider(cryptoMaterialProvider);
  // eigentlicher Lesevorgang im Reader
  Provider<InputStream, IOException> stream = getStream(source);
  reader.read(stream);
 }

@Override
public void recover(Reader reader, final SH handle) throws RecoverFailedException, JadiceException {
  // Abfragen der PDFStandardSecurityHandlerSettings beim Reader.
  PDFStandardSecurityHandlerSettings pdfStandardSecurityHandlerSettings = reader.getSettings(PDFStandardSecurityHandlerSettings.class);
  // Definition des CryptoMaterialProviders
  CryptoMaterialProvider<PasswordMaterial> cryptoMaterialProvider = new CryptoMaterialProvider<PasswordMaterial>() {
    // Der CryptoMaterialProvider wiederum stellt einem CryptoMaterialReceiver das
    // PasswordMaterial zur Verfügung.
    @Override
    public void provide(CryptoMaterialReceiver<PasswordMaterial> receiver) {
      // Bereistellung des PasswordMaterial mit dem fixen Passwort DEFAULT_PASSWORD
      receiver.receive(new PasswordMaterial(DEFAULT_PASSWORD));
    }
  };
  // setzen des oben definierten cryptoMaterialProvider in den pdfStandardSecurityHandlerSettings
  // des Readers
  pdfStandardSecurityHandlerSettings.setCryptoMaterialProvider(cryptoMaterialProvider);
  try {
    Provider<InputStream, IOException> stream = getRecoveryStream(handle);
    reader.read(stream);
  } catch (IOException e) {
    throw new RecoverFailedException("Can't recover " + handle, e);
  }
}

Weitere Implementierungsbeispiele eines CryptoMaterialProvider finden sich in den weiterführenden Links am Ende dieses Artikels. In der dort verlinkten Dokumentation wird auch erklärt, wie ein CryptoMaterialProvider mit mehreren Standard-Passwörtern implementiert werden kann.

Der Vergleich des Passworts aus dem PasswordMaterial mit dem im Dokument gesetzten Wert erfolgt auf Basis eine byte-Array, nicht als String-Vergleich! Deswegen kann es bei Verwendung unterschiedlicher Encodings zu Fehlern kommen, auch wenn offenbar der korrekte String im PasswordMaterial gesetzt wurde. Ein Workaround für dieses Problem ist ebenfalls am Ende dieses Artikels dargestellt.




Anfordern des Passworts beim Benutzer

Ist beim Laden eines PDF-Dokuments das benötigte Passwort serverseitig nicht bekannt wird dort zunächst eine PDFSecurityDocumentCreationException geworfen.
Diese kann anschließend in Form eines MimicryThrowable an den Client propagiert werden. Dort kann dann durch einen Benutzerdialog das Passwort abgefragt und an den Server übertragen werden.

Ist das Passwort korrekt, wird das Dokument geladen und angezeigt. Kann das Dokument mit dem eingegebenen Passwort nicht geöffnet werden, wird erneut eine PDFSecurityDocumentCreationException geworfen und als MimicryThrowable an den Client gesendet.

Der serverseitige Ladevorgang kann durch den Client abgebrochen werden, beispielsweise wenn das Passwort dem Benutzer nicht bekannt ist.

Da das Passwort bei jedem Ladevorgang des Dokuments auf dem Server vorhanden sein muss, sollte sichergestellt sein dass das Passwort dort lange genug vorgehalten wird. Damit wird sichergestellt. dass das Dokument im Recovery-Fall nach der Entfernung aus dem Cache ohne Nutzerinteraktion neu geladen werden kann.




Beispielhafte Implementierung im Showcase

Eine beispielhafte Implementierung zur Anzeige passwortgeschützter PDF-Dateien wird im Showcase unter https://webtoolkit.jadice.com/showcase/index.html#!EncryptedDocumentExample vorgestellt.

Dort findet in Example.java der Ladevorgang des passwortgeschützten Dokuments encr.pdf statt. Dabei wird dem Reader im Aufruf von Reader#complete(...) ein AsyncCallback-Objekt übergeben, dessen onFailure-Methode bei Fehlern beim Dokumentenladevorgang aufgerufen wird.

Innerhalb der onFailure-Methode wird geprüft, ob der Fehler durch ein falsches oder fehlendes Passwort verursacht wurde. Ist dies der Fall wird ein PasswortDialog angezeigt.

Der PasswortDialog hinterlegt das Passwort bei Bestätigung der Eingabe in einem Source-Handle, das anschließend an den Reader übergeben wird.

Weitere Information zum Lesen von Dokumenten mit Source-Handles finden sich in Kapitel zwei des Tutorials "Getting Started" unter 2 - Laden eines Dokuments

-
Passwort-Dialog aus PasswortDialog.java



Erläuterungen zur Implementierung

In diesem Abschnitt soll anhand von Code-Auszügen die Implementierung des Showcase erläutert werden.


Example.java

Nach einem Klick auf den Button "Load password protected PDF" wird die darauf registrierte Methode onClikck() aufgerufen.
Innerhalb dieser Methode wird der Reader konfiguriert mit dem das Dokument gelsen werden soll. Dabei wird auch der nötige AsyncCallback registriert. Gleichzeitig wird der PasswortDialog konfiguriert.

Auszüge aus Example.java
public void onClick(ClickEvent event) {
Reader r = new Reader();
 
//Definition des source-handles zum Laden des Dokuments. ClassPathSource erbt dabei von der PasswordContainingSource, in der ein String als Passwort hinterlegt werden kann.
ClassPathSource source = new ClassPathSource("com/levigo/jadice/web/demo/showcase/client/examples/document/encr.pdf");

// Hinzufügen des Source-Handles zum reader
r.append(source);

// Definition des Passwortdialogs mit der PageView auf der dieser gegebenenfalls angezeigt werden soll und des source-Handle in dem das Passwort hinterlegt werden soll
PasswordDialog passwordDialog = new PasswordDialog(viewer.getPageView(), source);


// Definition des AsyncCallback der gegebenenfalls die Anzeige des Passwortdialogs steuert.
AsyncCallback<Document> asyncCallback = new AsyncCallback<Document>() {


// Tritt kein Fehler auf weil das Passwort korrekt eingegeben wurde oder weil kein Passwort benötigt wird wird das Dokument als Inahlt der PageView gesetzt.
  @Override
  public void onSuccess(Document result) {
    viewer.getPageView().setDocument(result);
  }


//Ist ein Fehler aufgetreten weil das Passwort nicht vorhanden oder falsch war soll der Passwortdialog angezeigt werden. 
  @Override
  public void onFailure(Throwable caught) {
    if (passwordDialog != null && //
        caught instanceof MimicryThrowable && //
        ((MimicryThrowable) caught).getClassName().equals(PDFSecurityDocumentCreationException.class.getName())) {
    passwordDialog.setAsyncCallback(this);
    passwordDialog.show();
    }
  }
};

// Abschließen des Readers mit Registrierung des Callbacks
r.complete(asyncCallback);
}


PasswordDialog.java

In PasswordDialog.java wird neben den funktionalen Elementen des angezeigten Dialogs auch wichtige Funktionalität implementiert. Diese soll in diesem Abschnitt weiter erläutert werden.

Auszüge aus PasswordDialog.java
// Die execute()-Methode wird nach Aktivierung des confirm-Buttons aufgerufen. 
public void execute(String password) {
  //Setzen des vom Benutzer eingetragenen Passworts im source-Handle
  source.setPassword(password);
  //Anschließend wird ein neuer Reader erzeugt, an den source-Handle mit Passwort angehängt wird. 
  Reader reader = new Reader();
  reader.append(source);
  // Abschließen des Readers mit Registrierung desselben Callbacks der bereits zur Anzeige des PasswordDialog geführt hat.
  reader.complete(callback);
}
 
// Um den Ladevorgang abzubrechen wird die onFailure-Methode des AsnycCallback mit einem Throwable aufgerufen das kein Mimicry-Throwable ist. Würde hier ein Mimicry-Throwable übergeben würde der PasswordDialog erneut angezeigt.
public void abort() {
  callback.onFailure(new Throwable("PasswordDialog aborted. Missing password for encrypted document."));
}




Weiterführende Links

  • Grundlagen zur Verschlüsselung und zum Schutz von PDF-Dokumenten werden in der Dokumentation der jadice documentplatform im Kapitel PDF: Verschlüsselung behandelt.
  • Konkrete Beispiele zur Verwendung der PDF-Security API in der jadice documentplatform sind in der Entwicklerdokumentation im Kapitel Beispiel zur Verwendung der PDF Security API erklärt.
  • Im Knowledge-Base Artikel Verschlüsselte PDFs sind die Grundlagen des Schutzes von PDF-Dateien mit Passwörtern erklärt.




Bekannte Probleme

Dokumente werden trotz korrektem Passwort nicht geladen

Abhängig davon, welches Encoding für das Passwort eines Dokuments verwendet wurde, kann es vorkommen, dass ein Dokument trotz korrekt eingegebenem Passwort nicht angezeigt werden kann. Dies liegt daran, dass das als String übergebene Passwort im PasswordMaterial der jadice documentplatform als byte-Array weiterverwendet werden muss.
Ein Workaround zu diesem Problem ist, in der read-Methode des DocumentDataProvider eine angepasste Implementierung des PasswordMaterial zu verwenden, bei der das zu verwendende Encoding definiert werden kann. Ist das Encoding nicht vorab bekannt, können verschiedene Encodings nacheinander ausprobiert werden.

Eine Implementierung einer solchen alternativen PasswordMaterial-Implementierung könnte folgendermaßen aussehen:

CustomCharsetPasswordMaterial.java
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;

/**
 * A custom implementation of {@link PasswordMaterial} which uses a custom charset to create the
 * password bytes in {@link #getPasswordBytes()}.
 */
public class CustomCharsetPasswordMaterial extends PasswordMaterial {

  private final Charset charset;

  /**
   * constructs a new instance of {@link CustomCharsetPasswordMaterial} which will provide the given
   * password encoded with the given charset.
   * 
   * @param password password to be provided
   * @param charset the charset to use
   */
  public CustomCharsetPasswordMaterial(String password, Charset charset) {
    super(password);
    this.charset = charset;
  }

  /**
   * @return a byte-array representation of the password held by this instance of
   *         {@link CustomCharsetPasswordMaterial}
   */
  @Override
  public byte[] getPasswordBytes() {
    return getPassword().getBytes(charset);
  }
}