Categories
Development Java

QR Codes in Java

Für ein Projekt soll ich einen QR-Code generieren, der eine URL beinhaltet, die den Zugriff auf eine bestimmte Ressource ermöglicht.

In meinem letzten Post habe ich dokumentiert, wie die URL-Parameter verschlüsselt werden können.

Um mich mich dem Thema der QR Code Generierung zu öffnen habe ich insbesondere diese beiden Artikel verwendet: Generating Barcodes and QR Codes in Java auf Baeldung und Generating QR Code in Java auf Javapoint.

Die Wahl der Library fiel auf ZXing (“zebra crossing”), denn das ist die main library that supports QR codes in Java. und ich habe keine Anhaltspunkte finden können, warum ich eine andere Library nehmen sollte.

ZXing

Here, we need to add two Maven dependencies: the core image library and the Java client:

<dependencies>
  <!-- ZXing for QR-Code Generation -->
  <!-- https://mvnrepository.com/artifact/com.google.zxing/core -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.5.1</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/com.google.zxing/javase -->
  <dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.5.1</version>
  </dependency>
</dependencies>

QR Code generieren:

package deringo;

import java.awt.Desktop;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.NotFoundException;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

public class TestMain {

    public static void main(String args[]) throws WriterException, IOException, NotFoundException {
        String data = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam";
        Path path = Paths.get("./test.png");
        // Encoding charset to be used
        String charset = "UTF-8";
        Map<EncodeHintType, ErrorCorrectionLevel> hashMap = new HashMap<EncodeHintType, ErrorCorrectionLevel>();
        // generates QR code with Low level(L) error correction capability
        hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
        // invoking the user-defined method that creates the QR code
        generateQRcode(data, path, charset, hashMap, 200, 200);// increase or decrease height and width accodingly

        System.out.println("QR Code created successfully.");
        Desktop.getDesktop().open(path.toFile());
    }

    public static void generateQRcode(String data, Path path, String charset, Map<EncodeHintType, ErrorCorrectionLevel> map, int h, int w)
            throws WriterException, IOException {
        BitMatrix matrix = new MultiFormatWriter().encode(new String(data.getBytes(charset), charset),
                BarcodeFormat.QR_CODE, w, h);
        MatrixToImageWriter.writeToPath(matrix, getExtension(path), path);
    }
    
    /**
     * Own function until we have this in JDK<br>
     * Finally, there is a new method Path#getExtension available right in the JDK as of Java 21:<br>
     * https://stackoverflow.com/questions/3571223/how-do-i-get-the-file-extension-of-a-file-in-java/74315488#74315488
     */
    public static String getExtension(Path path) {
        String extension = path.getFileName().toString().substring(path.getFileName().toString().lastIndexOf('.') + 1);
        return extension;
    }
}

ZXing und Docx4J

Der QR Code lässt sich schön als PNG generieren. Allerdings brauche ich den QR Code in einem Word Dokument. Das Word Dokument wird mit Docx4J generiert.

Den Einstieg in Docx4J habe ich bereits vollzogen. Besonders hilfreich ist Introduction To Docx4J auf Baeldung.

package deringo;

import java.awt.Desktop;
import java.io.File;
import java.nio.file.Files;

import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Drawing;
import org.docx4j.wml.Jc;
import org.docx4j.wml.JcEnumeration;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.R;

public class TestMain {

    public static void main(String[] args) throws Exception {
        WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage();
        MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
        mainDocumentPart.addStyledParagraphOfText("Title", "Welcome to my QR Code");
        mainDocumentPart.addParagraphOfText("Welcome to my QR Code");
        
        File image = new File("test.png" );
        byte[] fileContent = Files.readAllBytes(image.toPath());
        BinaryPartAbstractImage imagePart = BinaryPartAbstractImage
          .createImagePart(wordPackage, fileContent);
        Inline inline = imagePart.createImageInline(
          "QR Code Image (filename hint)", "Alt Text", 1, 2, false);
        P Imageparagraph = addImageToParagraph(inline);
        mainDocumentPart.getContent().add(Imageparagraph);
        
        File exportFile = new File("welcome.docx");
        wordPackage.save(exportFile);
        
        Desktop.getDesktop().open(exportFile);
    }
    
