Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Verschlüsselung von Streams in Java

Wer bereits die Funktionsweise von asymmetrischer und symmetrischer Verschlüsselung versteht, kann die Grundlagen gerne überspringen.

Grundlagen

Pretty-Good-Privacy (PGP) ist ein Verfahren zur sicheren Verschlüsselung von Daten und E-Mails. Dabei kombiniert dieser sowohl symmetrische als auch asymmetrische Verschlüsselungsverfahren.

Symmetrische Verschlüsselung bedeutet, dass derselbe Schlüssel sowohl zum Verschlüsseln als auch zum Entschlüsseln verwendet Wird. Dabei kann man eine Nachricht mit einem Passwort verschlüsseln und mit demselben Passwort wieder entschlüsseln. Dieses Verfahren ist relativ flott, da keine komplexeren Rechnungen erfolgen.

Asymmetrische Verschlüsselung hingegen verwendet zwei Schlüssel: einen öffentlichen und einen privaten Schlüssel. Eine Nachricht, die mit dem öffentlichen Schlüssel verschlüsselt wurde, kann nur mit dem dazugehörigen privaten Schlüssel entschlüsselt werden. Wenn man an Person X Daten senden möchte, wird dann entsprechend der öffentliche Schlüssel von X verwendet. Darauf kann nur Person X die Nachricht mit ihrem Privatschlüssel entschlüsseln. Dies ermöglicht die sichere Datenübertragung, ohne dass der private Schlüssel preisgegeben werden muss. Dabei werden auch Sicherheitszertifikate verwendet, um zu vergewissern, dass es sich auch wirklich um Person X handelt. (Vermeidung der Man in the Middle-Attack)

Asymmetrische Verschlüsselung ist allerdings weitaus langsamer, als die symmetrische Verschlüsselung, da hier komplexe mathematische Formeln verwendet werden (modulare Potenzierung unter der Eulerschen Phi-Funktion). Daher wird in PGP über ein asymmetrisches Verfahren ein Passwort ausgetauscht und dieses dann zur symmetrischen Verschlüsselung verwendet. [1]

Implementierung mit BouncyCastle (BC)

Hinzufügen der nötigen BouncyCastle-Dependencies:

org.bouncycastle
   bcprov-jdk15on
   ${bouncycastle.version}org.bouncycastle
   bcpg-jdk15on
   ${bouncycastle.version}

Verschlüsselung

Anlegen eines OutputStreamEncryptors für das Verschlüsseln:

public OutputStreamEncrypter(PGPPublicKey publicKey) {
   Security.addProvider(new BouncyCastleProvider());
   encryptedDataGenerator = getEncryptedDataGenerator(publicKey);
   compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_COMPRESSION);
   literalDataGenerator = new PGPLiteralDataGenerator();
}

Hier wird BouncyCastle als Provider festgelegt und die nötigen Generatoren für Verschlüsselung, Kompression und PGP-Formatierung geöffnet.

PGPLiteralDataGenerator dient als Wrapper für den Body und ist für PGP notwendig. [2]

PGPCompressedDataGenerator dient der Komprimierung des Bodys. BEST_COMPRESSION (9) komprimiert im Schnitt um Faktor 20. (10 GB → ~0.5 GB)

PGPEncryptedDataGenerator dient der Verschlüsselung und wird mit dem öffentlichen Schlüssel wie folgt erstellt:

private PGPEncryptedDataGenerator getEncryptedDataGenerator(PGPPublicKey publicKey) {
   JcePGPDataEncryptorBuilder encryptorBuilder = new JcePGPDataEncryptorBuilder(PGPEncryptedData.CAST5)
         .setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider("BC");
   PGPEncryptedDataGenerator dataGenerator = new PGPEncryptedDataGenerator(encryptorBuilder);
   dataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider("BC"));
   return dataGenerator;
}

Hierbei wird CAST5 als symmetrischer Algorithmus definiert und nach dem asymmetrischen Passwort-Austausch verwendet. setWithIntegrityPacket fügt den einzelnen verschlüsselten Blöcken noch ein Integritätspacket hinzu, um Unverfälschtheit zu gewährleisten. [3]

Um nun den verschlüsselten Outputstream zu erhalten, wird folgende Streamverkettung (stream chaining) verwendet:

public OutputStream getEncryptedStream(final OutputStream toEncryptAndCompress) throws PGPException, IOException {
   // 4. Binary to Base64 + Header
   armoredOut = new ArmoredOutputStream(toEncryptAndCompress);
   // 3. Encrypts the file
   encryptedOut = encryptedDataGenerator.open(armoredOut, new byte[BYTE_BLOCK_16_BIT]);
   // 2. Compression of the file
   compressedOut = compressedDataGenerator.open(encryptedOut);
   // 1. Creates LiteralDataStream
   literalOut = literalDataGenerator.open(compressedOut, PGPLiteralData.BINARY, "", new Date(), new byte[BYTE_BLOCK_16_BIT]);
   return literalOut;
}

Bei der Verschachtelung von Streams wird die äußerste Schicht zuerst angewendet, daher ist die Nummerierung zu beachten.

Klartext → LiteralData (PGP-Format) → Kompression → Verschlüsselung → Base64-Encodierung

Hierbei werden einzelne Blöcke des reingegebenen BufferedOutputStreams toEncryptAndCompress komprimiert, verschlüsselt und kodiert.

Somit kann man nun mit folgenden Zeilen Code eine verschlüsselte Datei anlegen:

public String encryptAndCompress(String filePath) throws IOException {
   try (final InputStream fileIn = new FileInputStream(filePath);
         final InputStream bufferedFileIn = new BufferedInputStream(fileIn, BYTE_BLOCK_16_BIT)){
      final File file = new File(tempDir + File.separator + "encrypted.gpg");
      final OutputStream fileOut = new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE);
      final OutputStream encryptedStream = getEncryptedStream(fileOut);

      int bytesRead;
      byte[] buffer = new byte[BYTE_BLOCK_16_BIT];
      while ((bytesRead = bufferedFileIn.read(buffer)) != -1) {
         encryptedStream.write(buffer, 0, bytesRead);
      }
      close();
      fileOut.close();
   }
}

Damit die Generatoren auch wieder geschlossen werden, implementiert OutputStreamEncrypter das Interface „Autocloseable“ und der close sieht dann wie folgt aus:

@Override
public void close() {
   Utils.closeQuietly(lOut, comOut, encOut);
   closeGenerators();
   Utils.closeQuietly(armoredOutputStream);
}

public void closeGenerators() {
   closeEncryptedDataGeneratorQuiently();
   closeCompressedDataGeneratorQuiently();
   closeLiteralDataGeneratorQuiently();
}

Die Reihenfolge, in welcher die Streams und Generatoren geschlossen werden, ist irreversibel für das Funktionieren der Anwendung.

Entschlüsselung

Anlegen eines InputStreamDecryptors für das Entschlüsseln:

public InputStreamDecrypter(PGPKeyPair keyPair_) {
   Security.addProvider(new BouncyCastleProvider());
   keyPair = keyPair_;
}

Hierbei wird wieder BouncyCastle als Provider festgelegt, allerdings werden hier keine Generators wie oben gebraucht.

Um seinen entschlüsselten Stream zu erhalten, wird folgende Methode verwendet:

private InputStream getDecryptedStream(InputStream encryptedStream) throws IOException, PGPException {
 // 5. Entfernen der ASCII-Armor
   unarmoredIn = PGPUtil.getDecoderStream(encryptedStream);
 // 6. Entschlüsselten Stream mittels Privatschlüssel erhalten
   decryptedIn = decryptData(unarmoredIn);
 // 7. Dekomprimierten Stream erhalten   
   decompressedIn = decompressData(decryptedIn);
 // 8. Original Bytestream erhalten
   deliteralIn = readLiteral(decompressedIn);
   return deliteralIn;
}

Da beim Entschlüsseln keine Verschachtelung von Streams stattfindet, sondern eher ein „Auspacken“ durch das Herausholen von Objekten, sieht die Reihenfolge wie folgt aus:

Base64-Decodierung → Entschlüsselung → Dekompression → ReadLiteralData → Klartext

Dabei entfernt getDecoderStream (5) die Base64-Kodierung und kann nun entschlüsselt werden.

In decryptData fällt der komplexe Anteil für den Erhalt des entschlüsselten Streams an.

private InputStream decryptData(InputStream unarmoredInput) throws PGPException, IOException {
   PGPPublicKeyEncryptedData pbe = getPgpPublicKeyEncryptedData(unarmoredInput);
   // Einfachheitshalber wird der Key bereits als keyPair gespeichert
   PGPPrivateKey privateKey = keyPair.getPrivateKey();
   InputStream decrypedIn = pbe.getDataStream(new JcePublicKeyDataDecryptorFactoryBuilder()
         .setProvider("BC").build(privateKey));
   if (decrypedIn == null) {
      throw new IOException("Could not get datastream for encrypted data.");
   }
   return decrypedIn;
}

private PGPPublicKeyEncryptedData getPgpPublicKeyEncryptedData(InputStream inDecoder) throws IOException {
   PGPObjectFactory pgpF = new PGPObjectFactory(inDecoder, new JcaKeyFingerprintCalculator());
   Object o = pgpF.nextObject();
   PGPEncryptedDataList enc = getSessionData(o, pgpF);
   Iterator> it = enc.getEncryptedDataObjects();
   return (PGPPublicKeyEncryptedData) it.next();
}

private PGPEncryptedDataList getSessionData(Object o, PGPObjectFactory of) throws IOException {
   PGPEncryptedDataList enc;
   if (o instanceof PGPEncryptedDataList) {
      enc = (PGPEncryptedDataList) o;
   } else {
      enc = (PGPEncryptedDataList) of.nextObject();
   }
   return enc;
}

Was genau hier passiert, ist gar nicht so kompliziert, auch wenn es so aussehen mag. Wir ziehen uns lediglich die verschlüsselten Objekte aus dem Datenstream heraus. Dazu verwenden wir die von BC bereitgestellten Klassen (PGPEncryptedDataList und PGPPublicKeyEncryptedData), sowie die Factory PGPObjectFactory.

Wir erinnern uns, dass wir bei der Verschlüsselung mittels addMethod die PublicKey-Verschlüsselung festgelegt haben und als symmetrisches Verfahren CAST5 definiert haben. Entsprechend verwenden wir nun analog das PGPPublicKeyEncryptedData-Objekt, welches sich mit dem dazugehörigen Privatschlüssel entschlüsseln lässt. Im Code ist der Privatschlüssel bereits im keyPair hinterlegt, aber normalerweise sollte dieser aus einer Datei mit dem dazugehörigen Passphrase ausgelesen werden. (siehe Laden der Schlüssel)

Nachfolgend bei decompressData und readLiteral ist das Vorgehen ähnlich.

private InputStream decompressData(InputStream decryptedInStream) throws IOException, PGPException {
   PGPObjectFactory plainFact = new PGPObjectFactory(decryptedInStream, new JcaKeyFingerprintCalculator());
   Object obj = plainFact.nextObject();
   PGPCompressedData cData = (PGPCompressedData) obj;
   return new BufferedInputStream(cData.getDataStream());
}

private InputStream readLiteral(InputStream decompressedStream) throws IOException {
   PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(decompressedStream, new JcaKeyFingerprintCalculator());
   Object message = pgpObjectFactory.nextObject();
   PGPLiteralData ld = (PGPLiteralData) message;
   return new BufferedInputStream(ld.getInputStream(), BUFFER_SIZE);
}

Man erzeugt sich jeweils eine PGPObjectFactory und zieht aus dieser die Objekte heraus, welche man an dieser Stell „erwartet“. Für mehr Sicherheit ist eine Prüfung mit instanceof auch nicht verkehrt. Mit einem Aufruf auf getDecryptedStream(encryptedStream) erhält man so den entschlüsselten Inputstream und kann diesen als Datei speichern oder direkt weiterverarbeiten.

Laden der Schlüssel

