GPG data stream encryption and signing with Bouncycastle

I found it incredibly difficult to use BouncyCastle’s Java library to create data that GPG was actually able to decrypt. Even if compression is not the matter because you only have small tokens, it did not work without a Zip-DataGenerator in between (although there are plaintext variants of these Generators among the BC classes). GPG was complaining about not being able to decode character sequence “2D” or something.

You can save the string to a file if you need this, I had to create string data for passing it to a restful API.

protected final String publickeyFile; // the public key of the receiver, in armored format
protected final String privatekeyFile; // your private key ring file 
protected final String userId; // usually your email-address in your key ring
protected final String password; // password to your private key ring
protected final SecureRandom randomGenerator;

/**
 * Encrypt plaintext message using public key from publickeyFile.
 * 
 * @param message the message
 * @return the string
 */
private String encrypt( String message )
    throws PGPException, IOException, NoSuchProviderException
{
    // read public key file, this is an armored key file, no binary stream
    InputStream in =
        new ArmoredInputStream( Thread.currentThread().getContextClassLoader()
            .getResourceAsStream( publickeyFile ) );
    // find the PGP key in the file
    PGPPublicKey publicKey = findPublicGPGKey( in );
    Preconditions
        .checkNotNull( publicKey, Format.format( "Did not find the GPG key in keyfile ", publickeyFile ) );

    // Encode the string into bytes using utf-8
    byte[] utf8Bytes = message.getBytes( UTF8_ENCODING );

    ByteArrayOutputStream compressedOutput = new ByteArrayOutputStream();
    // compress bytes with zip
    PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
    // the reason why we compress here is GPG not being able to decrypt our message input but if we do not compress.
    // I guess pkzip compression also encodes only to GPG-friendly characters.
    PGPCompressedDataGenerator compressedDataGenerator =
        new PGPCompressedDataGenerator( CompressionAlgorithmTags.ZIP );
    try
    {
        OutputStream literalDataOutput =
            literalDataGenerator.open( compressedOutput, PGPLiteralData.BINARY, FAKE_PGP_INPUT_FILENAME,
                utf8Bytes.length, new Date() );
        // update bytes in the stream
        literalDataOutput.write( utf8Bytes );
    }
    catch ( IOException e )
    {
        // catch but close the streams in finally
        throw e;
    }
    finally
    {
        compressedDataGenerator.close();
        Closeables.closeQuietly( compressedOutput );
    }

    // now we have zip-compressed bytes
    byte[] compressedBytes = compressedOutput.toByteArray();

    PGPEncryptedDataGenerator encryptedDataGenerator =
        new PGPEncryptedDataGenerator( PGPEncryptedData.CAST5, true, randomGenerator, BC_PGP_PROVIDER );
    // use public key to encrypt data
    encryptedDataGenerator.addMethod( publicKey );

    // literalDataOutput --> compressedOutput --> ArmoredOutputStream --> ByteArrayOutputStream
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ArmoredOutputStream armoredOut = new ArmoredOutputStream( byteArrayOutputStream );
    OutputStream encryptedOutput = null;
    try
    {
        encryptedOutput = encryptedDataGenerator.open( armoredOut, compressedBytes.length );
        encryptedOutput.write( compressedBytes );
    }
    catch ( IOException e )
    {
        throw e;
    }
    catch ( PGPException e )
    {
        throw e;
    }
    finally
    {
        Closeables.closeQuietly( encryptedOutput );
        Closeables.closeQuietly( armoredOut );
    }
    String encrypted = new String( byteArrayOutputStream.toByteArray() );
    LOG.debug( Format.format( "Message: {} ", message ) );
    LOG.debug( Format.format( "Encrypted: {} ", encrypted ) );
    return encrypted;
}

/**
 * Find public gpg key in InputStream.
 * 
 * @param inputStream the input stream
 * @return the PGP public key
 */
private PGPPublicKey findPublicGPGKey( InputStream inputStream )
    throws IOException, PGPException
{
    // get all key rings in the input stream
    PGPPublicKeyRingCollection publicKeyRingCollection =
        new PGPPublicKeyRingCollection( PGPUtil.getDecoderStream( inputStream ) );
    LOG.debug( Format.format( "key ring size: {}", publicKeyRingCollection.size() ) );
    Iterator<PGPPublicKeyRing> keyRingIter = publicKeyRingCollection.getKeyRings();
    // iterate over keyrings
    while ( keyRingIter.hasNext() )
    {
        PGPPublicKeyRing keyRing = keyRingIter.next();
        Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys();
        // iterate over public keys in the key ring
        while ( keyIter.hasNext() )
        {
            PGPPublicKey tmpKey = keyIter.next();
            Preconditions.checkArgument( tmpKey != null );
            LOG.debug( Format.format( "Encryption key = {}, Master key = {}, UserIDs: {}",
                tmpKey.isEncryptionKey(), tmpKey.isMasterKey(), Iterators.toString( tmpKey.getUserIDs() ) ) );
            // we need a master encryption key
            if ( tmpKey.isEncryptionKey() && tmpKey.isMasterKey() )
            {
                return tmpKey;
            }
        }
    }
    throw new PGPException( "No public key found!" );
}

/**
 * Find private gpg key in InputStream.
 * 
 * @param inputStream the input stream
 * @param userId the user id
 * @return the PGP secret key
 */
private PGPSecretKey findPrivateGPGKey( InputStream inputStream, String userId )
    throws IOException, PGPException, NoSuchProviderException
{
    // iterate over every private key in the key ring
    PGPSecretKeyRingCollection secretKeyRings =
        new PGPSecretKeyRingCollection( PGPUtil.getDecoderStream( inputStream ) );
    // look for the key ring that is used to authenticate our reporting facilities
    Iterator<PGPSecretKeyRing> privateKeys = secretKeyRings.getKeyRings( userId );
    // iterate over every private key in the ring
    while ( privateKeys.hasNext() )
    {
        PGPSecretKeyRing secretKeyRing = privateKeys.next();
        PGPSecretKey tmpKey = secretKeyRing.getSecretKey();
        Preconditions.checkArgument( tmpKey != null );
        LOG.debug( Format.format( "Signing key = {}, Master key = {}, UserId = {}", tmpKey.isSigningKey(),
            tmpKey.isMasterKey(), userId ) );
        // we want the signing master key
        if ( tmpKey.isSigningKey() && tmpKey.isMasterKey() )
        {
            return tmpKey;
        }
    }
    throw new PGPException( "No private key found!" );
}

/**
 * Sign a plaintext message using our private PGP key file.
 * 
 * @param message the message
 * @return the string
 */
private String sign( String message )
    throws SignatureException, NoSuchAlgorithmException, PGPException, NoSuchProviderException, IOException
{
    InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream( privatekeyFile );
    PGPSecretKey secretKey = findPrivateGPGKey( in, userId );
    PGPPrivateKey privateKey = secretKey.extractPrivateKey( password.toCharArray(), BC_PGP_PROVIDER );

    PGPSignatureGenerator signature =
        new PGPSignatureGenerator( secretKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1, BC_PGP_PROVIDER );
    signature.initSign( PGPSignature.BINARY_DOCUMENT, privateKey );

    Iterator<String> userIds = secretKey.getPublicKey().getUserIDs();
    // use the first userId
    if ( userIds.hasNext() )
    {
        PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator();
        subpacketGenerator.setSignerUserID( false, userIds.next() );
        signature.setHashedSubpackets( subpacketGenerator.generate() );
    }
    else
    {
        throw new ReportingException( "Did not find userId" );
    }

    byte[] utf8Bytes = message.getBytes( UTF8_ENCODING );

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    ArmoredOutputStream armoredOut = new ArmoredOutputStream( byteArrayOutputStream );

    PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator( PGPCompressedData.ZLIB );
    BCPGOutputStream bcOutputStream = new BCPGOutputStream( compressedDataGenerator.open( armoredOut ) );

    signature.generateOnePassVersion( false ).encode( bcOutputStream );

    // literalDataOutput --> bcOutputStream--> compressedOutput --> ArmoredOutputStream --> ByteArrayOutputStream
    // &&
    // signatureOutput --> bcOutputStream--> compressedOutput --> ArmoredOutputStream --> ByteArrayOutputStream
    PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
    try
    {
        OutputStream literalDataOutput =
            literalDataGenerator.open( bcOutputStream, PGPLiteralData.BINARY, FAKE_PGP_INPUT_FILENAME,
                utf8Bytes.length, new Date() );

        // update bytes in the streams
        literalDataOutput.write( utf8Bytes );
        signature.update( utf8Bytes );

        // close generators and update signature
        literalDataGenerator.close();
        signature.generate().encode( bcOutputStream );
        compressedDataGenerator.close();
    }
    catch ( IOException e )
    {
        throw e;
    }
    catch ( PGPException e )
    {
        throw e;
    }
    finally
    {
        Closeables.closeQuietly( byteArrayOutputStream );
        Closeables.closeQuietly( armoredOut );
    }
    // }
    String signed = new String( byteArrayOutputStream.toByteArray() );
    LOG.debug( Format.format( "Message: {} ", message ) );
    LOG.debug( Format.format( "Signature: {} ", signed ) );
    return signed;
}

Leave a Reply

Your email address will not be published. Required fields are marked *