Skip to content

JKS compatibility load throws an unchecked ArrayIndexOutOfBoundsException on a short store #2451

Description

@Arpan0995

JKSKeyStoreSpi.validateStream (JKSKeyStoreSpi.java:364-392) reads the stream into rawStore (:368); on the password branch (:370) it subtracts the SHA-1 digest size from that length unchecked. :373 digests rawStore.length - checksumCalculator.getDigestSize() bytes and :380 copies from rawStore.length - checksum.length. Below 20 bytes both expressions go negative: update clamps its length to zero, and the System.arraycopy gets source index rawStore.length - 20 and throws ArrayIndexOutOfBoundsException. engineLoad (:208) has no catch, so it escapes KeyStore.load, which declares only IOException, NoSuchAlgorithmException and CertificateException.

Lines are origin/main ab16374d37; the class is byte identical at r1rv86 and in 1.87-SNAPSHOT sources, and the jdk1.1 and jdk1.4 copies do the same. AdaptingKeyStoreSpi.engineLoad reaches it through the gate at :150 and the probe at :158. keystore.type.compat=true is stock in JDK 25 (conf/security/java.security:301), so the PKCS12 types take that path as shipped; the FIPS types and PKCS12-PBMAC1 probe regardless.

import java.io.*; import java.security.*; import java.util.HexFormat;

public class JksShort {
    static void load(String tag, String type, String prov, int len, char[] pw) {
        // magic 0xFEEDFEED + version 1
        byte[] s = HexFormat.of().parseHex("feedfeed00000001" + "00".repeat(len - 8));
        try { KeyStore.getInstance(type, prov).load(new ByteArrayInputStream(s), pw); }
        catch (Throwable t) {
            System.out.println(tag + " [" + type + "/" + prov + "] bytes=" + len + " -> "
                + t.getClass().getName() + ": " + t.getMessage() + "  declaredByLoad="
                + (t instanceof IOException || t instanceof NoSuchAlgorithmException || t instanceof java.security.cert.CertificateException));
        }
    }
    public static void main(String[] a) {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
        String k = "keystore.type.compat";
        System.out.println(k + "=" + Security.getProperty(k));
        char[] pw = "changeit".toCharArray();
        load("8B  pw   ", "PKCS12", "BC", 8, pw);
        load("20B pw   ", "PKCS12", "BC", 20, pw);
        load("8B  null ", "PKCS12", "BC", 8, null);
        load("8B  pw   ", "JKS", "SUN", 8, pw);
    }
}

On the 1.86 and 1.87-SNAPSHOT jars, JDK 25:

keystore.type.compat=true
8B  pw    [PKCS12/BC] bytes=8 -> java.lang.ArrayIndexOutOfBoundsException: arraycopy: source index -12 out of bounds for byte[8]  declaredByLoad=false
20B pw    [PKCS12/BC] bytes=20 -> java.io.IOException: password incorrect or store tampered with  declaredByLoad=true
8B  null  [PKCS12/BC] bytes=8 -> java.io.EOFException: null  declaredByLoad=true
8B  pw    [JKS/SUN] bytes=8 -> java.io.EOFException: null  declaredByLoad=true

A 20-byte store reaches the comparison and the intended IOException; with a null password the same short store gives EOFException, as does SUN's JKS on the same bytes.

Proposed change

A length floor right after the read at :368, throwing that same IOException when rawStore.length is below checksumCalculator.getDigestSize(), would keep this inside the declared contract, with the same change in the other two copies and a truncated-store test. It is a contract defect and no more: the store is rejected either way, only the exception type changes. Happy to put a patch together.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions