Skip to content

Instantly share code, notes, and snippets.

@sgonyea
Created March 2, 2012 01:34
Show Gist options
  • Save sgonyea/1954648 to your computer and use it in GitHub Desktop.
Save sgonyea/1954648 to your computer and use it in GitHub Desktop.
BouncyCastle PGP Encryption in JRuby, _WITHOUT_ writing the data to disk before encryption
require 'java'
require 'java/bcpg-jdk16-146.jar'
module MyProject
def self.keys_dir
@keys_dir ||= Bundler.root.join("config/pgp/")
end
module PGP
java_import 'java.io.ByteArrayInputStream'
java_import 'java.io.ByteArrayOutputStream'
java_import 'java.io.DataOutputStream'
java_import 'java.security.NoSuchProviderException'
java_import 'java.security.SecureRandom'
java_import 'java.security.Security'
java_import 'org.bouncycastle.jce.provider.BouncyCastleProvider'
java_import 'org.bouncycastle.bcpg.ArmoredOutputStream'
include_package "org.bouncycastle.openpgp"
AB_Public_Key = MyProject.keys_dir.join("ab-pgp_public.asc").to_s
AB_Email_Addr = "ab@othercorp.com"
CD_Private_Key = MyProject.keys_dir.join("cd-pgp_private.asc").to_s
CD_Public_Key = MyProject.keys_dir.join("cd-pgp_public.asc").to_s
CD_Email_Addr = "cd@mycorp.com"
BC_Provider_Code = "BC"
Security.add_provider BouncyCastleProvider.new
# This is so awful. The Bouncy Castle API is trash.
def self.private_key_for_id(key_id)
file = File.open(CD_Private_Key)
pgp_sec = PGPSecretKeyRingCollection.new(PGPUtil.get_decoder_stream(file.to_inputstream))
sec_key = pgp_sec.get_secret_key(key_id)
if sec_key then sec_key.extract_private_key(nil, BC_Provider_Code)
else
nil
end
end
def self.public_key_from_filename(filename)
file = File.open(filename)
pk_col = PGPPublicKeyRingCollection.new(PGPUtil.get_decoder_stream(file.to_inputstream))
key_enumerator = pk_col.get_key_rings
encryption_key = nil
key_enumerator.each do |pk_ring|
pk_enumerator = pk_ring.get_public_keys
pk_enumerator.each do |key|
next unless key.is_encryption_key
encryption_key = key
break
end
end
encryption_key
end
def self.encrypt_file(filename)
encrypt(File.read filename)
end
# @param [PGPLiteralDataGenerator] pldg
# @return [Method] The Java Method :open for the given pldg
def self.pgp_literal_data_generator_to_open_call(pldg)
pldg.java_method :open, [java.io.OutputStream, Java::char, java.lang.String, Java::long, java.util.Date]
end
Public_Keys = {
:ab => public_key_from_filename(AB_Public_Key),
:cd => public_key_from_filename(CD_Public_Key)
}
def self.modification_time
Time.now
end
def self.pipe_contents(plain_text, p_out)
bytes = plain_text.to_java_bytes
p_out.write(bytes, 0, bytes.length)
end
def self.encrypt(plain_text, key_name=:ab)
key = Public_Keys[key_name]
baos = ByteArrayOutputStream.new
out = ArmoredOutputStream.new(baos)
b_out = ByteArrayOutputStream.new
com_data = PGPCompressedDataGenerator.new(PGPCompressedDataGenerator::ZIP)
pldg = PGPLiteralDataGenerator.new
pldg_open = pgp_literal_data_generator_to_open_call(pldg)
p_out = pldg_open.call(
com_data.open(b_out), # OutputStream out
PGPLiteralData::BINARY, # char format
"pgp", # String name
plain_text.length, # long length
modification_time # Date modificationTime
)
pipe_contents(plain_text, p_out)
com_data.close
bytes = b_out.to_byte_array
cpk = PGPEncryptedDataGenerator.new(PGPEncryptedDataGenerator::CAST5, SecureRandom.new, BC_Provider_Code)
cpk.add_method(key)
c_out = cpk.open(out, bytes.length)
c_out.write(bytes)
cpk.close
out.close
baos.to_string
end
def self.decrypt_file(filename)
file = File.read(filename)
decrypt(file)
end
def self.decrypt(encrypted_text)
bytes = encrypted_text.to_byte_array_input_stream
dec_s = PGPUtil.get_decoder_stream(bytes)
pgp_f = PGPObjectFactory.new(dec_s)
enc_data = pgp_f.next_object
enc_data = pgp_f.next_object unless PGPEncryptedDataList === enc_data
data_enumerator = enc_data.get_encrypted_data_objects
sec_key = nil
pbe = nil
data_enumerator.each do |pubkey_enc_data|
pbe = pubkey_enc_data
key_id = pubkey_enc_data.get_key_id
sec_key = private_key_for_id(key_id)
if sec_key.nil?
# @todo: Should we notify Airbrake?
MyProject.logger.error "This may be cause for concern. The data being decrypted has a key_id of '#{key_id}', which can not be found in the private key file '#{CD_Private_Key}'."
else
break
end
end
clear = pbe.get_data_stream(sec_key, BC_Provider_Code)
plain_fact = PGPObjectFactory.new(clear)
message = plain_fact.next_object
if(PGPCompressedData === message)
pgp_fact = PGPObjectFactory.new(message.get_data_stream)
message = pgp_fact.next_object
end
baos = ByteArrayOutputStream.new
if(PGPLiteralData === message)
unc = message.get_input_stream
while((ch = unc.read) >= 0)
baos.write(ch)
end
end
baos.to_string
end
end
end
@sgonyea
Copy link
Author

sgonyea commented Mar 2, 2012

One problem (of many) with Bouncy Castle's terrible API is that it requires you to write your data to disk prior to encryption. If this is a violation of your companies (equally terrible) policies, then you need to rip out and re-write some of Bouncy Castle's internals. Mega-sigh.

Other huge problems is the amount of shuffling you do, with objects, in BouncyCastle's APIs.

@sgonyea
Copy link
Author

sgonyea commented Mar 2, 2012

Also, this code is hardcoded to a specific private key. If you need decryption for multiple private keys, your life sucks.

@sgonyea
Copy link
Author

sgonyea commented Mar 2, 2012

Also note: You need to go download the bouncy castle JAR and it save it somewhere.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment