class Keystores::JavaKeystore
An implementation of a Java Key Store (JKS) Format
Constants
- KEY_ENTRY_TAG
- MAGIC
Defined by JavaKeyStore.java
- TRUSTED_CERTIFICATE_ENTRY_TAG
- TYPE
- VERSION_1
- VERSION_2
Public Class Methods
# File lib/keystores/java_key_store.rb, line 21 def initialize @entries = {} @entries_mutex = Mutex.new end
Public Instance Methods
# File lib/keystores/java_key_store.rb, line 26 def aliases @entries.keys end
# File lib/keystores/java_key_store.rb, line 30 def contains_alias(aliaz) @entries.has_key?(aliaz) end
# File lib/keystores/java_key_store.rb, line 34 def delete_entry(aliaz) @entries_mutex.synchronize { @entries.delete(aliaz) } end
# File lib/keystores/java_key_store.rb, line 38 def get_certificate(aliaz) entry = @entries[aliaz] unless entry.nil? if entry.is_a? TrustedCertificateEntry entry.certificate elsif entry.is_a? KeyEntry entry.certificate_chain[0] else nil end end end
# File lib/keystores/java_key_store.rb, line 51 def get_certificate_alias(certificate) @entries.each do |aliaz, entry| if entry.is_a? TrustedCertificateEntry # We have to DER encode both of the certificates because OpenSSL::X509::Certificate doesn't implement equal? return aliaz if certificate.to_der == entry.certificate.to_der elsif entry.is_a? KeyEntry # We have to DER encode both of the certificates because OpenSSL::X509::Certificate doesn't implement equal? return aliaz if certificate.to_der == entry.certificate_chain[0].to_der end end nil end
# File lib/keystores/java_key_store.rb, line 64 def get_certificate_chain(aliaz) entry = @entries[aliaz] if !entry.nil? && entry.is_a?(KeyEntry) entry.certificate_chain else nil end end
# File lib/keystores/java_key_store.rb, line 73 def get_key(aliaz, password) entry = @entries[aliaz] # This somewhat odd control flow mirrors the Java code for ease of porting # TODO clean this up if entry.nil? || !entry.is_a?(KeyEntry) return nil end if password.nil? raise IOError.new('Password must not be nil') end encrypted_private_key = entry.encrypted_private_key encrypted_private_key_info = Keystores::Jks::EncryptedPrivateKeyInfo.new(:encoded => encrypted_private_key) Keystores::Jks::KeyProtector.new(password).recover(encrypted_private_key_info) end
# File lib/keystores/java_key_store.rb, line 91 def get_type TYPE end
# File lib/keystores/java_key_store.rb, line 95 def is_certificate_entry(aliaz) !@entries[aliaz].nil? && @entries[aliaz].is_a?(TrustedCertificateEntry) end
# File lib/keystores/java_key_store.rb, line 99 def is_key_entry(aliaz) !@entries[aliaz].nil? && @entries[aliaz].is_a?(KeyEntry) end
# File lib/keystores/java_key_store.rb, line 103 def load(key_store_file, password) @entries_mutex.synchronize do key_store_bytes = key_store_file.respond_to?(:read) ? key_store_file.read : IO.binread(key_store_file) # We pass this Message Digest around and add all of the bytes we read to it so we can verify integrity md = get_pre_keyed_hash(password) magic = read_int!(key_store_bytes, md) version = read_int!(key_store_bytes, md) if magic != MAGIC || (version != VERSION_1 && version != VERSION_2) raise IOError.new('Invalid keystore format') end count = read_int!(key_store_bytes, md) count.times do tag = read_int!(key_store_bytes, md) if tag == KEY_ENTRY_TAG key_entry = KeyEntry.new aliaz = read_utf!(key_store_bytes, md) time = read_long!(key_store_bytes, md) key_entry.creation_date = time private_key_length = read_int!(key_store_bytes, md) encrypted_private_key = key_store_bytes.slice!(0..(private_key_length - 1)) md << encrypted_private_key key_entry.encrypted_private_key = encrypted_private_key number_of_certs = read_int!(key_store_bytes, md) certificate_chain = [] number_of_certs.times do certificate_chain << read_certificate(key_store_bytes, version, md) end key_entry.certificate_chain = certificate_chain @entries[aliaz] = key_entry elsif tag == TRUSTED_CERTIFICATE_ENTRY_TAG trusted_cert_entry = TrustedCertificateEntry.new aliaz = read_utf!(key_store_bytes, md) time = read_long!(key_store_bytes, md) trusted_cert_entry.creation_date = time certificate = read_certificate(key_store_bytes, version, md) trusted_cert_entry.certificate = certificate @entries[aliaz] = trusted_cert_entry else raise IOError.new('Unrecognized keystore entry') end end unless password.nil? verify_key_store_integrity(key_store_bytes, md) end end end
# File lib/keystores/java_key_store.rb, line 164 def set_certificate_entry(aliaz, certificate) @entries_mutex.synchronize do entry = @entries[aliaz] if !entry.nil? && entry.is_a?(KeyEntry) raise ArgumentError.new('Cannot overwrite own certificate') end entry = TrustedCertificateEntry.new entry.certificate = certificate # Java uses new Date().getTime() which returns milliseconds since epoch, so we do the same here with %Q entry.creation_date = DateTime.now.strftime('%Q').to_i @entries[aliaz] = entry end end
# File lib/keystores/java_key_store.rb, line 180 def set_key_entry(aliaz, key, certificate_chain, password) @entries_mutex.synchronize do entry = @entries[aliaz] if !entry.nil? && entry.is_a?(TrustedCertificateEntry) raise ArgumentError.new('Cannot overwrite own key') end entry = KeyEntry.new # Java uses new Date().getTime() which returns milliseconds since epoch, so we do the same here with %Q entry.creation_date = DateTime.now.strftime('%Q').to_i entry.encrypted_private_key = Keystores::Jks::KeyProtector.new(password).protect(key) entry.certificate_chain = [certificate_chain].flatten @entries[aliaz] = entry end end
# File lib/keystores/java_key_store.rb, line 197 def size @entries.size end
# File lib/keystores/java_key_store.rb, line 201 def store(key_store_file, password) @entries_mutex.synchronize do # password is mandatory when storing if password.nil? raise ArgumentError.new("password can't be null") end md = get_pre_keyed_hash(password) io = key_store_file.respond_to?(:write) ? key_store_file : File.open(key_store_file, 'wb') write_int(io, MAGIC, md) # Always write the latest version write_int(io, VERSION_2, md) write_int(io, @entries.size, md) @entries.each do |aliaz, entry| if entry.is_a? KeyEntry write_int(io, KEY_ENTRY_TAG, md) write_utf(io, aliaz, md) write_long(io, entry.creation_date, md) write_int(io, entry.encrypted_private_key.length, md) write(io, entry.encrypted_private_key, md) certificate_chain = entry.certificate_chain chain_length = certificate_chain.nil? ? 0 : certificate_chain.length write_int(io, chain_length, md) unless certificate_chain.nil? certificate_chain.each { |certificate| write_certificate(io, certificate, md) } end elsif entry.is_a? TrustedCertificateEntry write_int(io, TRUSTED_CERTIFICATE_ENTRY_TAG, md) write_utf(io, aliaz, md) write_long(io, entry.creation_date, md) write_certificate(io, entry.certificate, md) else raise IOError.new('Unrecognized keystore entry') end end # Write the keyed hash which is used to detect tampering with # the keystore (such as deleting or modifying key or # certificate entries). io.write(md.digest) io.flush end end
Private Instance Methods
Derive a key in the same goofy way that Java does
# File lib/keystores/java_key_store.rb, line 275 def get_pre_keyed_hash(password) md = OpenSSL::Digest::SHA1.new passwd_bytes = [] password.unpack('c*').each do |byte| passwd_bytes << (byte >> 8) passwd_bytes << byte end md << passwd_bytes.pack('c*') md << 'Mighty Aphrodite'.force_encoding('UTF-8') md end
# File lib/keystores/java_key_store.rb, line 252 def read_certificate(key_store_bytes, version, md) # If we are a version 2 JKS, we check to see if we have the right certificate type # Version 1 JKS format unconditionally assumed X509 if version == 2 cert_type = read_utf!(key_store_bytes, md) if cert_type != 'X.509' && cert_type != 'X509' raise IOError.new("Unrecognized certificate type: #{cert_type}") end end certificate_length = read_int!(key_store_bytes, md) certificate = key_store_bytes.slice!(0..(certificate_length - 1)) md << certificate OpenSSL::X509::Certificate.new(certificate) end
Java uses DataInputStream#readInt() which is defined as reading 4 bytes and interpreting it as an int
# File lib/keystores/java_key_store.rb, line 299 def read_int!(bytes, md) bytes = bytes.slice!(0..3) md << bytes bytes.unpack('N')[0] end
Java uses DataInputStream#readLong which is defined as reading 8 bytes and interpreting it as a signed long
# File lib/keystores/java_key_store.rb, line 322 def read_long!(bytes, md) bytes = bytes.slice!(0..7) md << bytes bytes.unpack('q>')[0] end
Java uses DataInputStream#readUnsignedShort() which is defined as reading 2 bytes and interpreting it as an int
# File lib/keystores/java_key_store.rb, line 306 def read_unsigned_short!(bytes, md) bytes = bytes.slice!(0..1) md << bytes bytes.unpack('n')[0] end
Java uses DataInputStream#readUTF which does a bunch of crap to read a modified UTF-8 format TODO, this is a bit of a hack, but seems to work fine. We just assume we get a string out of the array
# File lib/keystores/java_key_store.rb, line 314 def read_utf!(bytes, md) utf_length = read_unsigned_short!(bytes, md) bytes = bytes.slice!(0..(utf_length - 1)) md << bytes bytes end
# File lib/keystores/java_key_store.rb, line 287 def verify_key_store_integrity(key_store_bytes, md) # The remaining key store bytes are the password based hash actual_hash = key_store_bytes computed_hash = md.digest # TODO, change how we compare these to defend against timing attacks even though JAVA doesn't if actual_hash != computed_hash raise IOError.new('Keystore was tampered with, or password was incorrect') end end
# File lib/keystores/java_key_store.rb, line 355 def write(file, bytes, md) md << bytes file.write(bytes) end
# File lib/keystores/java_key_store.rb, line 267 def write_certificate(file, certificate, md) encoded = certificate.to_der write_utf(file, 'X.509', md) write_int(file, encoded.length, md) write(file, encoded, md) end
Java uses DataInputStream#writeInt() which writes a 32 bit integer
# File lib/keystores/java_key_store.rb, line 335 def write_int(file, int, md) int = [int].pack('N') md << int file.write(int) end
Java uses DataInputStream#writeLong which writes a 64 bit integer
# File lib/keystores/java_key_store.rb, line 349 def write_long(file, long, md) long = [long].pack('q>') md << long file.write(long) end
Java uses DataInputStream#writeShort() which writes a 16 bit integer
# File lib/keystores/java_key_store.rb, line 342 def write_short(file, short, md) short = [short].pack('n') md << short file.write(short) end
Java uses DataOutputStream#writeUTF to write the length + string
# File lib/keystores/java_key_store.rb, line 329 def write_utf(file, string, md) write_short(file, string.length, md) write(file, string, md) end