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

new() click to toggle source
# File lib/keystores/java_key_store.rb, line 21
def initialize
  @entries = {}
  @entries_mutex = Mutex.new
end

Public Instance Methods

aliases() click to toggle source
# File lib/keystores/java_key_store.rb, line 26
def aliases
  @entries.keys
end
contains_alias(aliaz) click to toggle source
# File lib/keystores/java_key_store.rb, line 30
def contains_alias(aliaz)
  @entries.has_key?(aliaz)
end
delete_entry(aliaz) click to toggle source
# File lib/keystores/java_key_store.rb, line 34
def delete_entry(aliaz)
  @entries_mutex.synchronize { @entries.delete(aliaz) }
end
get_certificate(aliaz) click to toggle source
# 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
get_certificate_alias(certificate) click to toggle source
# 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
get_certificate_chain(aliaz) click to toggle source
# 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
get_key(aliaz, password) click to toggle source
# 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
get_type() click to toggle source
# File lib/keystores/java_key_store.rb, line 91
def get_type
  TYPE
end
is_certificate_entry(aliaz) click to toggle source
# File lib/keystores/java_key_store.rb, line 95
def is_certificate_entry(aliaz)
  !@entries[aliaz].nil? && @entries[aliaz].is_a?(TrustedCertificateEntry)
end
is_key_entry(aliaz) click to toggle source
# File lib/keystores/java_key_store.rb, line 99
def is_key_entry(aliaz)
  !@entries[aliaz].nil? && @entries[aliaz].is_a?(KeyEntry)
end
load(key_store_file, password) click to toggle source
# 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
set_certificate_entry(aliaz, certificate) click to toggle source
# 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
set_key_entry(aliaz, key, certificate_chain, password) click to toggle source
# 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
size() click to toggle source
# File lib/keystores/java_key_store.rb, line 197
def size
  @entries.size
end
store(key_store_file, password) click to toggle source
# 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

get_pre_keyed_hash(password) click to toggle source

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
read_certificate(key_store_bytes, version, md) click to toggle source
# 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
read_int!(bytes, md) click to toggle source

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
read_long!(bytes, md) click to toggle source

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
read_unsigned_short!(bytes, md) click to toggle source

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
read_utf!(bytes, md) click to toggle source

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
verify_key_store_integrity(key_store_bytes, md) click to toggle source
# 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
write(file, bytes, md) click to toggle source
# File lib/keystores/java_key_store.rb, line 355
def write(file, bytes, md)
  md << bytes
  file.write(bytes)
end
write_certificate(file, certificate, md) click to toggle source
# 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
write_int(file, int, md) click to toggle source

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
write_long(file, long, md) click to toggle source

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
write_short(file, short, md) click to toggle source

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
write_utf(file, string, md) click to toggle source

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