    private static P addImageToParagraph(Inline inline) {
        ObjectFactory factory = new ObjectFactory();
        P p = factory.createP();
        R r = factory.createR();
        p.getContent().add(r);
        Drawing drawing = factory.createDrawing();
        r.getContent().add(drawing);
        drawing.getAnchorOrInline().add(inline);
        // center image
        PPr paragraphProperties = factory.createPPr();
        Jc justification = factory.createJc();
        justification.setVal(JcEnumeration.CENTER);
        paragraphProperties.setJc(justification);
        p.setPPr(paragraphProperties);
        
        return p;
    }
}

Das Ergebnis sieht brauchbar aus:

Allerdings habe ich die zuvor gespeicherte Datei verwendet. Ich möchte das aber on the fly machen, also ohne, dass ich den QR Code erst speichere und dann in das Word Dokument übernehme.

package deringo;

import java.awt.Desktop;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.imageio.ImageIO;

import org.docx4j.dml.wordprocessingDrawing.Inline;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.BinaryPartAbstractImage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Drawing;
import org.docx4j.wml.Jc;
import org.docx4j.wml.JcEnumeration;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.PPr;
import org.docx4j.wml.R;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

public class TestMain {

    public static void main(String[] args) throws Exception {
        WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage();
        MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart();
        mainDocumentPart.addStyledParagraphOfText("Title", "Welcome to my QR Code");
        mainDocumentPart.addParagraphOfText("Welcome to my QR Code");
        
        BinaryPartAbstractImage imagePart = BinaryPartAbstractImage
                .createImagePart(wordPackage, getQRCode());
        
        Inline inline = imagePart.createImageInline(
          "QR Code Image (filename hint)", "Alt Text", 1, 2, false);
        P Imageparagraph = addImageToParagraph(inline);
        mainDocumentPart.getContent().add(Imageparagraph);
        
        File exportFile = new File("welcome.docx");
        wordPackage.save(exportFile);
        
        Desktop.getDesktop().open(exportFile);
    }
    
    private static P addImageToParagraph(Inline inline) {
        ObjectFactory factory = new ObjectFactory();
        P p = factory.createP();
        R r = factory.createR();
        p.getContent().add(r);
        Drawing drawing = factory.createDrawing();
        r.getContent().add(drawing);
        drawing.getAnchorOrInline().add(inline);
        // center image
        PPr paragraphProperties = factory.createPPr();
        Jc justification = factory.createJc();
        justification.setVal(JcEnumeration.CENTER);
        paragraphProperties.setJc(justification);
        p.setPPr(paragraphProperties);
        
        return p;
    }
    
    public static byte[] getQRCode() throws Exception {
        String data = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam";
        // Encoding charset to be used
        String charset = "UTF-8";
        Map<EncodeHintType, ErrorCorrectionLevel> hashMap = new HashMap<EncodeHintType, ErrorCorrectionLevel>();
        // generates QR code with Low level(L) error correction capability
        hashMap.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
     
        BitMatrix matrix = new MultiFormatWriter().encode(new String(data.getBytes(charset), charset),
                BarcodeFormat.QR_CODE, 200, 200);
        BufferedImage bi = MatrixToImageWriter.toBufferedImage(matrix);
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(bi, "png", baos);
        byte[] bytes = baos.toByteArray();
        return bytes;
    }
}

Categories
Development Java

Java Encryption and Decryption

Für ein Projekt soll ich einen QR-Code generieren, der eine URL beinhaltet, die den Zugriff auf eine bestimmte Ressource ermöglicht.

Das einfachste Beispiel wäre so etwas wie: https://Kaulbach.de/QR-Ressouce/id=1

Ich möchte allerdings nicht, dass jemand, der die URL kennt, alle Ressourcen ansehen kann, indem er einfach den id-Parameter hochzählt.

Daher soll der id-Parameter verschlüsselt werden.

Tutorial

Ich arbeite mich durch den Artikel Java AES Encryption and Decryption auf Baeldung.

Verwendet wird Advanced Encryption Standard (AES) in der Variante AES/CBC/PKCS5Padding "because it's widely used in many projects".

AES Parameters

In the AES algorithm, we need three parameters: input data, secret key, and IV

Im Code wird dann auch noch ein Salt verwendet, also sind es sogar vier Parameter.

Code

Der komplette Code des Tutorials befindet sich in GitHub.

Hier der Minimal-Code, der für mich relevant ist:

package deringo;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class AESUtil {

    public static void main(String[] args) throws Exception {
        String input = "baeldung";
        String password = "myPassword";
        String salt = "mySalt";
        SecretKey key = getKeyFromPassword(password, salt); //generateKey(128);
        IvParameterSpec ivParameterSpec = generateIv();
        String algorithm = "AES/CBC/PKCS5Padding";
        String cipherText = encrypt(algorithm, input, key, ivParameterSpec);
        String plainText = decrypt(algorithm, cipherText, key, ivParameterSpec);
        System.out.println( input + " : " + plainText);
    }
    
    public static SecretKey getKeyFromPassword(String password, String salt)
        throws NoSuchAlgorithmException, InvalidKeySpecException {
        
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec)
            .getEncoded(), "AES");
        return secret;
    }
    
    public static IvParameterSpec generateIv() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return new IvParameterSpec(iv);
    }
    
    public static String encrypt(String algorithm, String input, SecretKey key,
        IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
        
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, key, iv);
        byte[] cipherText = cipher.doFinal(input.getBytes());
        return Base64.getEncoder()
            .encodeToString(cipherText);
    }
    
    public static String decrypt(String algorithm, String cipherText, SecretKey key,
        IvParameterSpec iv) throws NoSuchPaddingException, NoSuchAlgorithmException,
        InvalidAlgorithmParameterException, InvalidKeyException,
        BadPaddingException, IllegalBlockSizeException {
        
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, key, iv);
        byte[] plainText = cipher.doFinal(Base64.getDecoder()
            .decode(cipherText));
        return new String(plainText);
    }

}

Allerdings ändert sich bei jedem Durchlauf der Initialization Vector (IV). Dadurch kann man den verschlüsselten Wert (der in dem Beispielcode nicht ausgegeben wird) nicht in einem zweiten Durchlauf wieder entschlüsseln. Das entspricht somit noch nicht dem, was ich brauche.

Mein Code

Ich möchte lediglich einen String verschlüsseln, über den QR-Code weitergeben und später soll der verschlüsselte Wert wieder entschlüsselt werden. Daher muss ich, anders als in dem Beispiel oben, Passwort, Salt und IV speichern.

Dazu brauche ich einen IV im String Format, den ich mir als erstes generieren lasse:

public static String generateIV() {
  byte[] iv = new byte[16];
  new SecureRandom().nextBytes(iv);
  return Base64.getEncoder().encodeToString(iv);
}

Passwort, Salt und IV-String werden in den Properties gespeichert:

##################################### 
### SimpleCryptoUtil
SimpleCryptoUtil.password=myPassword
SimpleCryptoUtil.salt=mySalt
SimpleCryptoUtil.iv=2WbRy1wh3HsLe8Vir3TxkA==
#####################################

Damit kann ich die Methoden zum Erstellen von SecretKey und IvParameterSpec erstellen und darauf aufbauend die Methoden encrypt(String) und decrypt(String):

package deringo;

import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.quinscape.melba.support.Config;

public class SimpleCryptoUtil {
    private static Logger logger = LoggerFactory.getLogger(SimpleCryptoUtil.class);
    
    public static String encrypt(String input) {
        try {
            String algorithm = "AES/CBC/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(algorithm);
            cipher.init(Cipher.ENCRYPT_MODE, getKey(), getIv());
            byte[] cipherText = cipher.doFinal(input.getBytes());
            return Base64.getEncoder().encodeToString(cipherText);
        } catch (GeneralSecurityException e) {
            logger.error(e.getMessage());
            return null;
        }
    }
    
    public static String decrypt(String input) {
        try {
            String algorithm = "AES/CBC/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(algorithm);
            cipher.init(Cipher.DECRYPT_MODE, getKey(), getIv());
            byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(input));
            return new String(plainText);
        } catch (GeneralSecurityException e) {
            logger.error(e.getMessage());
            return null;
        }
    }
    
    private static SecretKey getKey() throws InvalidKeySpecException, NoSuchAlgorithmException {
        String password = Config.get("SimpleCryptoUtil.password");
        String salt     = Config.get("SimpleCryptoUtil.salt");
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, 256);
        SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
        return secret;
    }
    
    private static IvParameterSpec getIv() {
        String iv = Config.get("SimpleCryptoUtil.iv");
        return new IvParameterSpec(Base64.getDecoder().decode(iv));
    }

    protected static String generateIV() {
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        return Base64.getEncoder().encodeToString(iv);
    }
}

Die Verwendung ist dann so einfach wie gewünscht:

public static void main(String[] args) throws Exception {
  String secret = "geheim";
  String verschluesselt = SimpleCryptoUtil.encrypt(secret);
  System.out.println(secret + " : " + verschluesselt);
  String entschluesselt = SimpleCryptoUtil.decrypt(verschluesselt);
  System.out.println(verschluesselt + " : " + entschluesselt);
}