Die Schlüsselpaar-Generierung ist auch komplett ohne Java möglich und auch empfehlenswert, da der Code für diese nicht so oft gebraucht wird. Dabei verlinke ich an diesen Post The Best Way To Generate PGP Key Pair | Encryption Consulting, welcher die Kommandos unter Windows erläutert. Hierbei wird für den Privatschlüssel zusätzlich eine Passphrase gebraucht, sodass der Schlüssel nicht für jeden ersichtlich ist, wenn er auf der Platte liegt. Im besten Fall hält man sich bei diesem Passwort auch an die Passwortrichtlinien (siehe https://www.1pw.de/brute-force.php), denn dieses könnte man im Loop durch brute-force ermitteln.

Folgendes Verfahren wird zum Laden des öffentlichen Schlüssels und Privatschlüssels verwendet:

private final String PUB_KEY = "/keys/public.pub";

private PGPPublicKey readPublicKey() {
   PGPPublicKey key = null;
   try(FileInputStream in = new FileInputStream(PUB_KEY)) {
      InputStream inDecoder = PGPUtil.getDecoderStream(in);
      PGPPublicKeyRingCollection pubRings = new PGPPublicKeyRingCollection(inDecoder, new JcaKeyFingerprintCalculator());
      Iterator> ringsIt = pubRings.getKeyRings();
      while (key == null && ringsIt.hasNext()) {
         PGPPublicKeyRing kRing = (PGPPublicKeyRing) ringsIt.next();
         Iterator> keysIt = kRing.getPublicKeys();
         while (key == null && keysIt.hasNext()) {
            PGPPublicKey k = (PGPPublicKey) keysIt.next();
            if(k.isEncryptionKey()) {
               key = k;
            }
         }
      }
      if (key == null) {
         throw new IllegalArgumentException("Cannot find encryption key.");
      }
   } catch (IOException | PGPException e) {
      // Log error and exit
   }
   return key;
}

private final String SEC_KEY_PATH = "/path/to/sec_key.sec";

private PGPPrivateKey findSecretKey(long keyID, char[] pass) throws IOException, PGPException {
   List secretKeyRings = new ArrayList();
   try (InputStream decoderStream = PGPUtil.getDecoderStream(new FileInputStream(SEC_KEY_PATH))) {
      secretKeyRings.add(new PGPSecretKeyRing(decoderStream, new JcaKeyFingerprintCalculator()));
   }
   PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(secretKeyRings);
   PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);
   if(pgpSecKey == null) {
      return null;
   } else {
      return pgpSecKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(pass));
   }
}

Entsprechend müsste dann der Code für decryptData() im InputStreamDecrypter wiefolgt angepasst werden, sodass er den privaten Schlüssel aus der Datei ließt:

private InputStream decryptData(InputStream unarmoredInput, char[] passphrase) throws PGPException, IOException {
   Iterator> dataObjects = getPgpPublicKeyEncryptedDataObjects(unarmoredInput);

   PGPPublicKeyEncryptedData pbe = null;
   PGPPrivateKey privateKey = null;
   while (privateKey == null && dataObjects.hasNext()) {
      pbe = (PGPPublicKeyEncryptedData) dataObjects.next();
      privateKey = findSecretKey(pbe.getKeyID(), passphrase);
   }
   InputStream decrypedIn = pbe.getDataStream(new JcePublicKeyDataDecryptorFactoryBuilder()
         .setProvider("BC").build(privateKey));
   if (decrypedIn == null) {
      throw new IOException("Could not get datastream for encrypted data.");
   }
   return decrypedIn;
}

Im best-case speichert ihr dann den Schlüssel an Stelle X und die verwendete Passphrase an Stelle Y. Es wäre ja doof, Schlüssel und Tresor direkt nebeneinander aufzubewahren.

Vor-/ und Nachteile

Nachteile:

  • Komplexe Code-Implementierung
  • Komplexe Datenhaltung (Separate Haltung des privaten Schlüssels und der Passphrase)

Vorteile:

  • Per Definition „noch“ sicher
  • Schneller symmetrischer Austausch
  • Durch BouncyCastle ist Kompression im gleichen Zug möglich
  • Datenintegrität und Authentizität kann gesichert werden
  • BouncyCastle bietet eine Vielzahl an Verschlüsselungs-Algorithmen

Quellen

[1] Was ist PGP-Verschlüsselung und wie funktioniert sie? https://www.varonis.com/de/blog/pgp-encryption Zugriff am 31.03.2023 [2] OpenPGP Under The Hood: https://under-the-hood.sequoia-pgp.org/literal-data/ Zugriff am 31.03.2023 [3] Digital Signature: https://security.stackexchange.com/questions/56593/does-a-pgp-signature-provide-integrity-verification Zugriff am 03.04.2023

Der Beitrag Verschlüsselung von Streams in Java erschien zuerst auf Business -Software- und IT-Blog - Wir gestalten digitale Wertschöpfung.



This post first appeared on DoubleSlash IT Business Und Software, please read the originial post: here

Share the post

Verschlüsselung von Streams in Java

×

Subscribe to Doubleslash It Business Und Software

Get updates delivered right to your inbox!

Thank you for your subscription

×