3675 lines
132 KiB
Java
3675 lines
132 KiB
Java
/* WolfSSLKeyStore.java
|
|
*
|
|
* Copyright (C) 2006-2026 wolfSSL Inc.
|
|
*
|
|
* This file is part of wolfSSL.
|
|
*
|
|
* wolfSSL is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* wolfSSL is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
|
*/
|
|
|
|
package com.wolfssl.provider.jce;
|
|
|
|
import java.util.Date;
|
|
import java.util.Enumeration;
|
|
import java.util.Arrays;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.Map;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.DataInputStream;
|
|
import java.io.DataOutputStream;
|
|
import java.io.IOException;
|
|
import java.security.Key;
|
|
import java.security.KeyFactory;
|
|
import java.security.KeyStoreSpi;
|
|
import java.security.PrivateKey;
|
|
import java.security.PublicKey;
|
|
import java.security.SecureRandom;
|
|
import java.security.Security;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.security.UnrecoverableKeyException;
|
|
import java.security.spec.PKCS8EncodedKeySpec;
|
|
import java.security.spec.InvalidKeySpecException;
|
|
import java.security.KeyStoreException;
|
|
import java.security.NoSuchProviderException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.cert.Certificate;
|
|
import java.security.cert.X509Certificate;
|
|
import java.security.cert.CertificateFactory;
|
|
import java.security.cert.CertificateException;
|
|
import java.security.cert.CertificateEncodingException;
|
|
import java.security.MessageDigest;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.CharBuffer;
|
|
import java.nio.charset.StandardCharsets;
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.SecretKey;
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.NoSuchPaddingException;
|
|
import javax.crypto.IllegalBlockSizeException;
|
|
import javax.crypto.BadPaddingException;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
import javax.crypto.spec.IvParameterSpec;
|
|
import javax.security.auth.DestroyFailedException;
|
|
|
|
import com.wolfssl.wolfcrypt.Asn;
|
|
import com.wolfssl.wolfcrypt.Aes;
|
|
import com.wolfssl.wolfcrypt.FeatureDetect;
|
|
import com.wolfssl.wolfcrypt.MlDsa;
|
|
import com.wolfssl.wolfcrypt.Pwdbased;
|
|
import com.wolfssl.wolfcrypt.WolfCrypt;
|
|
import com.wolfssl.wolfcrypt.WolfSSLCertManager;
|
|
import com.wolfssl.wolfcrypt.WolfCryptException;
|
|
|
|
/**
|
|
* wolfSSL KeyStore implementation (WKS).
|
|
*
|
|
* This KeyStore has been designed to be compatible with wolfCrypt
|
|
* FIPS 140-2 and 140-3, using algorithms and modes inside the wolfCrypt FIPS
|
|
* module boundary.
|
|
*
|
|
* Private keys are protected inside this KeyStore implementation using
|
|
* PKCS#5 PBKDF2 and AES-CBC with HMAC-SHA512, specifically:
|
|
*
|
|
* 1. PKCS#5 PBKDF2 derives an encryption key from provided user password
|
|
* + Password is converted from char[] to byte[] using UTF-8
|
|
* + Salt size = 16 bytes, Iteration count = 210,000
|
|
* + Iterations can be customized using wolfjce.wks.iterationCount
|
|
* Security property in a java.security file
|
|
*
|
|
* 2. AES-CBC encrypts the private key using derived password
|
|
* + IV length = 16 bytes
|
|
* + Key length = 32 bytes (256 bits)
|
|
*
|
|
* 3. HMAC-SHA512 is calculated over the encrypted key and associated
|
|
* parameters (Encrypt-then-MAC).
|
|
*
|
|
* When this KeyStore is stored (engineStore()), the following format is used.
|
|
* There is an HMAC-SHA512 stored at the end which is calculated over the
|
|
* entire HEADER + ENTRIES + PBKDF2 SALT LEN/SALT + PBKDF2 iterations, which
|
|
* is used to check the KeyStore integrity when loaded back in (engineLoad())
|
|
* to detect corrupt or tampered KeyStores.
|
|
*
|
|
* HEADER:
|
|
* magicNumber (int / 7)
|
|
* keystoreVersion (int)
|
|
* entryCount (int)
|
|
* ENTRIES (can be any of below, depending on type)
|
|
* [WKSPrivateKey]
|
|
* entryId (int / 1)
|
|
* alias (UTF String)
|
|
* creationDate.getTime() (long)
|
|
* kdfSalt.length (int)
|
|
* kdfSalt (byte[])
|
|
* kdfIterations (int)
|
|
* iv.length (int)
|
|
* iv (byte[])
|
|
* encryptedKey.length (int)
|
|
* encryptedKey (byte[])
|
|
* chain.length (int)
|
|
* FOR EACH CERT:
|
|
* chain[i].getType() (UTF String)
|
|
* chain[i].getEncoded().length (int)
|
|
* chain[i].getEncoced() (byte[])
|
|
* hmac.length (int)
|
|
* hmac (HMAC-SHA512) (byte[])
|
|
* [WKSSecretKey]
|
|
* entryId (int / 3)
|
|
* alias (UTF String)
|
|
* creationDate.getTime() (long)
|
|
* key.getAlgorithm() (UTF String)
|
|
* kdfSalt.length (int)
|
|
* kdfIterations (int)
|
|
* kdfSalt (byte[])
|
|
* iv.length (int)
|
|
* iv (byte[])
|
|
* encryptedKey.length (int)
|
|
* encryptedKey (byte[])
|
|
* hmac.length (int)
|
|
* hmac (HMAC-SHA512) (byte[])
|
|
* [WKSCertificate]
|
|
* entryId (int / 2)
|
|
* alias (UTF String)
|
|
* creationDate.getTime() (long)
|
|
* cert.getType() (UTF String)
|
|
* cert.getEncoded().length (int)
|
|
* cert.getEncoced() (byte[])
|
|
* HMAC PBKDF2 salt length int
|
|
* HMAC PBKDF2 salt (byte[])
|
|
* HMAC PBKDF2 iterations int
|
|
* HMAC length int
|
|
* HMAC (HMAC-SHA512) (byte[])
|
|
*
|
|
* When loading a KeyStore (engineLoad()), the password is optional. If a
|
|
* password is provided, we recalculate the HMAC over the input KeyStore and
|
|
* check against the HMAC encoded in the KeyStore bytes to detect if the
|
|
* stored KeyStore has been tampered with. If a password is not provided,
|
|
* the integrity check will be skipped. This is consistent with existing
|
|
* (ie: JKS) KeyStore implementation behavior and allows for consistent use of
|
|
* system KeyStores (ex: cacerts) where users do not normally have/use the
|
|
* password when loading the KeyStore.
|
|
*
|
|
* Each PrivateKey and SecretKey entry includes a separate HMAC-SHA512.
|
|
* That HMAC is loaded together with the entry and verified against the
|
|
* provided password when the entry is retrieved by the user. This is
|
|
* independent of the entire KeyStore integrity HMAC verification.
|
|
*
|
|
* KEK Caching for Performance
|
|
*
|
|
* Repeated calls to {@code getKey()} can be slow due to PBKDF2 key derivation.
|
|
* This design is on purpose for security of private keys. An optional KEK
|
|
* (Key Encryption Key) cache can be enabled to improve performance by caching
|
|
* derived keys in memory.
|
|
*
|
|
* Security properties controlling KEK caching:
|
|
*
|
|
* {@code wolfjce.keystore.kekCacheEnabled} - Set to "true" to enable
|
|
* caching (default: false/disabled)
|
|
*
|
|
* {@code wolfjce.keystore.kekCacheTtlSec} - Cache TTL in seconds
|
|
* (default: 300 = 5 minutes)
|
|
*
|
|
* Security Note: Enabling the cache keeps derived keys in memory for the TTL
|
|
* duration. Only enable in trusted environments where the performance benefit
|
|
* outweighs the increased memory exposure window.
|
|
*/
|
|
public class WolfSSLKeyStore extends KeyStoreSpi {
|
|
|
|
/* RNG used for generating random IVs and salts */
|
|
private SecureRandom rand = null;
|
|
private static final Object randLock = new Object();
|
|
|
|
/* PBKDF2 parameters:
|
|
* [salt]: NIST SP 800-132 recommends salts should be at least 128 bits
|
|
* [iterations]: OWASP PBKDF2 guidance recommends 210,000 iterations
|
|
* of PBKDF2-HMAC-SHA512. HMAC-SHA512 was chosen since significantly
|
|
* fewer iterations are required as compared to HMAC-SHA256 (requires
|
|
* 600,000 iterations to match OWASP recommendations). Iterations
|
|
* can be customized using Java Security property
|
|
* 'wolfjce.wks.iterationCount' in java.security. Minimum iterations
|
|
* allowed is 10,000.
|
|
* [type]: SHA-512 (WolfCrypt.WC_HASH_TYPE_SHA512) */
|
|
private static final int WKS_PBKDF2_SALT_SIZE = 16;
|
|
private static final int WKS_PBKDF2_MIN_ITERATIONS = 10000;
|
|
private static final int WKS_PBKDF2_DEFAULT_ITERATIONS = 210000;
|
|
private static final int WKS_PBKDF2_ITERATION_COUNT;
|
|
private static final int WKS_PBKDF2_TYPE = WolfCrypt.WC_HASH_TYPE_SHA512;
|
|
|
|
/* AES-CBC parameters (bytes) */
|
|
private static final int WKS_ENC_IV_LENGTH = 16;
|
|
private static final int WKS_ENC_KEY_LENGTH = Aes.KEY_SIZE_256;
|
|
|
|
/* HMAC parameters:
|
|
* 64-bytes (512-bit) to match usage with HMAC-SHA512 */
|
|
private static final int WKS_HMAC_KEY_LENGTH = 64;
|
|
|
|
/* Max cert chain length, used in sanity check when loading a KeyStore.
|
|
* Can be customized via 'wolfjce.wks.maxCertChainLength' Java Security
|
|
* property in java.security file */
|
|
private static final int WKS_DEFAULT_MAX_CHAIN_COUNT = 100;
|
|
private static final int WKS_MAX_CHAIN_COUNT;
|
|
|
|
/* Max encoded entry size in bytes, configurable via
|
|
* 'wolfjce.wks.maxEntrySize' Security property */
|
|
private static final int WKS_DEFAULT_MAX_ENTRY_SIZE = 10 * 1024 * 1024;
|
|
private static final int WKS_MAX_ENTRY_SIZE;
|
|
|
|
/* WKS magic number, used when storing KeyStore to OutputStream */
|
|
private static final int WKS_MAGIC_NUMBER = 7;
|
|
|
|
/* WKS KeyStore version (may increment in future if behavior changes) */
|
|
private static final int WKS_STORE_VERSION = 1;
|
|
|
|
/* WKS entry IDs, used when storing/loading KeyStore */
|
|
private static final int WKS_ENTRY_ID_PRIVATE_KEY = 1;
|
|
private static final int WKS_ENTRY_ID_CERTIFICATE = 2;
|
|
private static final int WKS_ENTRY_ID_SECRET_KEY = 3;
|
|
|
|
/* Security property name to enable KEK cache (disabled by default) */
|
|
private static final String KEK_CACHE_ENABLED_PROPERTY =
|
|
"wolfjce.keystore.kekCacheEnabled";
|
|
|
|
/* Security property name for KEK cache TTL in seconds */
|
|
private static final String KEK_CACHE_TTL_PROPERTY =
|
|
"wolfjce.keystore.kekCacheTtlSec";
|
|
|
|
/* Default TTL: 5 minutes in milliseconds */
|
|
private static final long KEK_CACHE_DEFAULT_TTL_MS = 300000;
|
|
|
|
/**
|
|
* KeyStore entries as ConcurrentHashMap.
|
|
* Entry values are objects of one of the following types:
|
|
* WKSPrivateKey, WKSCertificate, WKSSecretKey. Keys are Strings which
|
|
* represent an alias name.
|
|
*/
|
|
private ConcurrentHashMap<String, Object> entries =
|
|
new ConcurrentHashMap<>();
|
|
|
|
private enum EntryType {
|
|
PRIVATE_KEY, /* WKSPrivateKey */
|
|
CERTIFICATE, /* WKSCertificate */
|
|
SECRET_KEY /* WKSSecretKey */
|
|
};
|
|
|
|
/**
|
|
* Cache for derived KEK keys, keyed by SHA-256(passwordHash + kdfSalt +
|
|
* kdfIterations). Used to avoid repeated PBKDF2 derivations for the same
|
|
* password/salt combination if enabled via Security property.
|
|
*/
|
|
private final Map<ByteArrayWrapper, KekCacheEntry> kekCache =
|
|
new ConcurrentHashMap<>();
|
|
|
|
/* Lock for cache operations */
|
|
private final Object cacheLock = new Object();
|
|
|
|
/**
|
|
* KEK cache entry holding derived KEK key and metadata.
|
|
*/
|
|
private static class KekCacheEntry {
|
|
|
|
byte[] derivedKey; /* cached KEK + HMAC key */
|
|
byte[] passHash; /* SHA-256 hash of password */
|
|
long expiryTime; /* System.currentTimeMillis() when entry expires */
|
|
|
|
KekCacheEntry(byte[] derivedKey, byte[] passHash, long expiryTime) {
|
|
this.derivedKey = derivedKey.clone();
|
|
this.passHash = passHash.clone();
|
|
this.expiryTime = expiryTime;
|
|
}
|
|
|
|
synchronized void wipe() {
|
|
if (derivedKey != null) {
|
|
Arrays.fill(derivedKey, (byte)0);
|
|
derivedKey = null;
|
|
}
|
|
if (passHash != null) {
|
|
Arrays.fill(passHash, (byte)0);
|
|
passHash = null;
|
|
}
|
|
expiryTime = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wrapper for byte arrays to use as kekCache map keys.
|
|
*/
|
|
private static class ByteArrayWrapper {
|
|
|
|
private final byte[] data;
|
|
|
|
ByteArrayWrapper(byte[] data) {
|
|
this.data = data.clone();
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
|
|
if (this == obj) {
|
|
return true;
|
|
}
|
|
if ((obj == null) || (getClass() != obj.getClass())) {
|
|
return false;
|
|
}
|
|
|
|
ByteArrayWrapper that = (ByteArrayWrapper)obj;
|
|
return Arrays.equals(data, that.data);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Arrays.hashCode(data);
|
|
}
|
|
|
|
void wipe() {
|
|
if (data != null) {
|
|
Arrays.fill(data, (byte)0);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static {
|
|
int iCount = WKS_PBKDF2_DEFAULT_ITERATIONS;
|
|
int cLength = WKS_DEFAULT_MAX_CHAIN_COUNT;
|
|
int eLength = WKS_DEFAULT_MAX_ENTRY_SIZE;
|
|
String iterations = null;
|
|
String chainCount = null;
|
|
String entrySize = null;
|
|
|
|
/* Set PBKDF2 iteration count, using default or one set by
|
|
* user in 'wolfjce.wks.iterationCount' Security property in
|
|
* java.security file */
|
|
iterations = Security.getProperty("wolfjce.wks.iterationCount");
|
|
if (iterations != null && !iterations.isEmpty()) {
|
|
try {
|
|
iCount = Integer.parseInt(iterations);
|
|
if (iCount < WKS_PBKDF2_MIN_ITERATIONS) {
|
|
log("wolfjce.wks.iterationCount (" + iCount + ") lower " +
|
|
"than min allowed (" + WKS_PBKDF2_MIN_ITERATIONS +
|
|
")");
|
|
iCount = WKS_PBKDF2_DEFAULT_ITERATIONS;
|
|
}
|
|
|
|
} catch (NumberFormatException e) {
|
|
/* Error parsing property, fall back to default */
|
|
log("error parsing wolfjce.wks.iterationCount property, " +
|
|
"using default instead");
|
|
}
|
|
}
|
|
|
|
log("setting PBKDF2 iterations: " + iCount);
|
|
WKS_PBKDF2_ITERATION_COUNT = iCount;
|
|
|
|
/* Set max certificate chain length limitation, using default or one
|
|
* set with `wolfjce.wks.maxCertChainLength` Security property in
|
|
* java.security file */
|
|
chainCount = Security.getProperty("wolfjce.wks.maxCertChainLength");
|
|
if (chainCount != null && !chainCount.isEmpty()) {
|
|
try {
|
|
cLength = Integer.parseInt(chainCount);
|
|
if (cLength <= 0) {
|
|
log("wolfjce.wks.maxCertChainLength (" + cLength +
|
|
") lower than 0, using default");
|
|
cLength = WKS_DEFAULT_MAX_CHAIN_COUNT;
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
/* Error parsing property, fall back to default */
|
|
log("error parsing wolfjce.wks.maxCertChainLength property, " +
|
|
"using default instead");
|
|
}
|
|
}
|
|
|
|
log("setting max cert chain length: " + cLength);
|
|
WKS_MAX_CHAIN_COUNT = cLength;
|
|
|
|
/* Set max encoded entry size limit, using default or one set with
|
|
* `wolfjce.wks.maxEntrySize` Security property */
|
|
entrySize = Security.getProperty("wolfjce.wks.maxEntrySize");
|
|
if (entrySize != null && !entrySize.isEmpty()) {
|
|
try {
|
|
eLength = Integer.parseInt(entrySize);
|
|
if (eLength <= 0) {
|
|
log("wolfjce.wks.maxEntrySize (" + eLength +
|
|
") lower than 0, using default");
|
|
eLength = WKS_DEFAULT_MAX_ENTRY_SIZE;
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
/* Error parsing property, fall back to default */
|
|
log("error parsing wolfjce.wks.maxEntrySize property, " +
|
|
"using default instead");
|
|
}
|
|
}
|
|
|
|
log("setting max entry size: " + eLength);
|
|
WKS_MAX_ENTRY_SIZE = eLength;
|
|
}
|
|
|
|
/**
|
|
* Create new WolfSSLKeyStore object
|
|
*/
|
|
public WolfSSLKeyStore() {
|
|
log("created new KeyStore: type WKS (version: " +
|
|
WKS_STORE_VERSION + ")");
|
|
}
|
|
|
|
/**
|
|
* Clear all KEK cache entries.
|
|
*
|
|
* This method removes all cached derived keys. Should be called when:
|
|
* - KeyStore instance is no longer needed
|
|
* - Want to ensure cached keys are removed from memory
|
|
* - Security policy requires explicit cache clearing
|
|
*
|
|
* The cache will also be cleared automatically when this KeyStore is
|
|
* garbage collected. But calling this method explicitly provides
|
|
* deterministic cleanup.
|
|
*
|
|
* This method is safe to call multiple times and has no effect
|
|
* if the cache is already empty.
|
|
*/
|
|
public void clearCache() {
|
|
clearKekCache();
|
|
}
|
|
|
|
/**
|
|
* Cleanup method to wipe KEK cache when KeyStore is garbage collected.
|
|
*/
|
|
@SuppressWarnings({"deprecation", "removal"})
|
|
@Override
|
|
protected void finalize() throws Throwable {
|
|
try {
|
|
/* Ensure KEK cache is cleared */
|
|
clearCache();
|
|
} finally {
|
|
super.finalize();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Native JNI method that calls wolfSSL_X509_check_private_key()
|
|
* to confirm that the provided X.509 certificate matches the given
|
|
* private key.
|
|
*
|
|
* @param derCert X.509 certificate encoded as DER byte array
|
|
* @param pkcs8PrivKey Private key encoded as PKCS#8 byte array
|
|
*
|
|
* @return true if matches, otherwise false if no match
|
|
*
|
|
* @throws WolfCryptException on native wolfSSL error
|
|
*/
|
|
private native boolean X509CheckPrivateKey(
|
|
byte[] derCert, byte[] pkcs8PrivKey) throws WolfCryptException;
|
|
|
|
/**
|
|
* Check if KEK caching is enabled via Security property.
|
|
*
|
|
* @return true if cache is enabled, false otherwise
|
|
*/
|
|
private boolean isKekCacheEnabled() {
|
|
|
|
String enabled = Security.getProperty(KEK_CACHE_ENABLED_PROPERTY);
|
|
|
|
if (enabled != null && enabled.equalsIgnoreCase("true")) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get KEK cache TTL from Security property, convert to ms and return.
|
|
*
|
|
* @return TTL in milliseconds
|
|
*/
|
|
private long getKekCacheTtlMs() {
|
|
|
|
long ttlSec;
|
|
String ttlStr = Security.getProperty(KEK_CACHE_TTL_PROPERTY);
|
|
|
|
if (ttlStr != null) {
|
|
try {
|
|
ttlSec = Long.parseLong(ttlStr.trim());
|
|
if (ttlSec > 0) {
|
|
/* Convert from sec to ms, checking for overflow */
|
|
if (ttlSec > Long.MAX_VALUE / 1000) {
|
|
/* Overflow would occur, return Long.MAX_VALUE */
|
|
return Long.MAX_VALUE;
|
|
}
|
|
return ttlSec * 1000;
|
|
}
|
|
} catch (NumberFormatException e) {
|
|
log("error parsing " + KEK_CACHE_TTL_PROPERTY +
|
|
" property, using default TTL instead");
|
|
}
|
|
}
|
|
|
|
return KEK_CACHE_DEFAULT_TTL_MS;
|
|
}
|
|
|
|
/**
|
|
* Hash password using SHA-256.
|
|
*
|
|
* Converts char[] to byte[] without creating an intermediate String.
|
|
*
|
|
* @param password password to hash
|
|
*
|
|
* @return SHA-256 hash of password
|
|
*
|
|
* @throws NoSuchAlgorithmException if SHA-256 not available
|
|
*/
|
|
private byte[] hashPassword(char[] password)
|
|
throws NoSuchAlgorithmException {
|
|
|
|
byte[] passBytes = null;
|
|
ByteBuffer byteBuffer = null;
|
|
CharBuffer charBuffer = null;
|
|
MessageDigest md = null;
|
|
|
|
try {
|
|
/* Convert char[] to byte[] */
|
|
charBuffer = CharBuffer.wrap(password);
|
|
byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
|
|
|
|
passBytes = new byte[byteBuffer.remaining()];
|
|
byteBuffer.get(passBytes);
|
|
|
|
md = MessageDigest.getInstance("SHA-256");
|
|
return md.digest(passBytes);
|
|
|
|
} finally {
|
|
if (passBytes != null) {
|
|
Arrays.fill(passBytes, (byte)0);
|
|
}
|
|
if (byteBuffer != null && byteBuffer.hasArray()) {
|
|
Arrays.fill(byteBuffer.array(), (byte)0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate cache key by hashing: password hash, salt, and iteration count.
|
|
*
|
|
* Including iteration count ensures entries with different PBKDF2
|
|
* iterations have different cache keys, even if they share the same
|
|
* password and salt.
|
|
*
|
|
* @param passwordHash SHA-256 hash of password
|
|
* @param kdfSalt PBKDF2 salt from the entry
|
|
* @param kdfIterations PBKDF2 iteration count from the entry
|
|
*
|
|
* @return SHA-256 hash to use as cache key
|
|
*
|
|
* @throws NoSuchAlgorithmException if SHA-256 not available
|
|
*/
|
|
private byte[] generateCacheKey(byte[] passwordHash, byte[] kdfSalt,
|
|
int kdfIterations) throws NoSuchAlgorithmException {
|
|
|
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
|
|
md.update(passwordHash);
|
|
md.update(kdfSalt);
|
|
/* Include iteration count as 4 bytes (big-endian) */
|
|
md.update((byte)(kdfIterations >> 24));
|
|
md.update((byte)(kdfIterations >> 16));
|
|
md.update((byte)(kdfIterations >> 8));
|
|
md.update((byte)(kdfIterations));
|
|
|
|
return md.digest();
|
|
}
|
|
|
|
/**
|
|
* Retrieve cached derived key for given password, salt, and iterations.
|
|
*
|
|
* @param password password used for key derivation
|
|
* @param kdfSalt PBKDF2 salt from the entry
|
|
* @param kdfIterations PBKDF2 iteration count from the entry
|
|
*
|
|
* @return cached derived key if found and valid, null otherwise
|
|
*/
|
|
private byte[] getCachedDerivedKey(char[] password, byte[] kdfSalt,
|
|
int kdfIterations) {
|
|
|
|
long now;
|
|
byte[] passHash = null;
|
|
byte[] cacheKeyBytes = null;
|
|
ByteArrayWrapper lookupKey = null;
|
|
KekCacheEntry entryValue = null;
|
|
|
|
/* Return null if caching is disabled */
|
|
if (!isKekCacheEnabled()) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
/* Compute password hash and cache key */
|
|
passHash = hashPassword(password);
|
|
cacheKeyBytes = generateCacheKey(passHash, kdfSalt, kdfIterations);
|
|
lookupKey = new ByteArrayWrapper(cacheKeyBytes);
|
|
|
|
synchronized (cacheLock) {
|
|
|
|
entryValue = kekCache.get(lookupKey);
|
|
if (entryValue != null) {
|
|
/* If cache entry expired, remove and return null */
|
|
now = System.currentTimeMillis();
|
|
if (now >= entryValue.expiryTime) {
|
|
|
|
/* Wipe both key and value, then remove from cache */
|
|
kekCache.computeIfPresent(lookupKey, (key, value) -> {
|
|
value.wipe();
|
|
key.wipe();
|
|
return null; /* Remove entry */
|
|
});
|
|
|
|
log("Cache entry expired, removed from cache");
|
|
return null;
|
|
}
|
|
|
|
/* Verify password hash matches */
|
|
if (!MessageDigest.isEqual(passHash, entryValue.passHash)) {
|
|
/* Password mismatch - don't use cache */
|
|
return null;
|
|
}
|
|
|
|
/* Cache hit - return copy of derived key */
|
|
log("Using cached PBKDF2 derived key");
|
|
return entryValue.derivedKey.clone();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
/* If MessageDigest SHA-256 is not available, return null */
|
|
return null;
|
|
|
|
} finally {
|
|
if (passHash != null) {
|
|
Arrays.fill(passHash, (byte)0);
|
|
}
|
|
if (cacheKeyBytes != null) {
|
|
Arrays.fill(cacheKeyBytes, (byte)0);
|
|
}
|
|
if (lookupKey != null) {
|
|
lookupKey.wipe();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Store derived KEK key in cache.
|
|
*
|
|
* @param password password used for key derivation
|
|
* @param kdfSalt PBKDF2 salt from the entry
|
|
* @param kdfIterations PBKDF2 iteration count from the entry
|
|
* @param derivedKey derived key to cache (KEK + HMAC key)
|
|
*/
|
|
private void cacheDerivedKey(char[] password, byte[] kdfSalt,
|
|
int kdfIterations, byte[] derivedKey) {
|
|
|
|
long expiryTime, now, ttl;
|
|
byte[] passHash = null;
|
|
byte[] cacheKeyBytes = null;
|
|
KekCacheEntry entry = null;
|
|
KekCacheEntry oldEntry = null;
|
|
ByteArrayWrapper mapKey = null;
|
|
|
|
/* Return if cache is disabled */
|
|
if (!isKekCacheEnabled()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
/* Compute password hash and cache key */
|
|
passHash = hashPassword(password);
|
|
cacheKeyBytes = generateCacheKey(passHash, kdfSalt, kdfIterations);
|
|
|
|
/* Calculate expiry time, checking for overflow */
|
|
now = System.currentTimeMillis();
|
|
ttl = getKekCacheTtlMs();
|
|
if (ttl > Long.MAX_VALUE - now) {
|
|
/* Overflow would occur - set to Long.MAX_VALUE */
|
|
expiryTime = Long.MAX_VALUE;
|
|
} else {
|
|
expiryTime = now + ttl;
|
|
}
|
|
entry = new KekCacheEntry(derivedKey, passHash, expiryTime);
|
|
mapKey = new ByteArrayWrapper(cacheKeyBytes);
|
|
|
|
synchronized (cacheLock) {
|
|
/* Check for old entry, remove/wipe if found */
|
|
oldEntry = kekCache.get(mapKey);
|
|
if (oldEntry != null) {
|
|
/* Find and wipe old key, then remove entry */
|
|
for (Map.Entry<ByteArrayWrapper, KekCacheEntry> mapEntry :
|
|
kekCache.entrySet()) {
|
|
if (mapEntry.getKey().equals(mapKey)) {
|
|
ByteArrayWrapper oldKey = mapEntry.getKey();
|
|
kekCache.remove(oldKey);
|
|
oldEntry.wipe();
|
|
oldKey.wipe();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/* Insert new entry */
|
|
kekCache.put(mapKey, entry);
|
|
}
|
|
|
|
log("Cached PBKDF2 derived key");
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
log("Error caching derived key: SHA-256 not available");
|
|
|
|
} finally {
|
|
if (passHash != null) {
|
|
Arrays.fill(passHash, (byte)0);
|
|
}
|
|
if (cacheKeyBytes != null) {
|
|
Arrays.fill(cacheKeyBytes, (byte)0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove specific cache entry on HMAC verification failure.
|
|
*
|
|
* @param password password used for key derivation
|
|
* @param kdfSalt PBKDF2 salt from the entry
|
|
* @param kdfIterations PBKDF2 iteration count from the entry
|
|
*/
|
|
private void invalidateCacheEntry(char[] password, byte[] kdfSalt,
|
|
int kdfIterations) {
|
|
|
|
byte[] passHash = null;
|
|
byte[] cacheKeyBytes = null;
|
|
ByteArrayWrapper lookupKey = null;
|
|
KekCacheEntry entryValue = null;
|
|
|
|
/* Do nothing if caching is disabled */
|
|
if (!isKekCacheEnabled()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
/* Compute password hash and cache key */
|
|
passHash = hashPassword(password);
|
|
cacheKeyBytes = generateCacheKey(passHash, kdfSalt, kdfIterations);
|
|
lookupKey = new ByteArrayWrapper(cacheKeyBytes);
|
|
|
|
synchronized (cacheLock) {
|
|
entryValue = kekCache.get(lookupKey);
|
|
|
|
if (entryValue != null) {
|
|
/* Wipe both key and value, then remove from cache */
|
|
kekCache.computeIfPresent(lookupKey, (key, value) -> {
|
|
value.wipe();
|
|
key.wipe();
|
|
return null; /* Remove entry */
|
|
});
|
|
|
|
log("Invalidated cache entry due to HMAC failure");
|
|
}
|
|
}
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
log("Error invalidating cache entry: SHA-256 not available");
|
|
|
|
} finally {
|
|
if (passHash != null) {
|
|
Arrays.fill(passHash, (byte)0);
|
|
}
|
|
if (cacheKeyBytes != null) {
|
|
Arrays.fill(cacheKeyBytes, (byte)0);
|
|
}
|
|
if (lookupKey != null) {
|
|
lookupKey.wipe();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear all entries from the KEK cache.
|
|
*/
|
|
private void clearKekCache() {
|
|
|
|
synchronized (cacheLock) {
|
|
int count = kekCache.size();
|
|
for (Map.Entry<ByteArrayWrapper, KekCacheEntry> entry :
|
|
kekCache.entrySet()) {
|
|
entry.getValue().wipe();
|
|
entry.getKey().wipe();
|
|
}
|
|
kekCache.clear();
|
|
|
|
if (count > 0) {
|
|
log("Cleared KEK cache (" + count + " entries wiped)");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return entry from internal map that matches alias and type.
|
|
*
|
|
* @param alias Alias for entry to retrieve
|
|
* @param type type of entry that should be returned, either
|
|
* EntryType.PRIVATE_KEY, EntryType.CERTIFICATE, or
|
|
* EntryType.SECRET_KEY
|
|
*
|
|
* @return entry Object if found, otherwise null if not found or entry
|
|
* for given alias does not match type requested
|
|
*/
|
|
private Object getEntryFromAlias(String alias, EntryType type) {
|
|
|
|
Object entry = null;
|
|
|
|
if (alias == null || alias.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
entry = entries.get(alias);
|
|
if (entry == null) {
|
|
return null;
|
|
}
|
|
|
|
switch (type) {
|
|
case PRIVATE_KEY:
|
|
if (entry instanceof WKSPrivateKey) {
|
|
return entry;
|
|
}
|
|
break;
|
|
case CERTIFICATE:
|
|
if (entry instanceof WKSCertificate) {
|
|
return entry;
|
|
}
|
|
break;
|
|
case SECRET_KEY:
|
|
if (entry instanceof WKSSecretKey) {
|
|
return entry;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Derive encryption and authentication keys from password using PBKDF2.
|
|
*
|
|
* @param pass password to use for key protection
|
|
* @param salt salt for PBKDF2 derivation
|
|
* @param iterations iterations for PBKDF2 derivation
|
|
* @param kLen key length to generate
|
|
*
|
|
* @return byte array continaing derived key of specified length
|
|
*
|
|
* @throws KeyStoreException on error deriving key
|
|
*/
|
|
private static byte[] deriveKeyFromPassword(char[] pass,
|
|
byte[] salt, int iterations, int kLen) throws KeyStoreException {
|
|
|
|
byte[] kek = null;
|
|
|
|
if (pass == null || pass.length == 0 || salt == null ||
|
|
salt.length == 0 || iterations <= 0 || kLen <= 0) {
|
|
throw new KeyStoreException(
|
|
"Invalid arguments when deriving key from password");
|
|
}
|
|
|
|
try {
|
|
kek = Pwdbased.PBKDF2(
|
|
WolfCryptSecretKeyFactory.passwordToByteArray(pass),
|
|
salt, iterations, kLen, WKS_PBKDF2_TYPE);
|
|
|
|
if (kek == null) {
|
|
throw new KeyStoreException(
|
|
"Error deriving key encryption key with PBKDF2");
|
|
}
|
|
|
|
} catch (WolfCryptException e) {
|
|
if (kek != null) {
|
|
Arrays.fill(kek, (byte)0);
|
|
}
|
|
throw new KeyStoreException(e);
|
|
}
|
|
|
|
return kek;
|
|
}
|
|
|
|
/**
|
|
* Generate HMAC over data using provided key.
|
|
*
|
|
* @param key HMAC key to be used
|
|
* @param data data to be used as input for HMAC
|
|
*
|
|
* @return generated HMAC value on success, null on error
|
|
*
|
|
* @throws KeyStoreException on error generating HMAC
|
|
*/
|
|
private static byte[] generateHmac(byte[] key, byte[] data)
|
|
throws KeyStoreException {
|
|
|
|
byte[] hmac = null;
|
|
SecretKeySpec spec = null;
|
|
Mac mac = null;
|
|
|
|
if (key == null || key.length == 0 ||
|
|
data == null || data.length == 0) {
|
|
throw new KeyStoreException(
|
|
"HMAC key or data null or zero length when generating");
|
|
}
|
|
|
|
try {
|
|
mac = Mac.getInstance("HmacSHA512", "wolfJCE");
|
|
spec = new SecretKeySpec(key, "SHA512");
|
|
|
|
/* Generate HMAC-SHA512 */
|
|
mac.init(spec);
|
|
mac.update(data);
|
|
hmac = mac.doFinal();
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new KeyStoreException(
|
|
"HmacSHA512 not available in wolfJCE Mac service", e);
|
|
|
|
} catch (NoSuchProviderException e) {
|
|
throw new KeyStoreException(
|
|
"WolfSSLKeyStore must currently use wolfJCE for " +
|
|
"HmacSHA512", e);
|
|
|
|
} catch (IllegalStateException e) {
|
|
throw new KeyStoreException(
|
|
"Error initializing Mac object", e);
|
|
|
|
} catch (InvalidKeyException e) {
|
|
throw new KeyStoreException(
|
|
"Invalid SecretKeySpec passed to Mac.init()");
|
|
|
|
} finally {
|
|
if (spec != null) {
|
|
try {
|
|
spec.destroy();
|
|
} catch (DestroyFailedException e) {
|
|
log("SecretKeySpec.destroy() failed in generateHmac()");
|
|
}
|
|
}
|
|
}
|
|
|
|
return hmac;
|
|
}
|
|
|
|
/**
|
|
* Encrypt plaintext key using AES-CBC.
|
|
*
|
|
* AES-CBC encryption uses Cipher.AES/CBC/PKCS5Padding mode.
|
|
*
|
|
* @param plainKey plaintext key to be encrypted/protected
|
|
* @param kek key encryption key, used to encrypt plaintext key
|
|
* @param pass password to use for key protection
|
|
* @param iv initialization vector (IV) for encryption operation
|
|
*
|
|
* @return byte array containing encrypted/protected key
|
|
*
|
|
* @throws KeyStoreException on error encrypting key
|
|
*/
|
|
private static byte[] encryptKey(byte[] plainKey, byte[] kek,
|
|
byte[] iv) throws KeyStoreException {
|
|
|
|
Cipher enc = null;
|
|
SecretKeySpec keySpec = null;
|
|
IvParameterSpec ivSpec = null;
|
|
byte[] encrypted = null;
|
|
|
|
if (plainKey == null || plainKey.length == 0 || kek == null ||
|
|
kek.length != WKS_ENC_KEY_LENGTH || iv == null ||
|
|
iv.length != Aes.BLOCK_SIZE) {
|
|
throw new KeyStoreException(
|
|
"Invalid arguments not allowed when encrypting key");
|
|
}
|
|
|
|
try {
|
|
try {
|
|
enc = Cipher.getInstance("AES/CBC/PKCS5Padding", "wolfJCE");
|
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
|
throw new KeyStoreException(
|
|
"AES/CBC/PKCS5Padding not available in wolfJCE Cipher", e);
|
|
|
|
} catch (NoSuchProviderException e) {
|
|
throw new KeyStoreException(
|
|
"WolfSSLKeyStore must currently use wolfJCE for AES", e);
|
|
}
|
|
|
|
keySpec = new SecretKeySpec(kek, "AES");
|
|
ivSpec = new IvParameterSpec(iv);
|
|
|
|
try {
|
|
enc.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
|
} catch (InvalidKeyException e) {
|
|
throw new KeyStoreException(
|
|
"Invalid AES key used for private key encryption", e);
|
|
} catch (InvalidAlgorithmParameterException e) {
|
|
throw new KeyStoreException(
|
|
"Invalid params used for private key encryption", e);
|
|
}
|
|
|
|
try {
|
|
encrypted = enc.doFinal(plainKey);
|
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
|
throw new KeyStoreException(
|
|
"Error encrypting private key with AES-CBC", e);
|
|
}
|
|
|
|
} finally {
|
|
if (keySpec != null) {
|
|
try {
|
|
keySpec.destroy();
|
|
} catch (DestroyFailedException e) {
|
|
log("SecretKeySpec.destroy() failed in encryptKey()");
|
|
}
|
|
}
|
|
}
|
|
|
|
return encrypted;
|
|
}
|
|
|
|
/**
|
|
* Decrypt protected key using AES-CBC, return original plaintext
|
|
* key as byte array.
|
|
*
|
|
* @param encKey encrypted/protected key as byte array
|
|
* @param kek key encryption key to decrypt with
|
|
* @param iv initialization vector (IV) for decryption operation
|
|
*
|
|
* @return unprotected plaintext key as byte array
|
|
*
|
|
* @throws KeyStoreException on error unprotecting/decrypting key
|
|
*/
|
|
private static byte[] decryptKey(byte[] encKey, byte[] kek, byte[] iv)
|
|
throws KeyStoreException {
|
|
|
|
Cipher dec = null;
|
|
SecretKeySpec keySpec = null;
|
|
IvParameterSpec ivSpec = null;
|
|
byte[] plain = null;
|
|
|
|
if (encKey == null || encKey.length == 0 || kek == null ||
|
|
kek.length == 0 || iv == null || iv.length == 0 ||
|
|
iv.length != Aes.BLOCK_SIZE) {
|
|
throw new KeyStoreException(
|
|
"Invalid arguments not allowed when decrypting key");
|
|
}
|
|
|
|
try {
|
|
/* Decrypt protected key with AES-CBC and KEK */
|
|
try {
|
|
dec = Cipher.getInstance("AES/CBC/PKCS5Padding", "wolfJCE");
|
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
|
throw new KeyStoreException(
|
|
"AES/CBC/PKCS5Padding not available in wolfJCE Cipher", e);
|
|
} catch (NoSuchProviderException e) {
|
|
throw new KeyStoreException(
|
|
"WolfSSLKeyStore must currently use wolfJCE for AES", e);
|
|
}
|
|
|
|
keySpec = new SecretKeySpec(kek, "AES");
|
|
ivSpec = new IvParameterSpec(iv);
|
|
|
|
try {
|
|
dec.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
|
} catch (InvalidKeyException e) {
|
|
throw new KeyStoreException(
|
|
"Invalid AES key used for private key decryption");
|
|
} catch (InvalidAlgorithmParameterException e) {
|
|
throw new KeyStoreException(
|
|
"Invalid AES-CBC parameters for private key decryption", e);
|
|
}
|
|
|
|
try {
|
|
/* Strips off padding internally, return is only plaintext */
|
|
plain = dec.doFinal(encKey);
|
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
|
if (plain != null) {
|
|
Arrays.fill(plain, (byte)0);
|
|
}
|
|
throw new KeyStoreException(
|
|
"Error decrypting private key with AES-CBC", e);
|
|
}
|
|
|
|
} finally {
|
|
if (keySpec != null) {
|
|
try {
|
|
keySpec.destroy();
|
|
} catch (DestroyFailedException e) {
|
|
log("SecretKeySpec.destroy() failed in decryptKey()");
|
|
}
|
|
}
|
|
}
|
|
|
|
return plain;
|
|
}
|
|
|
|
/**
|
|
* Return the key associated with the given alias, using the provided
|
|
* password to decrypt it.
|
|
*
|
|
* In order for a key to be returned it must have been associated with
|
|
* the alias through a call to setKeyEntry() with a PrivateKey or
|
|
* SecretKey object.
|
|
*
|
|
* @param alias alias for which to return the associated key
|
|
* @param password password used to decrypt key
|
|
*
|
|
* @return the requested Key, or null if the alias does not exist or does
|
|
* not match a key entry.
|
|
*
|
|
* @throws NoSuchAlgorithmException if the algorithm for recovering the
|
|
* key cannot be found
|
|
* @throws UnrecoverableKeyException if the key cannot be recovered
|
|
*/
|
|
@Override
|
|
public Key engineGetKey(String alias, char[] password)
|
|
throws NoSuchAlgorithmException, UnrecoverableKeyException {
|
|
|
|
int algoId = 0;
|
|
byte[] plainKey = null;
|
|
Object entry = null;
|
|
|
|
PrivateKey pKey = null;
|
|
PKCS8EncodedKeySpec p8Spec = null;
|
|
KeyFactory keyFact = null;
|
|
|
|
SecretKey sKey = null;
|
|
|
|
log("returning Key entry for alias: " + alias);
|
|
|
|
entry = getEntryFromAlias(alias, EntryType.PRIVATE_KEY);
|
|
if (entry == null) {
|
|
entry = getEntryFromAlias(alias, EntryType.SECRET_KEY);
|
|
if (entry == null) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (password == null || password.length == 0) {
|
|
throw new UnrecoverableKeyException("Password cannot be null");
|
|
}
|
|
|
|
try {
|
|
if (entry instanceof WKSPrivateKey) {
|
|
plainKey = ((WKSPrivateKey)entry).getDecryptedKey(
|
|
password, this);
|
|
|
|
p8Spec = new PKCS8EncodedKeySpec(plainKey);
|
|
|
|
try {
|
|
algoId = Asn.getPkcs8AlgoID(plainKey);
|
|
} catch (WolfCryptException wce) {
|
|
/* Reset to zero for possible ML-DSA PKCS#8 fallback below.
|
|
* Older wolfSSL can throw ASN_PARSE_E from
|
|
* ToTraditional_ex() */
|
|
algoId = 0;
|
|
}
|
|
|
|
/* Prefer wolfJCE KeyFactory by name for RSA/EC, fall back to
|
|
* default Provider lookup in builds where wolfJCE does not
|
|
* register that KeyFactory (e.g. KeyFactory.RSA when
|
|
* !WOLFSSL_PUBLIC_MP). No KeyFactory.RSASSA-PSS in wolfJCE
|
|
* yet, so PSS keeps default lookup. */
|
|
if (algoId == Asn.RSAk) {
|
|
keyFact = WolfCryptUtil.getKeyFactoryPreferWolfJCE("RSA");
|
|
} else if (algoId == Asn.RSAPSSk) {
|
|
keyFact = KeyFactory.getInstance("RSASSA-PSS");
|
|
} else if (algoId == Asn.ECDSAk) {
|
|
keyFact = WolfCryptUtil.getKeyFactoryPreferWolfJCE("EC");
|
|
} else if (algoId != 0 &&
|
|
(algoId == Asn.ML_DSA_LEVEL2k ||
|
|
algoId == Asn.ML_DSA_LEVEL3k ||
|
|
algoId == Asn.ML_DSA_LEVEL5k)) {
|
|
keyFact = getMlDsaKeyFactory();
|
|
} else if (algoId == 0 && FeatureDetect.MlDsaEnabled()) {
|
|
/* Fallback for older native wolfSSL, getPkcs8AlgoID
|
|
* returned 0 because native ToTraditional_ex() did not
|
|
* recognize ML-DSA OIDs. Let the ML-DSA KeyFactory
|
|
* validate, generatePrivate() below throws for
|
|
* non-ML-DSA DER (avoids a redundant pre-parse of
|
|
* the same PKCS#8). */
|
|
log("using ML-DSA fallback for older native wolfSSL");
|
|
keyFact = getMlDsaKeyFactory();
|
|
} else {
|
|
throw new NoSuchAlgorithmException(
|
|
"Only RSA, RSASSA-PSS, EC, and ML-DSA private " +
|
|
"key encoding supported: " + algoId);
|
|
}
|
|
|
|
try {
|
|
pKey = keyFact.generatePrivate(p8Spec);
|
|
if (pKey == null) {
|
|
throw new UnrecoverableKeyException(
|
|
"Error generating PrivateKey from " +
|
|
"PKCS8EncodedKeySpec");
|
|
}
|
|
} catch (InvalidKeySpecException e) {
|
|
throw new UnrecoverableKeyException(
|
|
"Invalid key spec for KeyFactory");
|
|
}
|
|
}
|
|
else if (entry instanceof WKSSecretKey) {
|
|
WKSSecretKey sk = (WKSSecretKey)entry;
|
|
|
|
plainKey = sk.getDecryptedKey(password, this);
|
|
|
|
sKey = new SecretKeySpec(plainKey, sk.keyAlgo);
|
|
}
|
|
|
|
} catch (WolfCryptException wce) {
|
|
throw new UnrecoverableKeyException("Error getting key: " + wce);
|
|
} finally {
|
|
if (plainKey != null) {
|
|
Arrays.fill(plainKey, (byte)0);
|
|
}
|
|
}
|
|
|
|
if (entry instanceof WKSPrivateKey) {
|
|
return (Key)pKey;
|
|
}
|
|
else if (entry instanceof WKSSecretKey) {
|
|
return (Key)sKey;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the certificate chain associated with the provided alias.
|
|
*
|
|
* The certificate chain returned must have been associated with the
|
|
* alias through a call to setKeyEntry() with a PrivateKey object.
|
|
*
|
|
* @param alias the alias for which to return the matching cert chain
|
|
*
|
|
* @return the certificate chain, ordered with the user/peer certificate
|
|
* first then going up to the root CA last. null if the alias
|
|
* does not exist or does not contain a certificate chain.
|
|
*/
|
|
@Override
|
|
public Certificate[] engineGetCertificateChain(String alias) {
|
|
|
|
Object entry = null;
|
|
|
|
log("returning Certificate[] for alias: " + alias);
|
|
|
|
entry = entries.get(alias);
|
|
if ((entry != null) && (entry instanceof WKSPrivateKey)) {
|
|
return ((WKSPrivateKey)entry).chain.clone();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return the certificate associated with the provided alias.
|
|
*
|
|
* If the stored certificate was associated with the alias using a call
|
|
* to setCertificateEntry() then the trusted certificate contained in
|
|
* the entry is returned.
|
|
*
|
|
* If the given alias contains a private key entry which was created
|
|
* with a call to setKeyEntry(), the first certificate in the chain
|
|
* used to create that key entry is returned (if the chain exists).
|
|
*
|
|
* @param alias the alias for which to return the matching certificate
|
|
*
|
|
* @return the certificate, or null if the alias does not exist or
|
|
* does not match any entries.
|
|
*/
|
|
@Override
|
|
public Certificate engineGetCertificate(String alias) {
|
|
|
|
Object entry = null;
|
|
|
|
log("returning Certificate for alias: " + alias);
|
|
|
|
entry = entries.get(alias);
|
|
if (entry != null) {
|
|
if (entry instanceof WKSCertificate) {
|
|
return ((WKSCertificate)entry).cert;
|
|
}
|
|
else if (entry instanceof WKSPrivateKey) {
|
|
WKSPrivateKey key = (WKSPrivateKey)entry;
|
|
if (key.chain != null && key.chain.length > 0) {
|
|
return key.chain[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return the creation date of the entry matching the provided alias.
|
|
*
|
|
* @param alias the alias used to find matching entry
|
|
*
|
|
* @return the creation date of the entry matching alias, or null if the
|
|
* alias does not exist.
|
|
*/
|
|
@Override
|
|
public Date engineGetCreationDate(String alias) {
|
|
|
|
Object entry = null;
|
|
|
|
log("returning creation date for entry at alias: " + alias);
|
|
|
|
entry = entries.get(alias);
|
|
if (entry != null) {
|
|
if (entry instanceof WKSCertificate) {
|
|
return ((WKSCertificate)entry).creationDate;
|
|
}
|
|
else if (entry instanceof WKSPrivateKey) {
|
|
return ((WKSPrivateKey)entry).creationDate;
|
|
}
|
|
else if (entry instanceof WKSSecretKey) {
|
|
return ((WKSSecretKey)entry).creationDate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Internal method to check if a Key object is supported by this KeyStore
|
|
* for storing into an alias.
|
|
*
|
|
* 1. Key must be PrivateKey or SecretKey
|
|
* 2. If PrivateKey object:
|
|
* a. Must be of format "PKCS#8"
|
|
* b. Must support encoding (.getEncoded())
|
|
* 3. If SecretKey object:
|
|
* a. Must by of format "RAW"
|
|
* b. Must support encoding (.getEncoded())
|
|
*
|
|
* @param key Key object to check if supported
|
|
*
|
|
* @throws KeyStoreException if Key object is not supported
|
|
*/
|
|
private void checkKeyIsSupported(Key key) throws KeyStoreException {
|
|
|
|
if (key == null) {
|
|
throw new KeyStoreException("Input key is null");
|
|
}
|
|
|
|
if (key instanceof PrivateKey) {
|
|
if (!key.getFormat().equals("PKCS#8")) {
|
|
throw new KeyStoreException("Only PKCS#8 format PrivateKeys " +
|
|
"are supported");
|
|
}
|
|
if (key.getEncoded() == null) {
|
|
throw new KeyStoreException("Key does not support encoding");
|
|
}
|
|
}
|
|
else if (key instanceof SecretKey) {
|
|
if (!key.getFormat().equals("RAW")) {
|
|
/* SecretKey should always be of format "RAW", double check */
|
|
throw new KeyStoreException("Only RAW format SecretKeys " +
|
|
"are supported");
|
|
}
|
|
if (key.getEncoded() == null) {
|
|
throw new KeyStoreException("Key does not support encoding");
|
|
}
|
|
}
|
|
else {
|
|
throw new KeyStoreException("Key must be of type PrivateKey " +
|
|
"or SecretKey, unsupported type");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to check that this is a supported Certificate chain.
|
|
*
|
|
* Current checks include:
|
|
* 1. Chain is not null or a zero length array
|
|
* 2. Chain is made up of X509Certificate objects
|
|
* 3. Chain cert signatures are correct as we walk up the chain
|
|
*
|
|
* The certificate chain should be ordered from leaf cert (entity) to
|
|
* top-most intermedate certificate.
|
|
*
|
|
* @param chain Certificate chain to check
|
|
*
|
|
* @throws KeyStoreException if Certificate array is not supported
|
|
*/
|
|
private void checkCertificateChain(Certificate[] chain)
|
|
throws KeyStoreException {
|
|
|
|
int i = 0;
|
|
byte[] encodedCert = null;
|
|
|
|
if (chain == null || chain.length == 0) {
|
|
throw new KeyStoreException("Certificate chain must not " +
|
|
"be null or empty when storing PrivateKey");
|
|
}
|
|
|
|
for (Certificate cert : chain) {
|
|
if (!(cert instanceof X509Certificate)) {
|
|
throw new KeyStoreException("Certificate chain objects must " +
|
|
"be of type X509Certificate");
|
|
}
|
|
}
|
|
|
|
if (chain.length > 1) {
|
|
/* Use wolfSSL CertManager to verify chain cert signatures match */
|
|
WolfSSLCertManager cm = new WolfSSLCertManager();
|
|
|
|
/* Load first chain cert as trusted (we don't have the
|
|
* root CA available to verify full chain at this point */
|
|
try {
|
|
encodedCert = chain[chain.length-1].getEncoded();
|
|
cm.CertManagerLoadCABuffer(encodedCert, encodedCert.length,
|
|
WolfCrypt.SSL_FILETYPE_ASN1);
|
|
} catch (WolfCryptException | CertificateEncodingException e) {
|
|
cm.free();
|
|
throw new KeyStoreException(
|
|
"Error checking cert chain integrity, loading " +
|
|
"chain[" + chain.length + "]");
|
|
}
|
|
|
|
try {
|
|
for (i = chain.length-2; i > 0; i--) {
|
|
encodedCert = chain[i].getEncoded();
|
|
/* Verify chain cert first against loaded CAs */
|
|
cm.CertManagerVerifyBuffer(encodedCert, encodedCert.length,
|
|
WolfCrypt.SSL_FILETYPE_ASN1);
|
|
|
|
if (i > 0) {
|
|
/* If verification passes, load as trusted */
|
|
cm.CertManagerLoadCABuffer(encodedCert,
|
|
encodedCert.length,
|
|
WolfCrypt.SSL_FILETYPE_ASN1);
|
|
}
|
|
}
|
|
} catch (WolfCryptException | CertificateEncodingException e) {
|
|
cm.free();
|
|
throw new KeyStoreException(
|
|
"Certificate chain invalid", e);
|
|
}
|
|
|
|
cm.free();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to check that an X509Certificate matches the provided
|
|
* private key.
|
|
*
|
|
* @param cert X.509 certificate to check, which should match PrivateKey
|
|
* @param key PrivateKey to check against certificate
|
|
*
|
|
* @throws KeyStoreException if leaf cert does not match private key
|
|
*/
|
|
private void checkCertificateChainMatchesPrivateKey(
|
|
X509Certificate cert, PrivateKey key) throws KeyStoreException {
|
|
|
|
boolean match = false;
|
|
byte[] derCert = null;
|
|
byte[] pkcs8Key = null;
|
|
|
|
if (cert == null || key == null) {
|
|
throw new KeyStoreException("Certificate or PrivateKey is null");
|
|
}
|
|
|
|
try {
|
|
derCert = cert.getEncoded();
|
|
if (derCert == null || derCert.length == 0) {
|
|
throw new KeyStoreException("Bad X509Certificate DER encoding");
|
|
}
|
|
} catch (CertificateEncodingException e) {
|
|
throw new KeyStoreException(e);
|
|
}
|
|
|
|
if (!key.getFormat().equals("PKCS#8") &&
|
|
!key.getFormat().equals("PKCS8")) {
|
|
throw new KeyStoreException("PrivateKey encoding not type PKCS#8");
|
|
}
|
|
|
|
pkcs8Key = key.getEncoded();
|
|
if (pkcs8Key == null || pkcs8Key.length == 0) {
|
|
throw new KeyStoreException("Bad PrivateKey PKCS#8 encoding");
|
|
}
|
|
try {
|
|
match = X509CheckPrivateKey(derCert, pkcs8Key);
|
|
if (!match && isMlDsaAlgorithmName(key.getAlgorithm())) {
|
|
/* Older native wolfSSL versions could return NULL from
|
|
* EVP_PKCS82PKEY(), causing X509CheckPrivateKey() to return
|
|
* false even when cert and key do match. Fall back to Java
|
|
* match using MlDsa JNI (derive SPKI from private key and
|
|
* compare to the cert encoded SubjectPublicKeyInfo. */
|
|
log("X509CheckPrivateKey returned false for ML-DSA key, " +
|
|
"trying Java-side match");
|
|
match = mlDsaCertMatchesPrivateKey(cert, pkcs8Key);
|
|
}
|
|
if (!match) {
|
|
throw new KeyStoreException("X509Certificate does not match " +
|
|
"provided private key");
|
|
}
|
|
} finally {
|
|
Arrays.fill(pkcs8Key, (byte)0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Java fallback for cert/private key match when native
|
|
* {@code X509CheckPrivateKey} can't handle ML-DSA (older native wolfSSL,
|
|
* wolfSSL_EVP_PKCS82PKEY() returns NULL for ML-DSA OIDs).
|
|
*
|
|
* <p>Imports the PKCS#8 private into a wolfJCE MlDsa, exports the derived
|
|
* public as X.509 SubjectPublicKeyInfo DER, and compares bytes against the
|
|
* cert's public key encoding. ML-DSA SPKI DER is canonical, so byte
|
|
* equality is safe.</p>
|
|
*/
|
|
private boolean mlDsaCertMatchesPrivateKey(X509Certificate cert,
|
|
byte[] pkcs8Key) {
|
|
|
|
PublicKey certPub = null;
|
|
MlDsa key = null;
|
|
byte[] certSpki = null;
|
|
byte[] derivedSpki = null;
|
|
|
|
if (!FeatureDetect.MlDsaEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
certPub = cert.getPublicKey();
|
|
if (certPub == null ||
|
|
!isMlDsaAlgorithmName(certPub.getAlgorithm())) {
|
|
return false;
|
|
}
|
|
|
|
certSpki = certPub.getEncoded();
|
|
if (certSpki == null) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
try {
|
|
/* Level auto-detected from PKCS#8 DER. */
|
|
key = new MlDsa();
|
|
key.importPrivateKeyDer(pkcs8Key);
|
|
}
|
|
catch (WolfCryptException e) {
|
|
/* Older native wolfSSL pre PR 10310 without auto-detect.
|
|
* Derive level explicitly (try each FIPS 204 level internally),
|
|
* then import with that level. */
|
|
if (key != null) {
|
|
key.releaseNativeStruct();
|
|
key = null;
|
|
}
|
|
key = new MlDsa(
|
|
MlDsa.parseAndValidateMlDsaPrivateKeyDer(pkcs8Key));
|
|
key.importPrivateKeyDer(pkcs8Key);
|
|
}
|
|
derivedSpki = key.exportPublicKeyDer(true);
|
|
return Arrays.equals(certSpki, derivedSpki);
|
|
}
|
|
catch (WolfCryptException e) {
|
|
return false;
|
|
}
|
|
finally {
|
|
if (key != null) {
|
|
key.releaseNativeStruct();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a JCA algorithm name identifies ML-DSA.
|
|
*
|
|
* Accepts the family name, the FIPS 204 parameter-set names, and the
|
|
* raw OID strings that pre-JDK-24 CertificateFactory implementations
|
|
* return from getAlgorithm() for algorithms they do not recognize.
|
|
*
|
|
* @param algo algorithm name from Key.getAlgorithm(), may be null
|
|
*
|
|
* @return true if the name identifies an ML-DSA key, otherwise false
|
|
*/
|
|
private static boolean isMlDsaAlgorithmName(String algo) {
|
|
|
|
if (algo == null) {
|
|
return false;
|
|
}
|
|
|
|
return algo.equalsIgnoreCase("ML-DSA") ||
|
|
algo.equalsIgnoreCase("ML-DSA-44") ||
|
|
algo.equalsIgnoreCase("ML-DSA-65") ||
|
|
algo.equalsIgnoreCase("ML-DSA-87") ||
|
|
algo.equals("2.16.840.1.101.3.4.3.17") ||
|
|
algo.equals("2.16.840.1.101.3.4.3.18") ||
|
|
algo.equals("2.16.840.1.101.3.4.3.19");
|
|
}
|
|
|
|
/**
|
|
* Get a KeyFactory for ML-DSA, preferring the wolfJCE provider.
|
|
*
|
|
* ML-DSA PKCS#8 produced by native wolfCrypt wc_Dilithium_KeyToDer()
|
|
* currently uses a version 1 encoding with a bundled publicKey
|
|
* attribute, which the JDK 24+ SunJCE strict PKCS#8 parser rejects
|
|
* ("publicKey seen in v1"). A generic KeyFactory.getInstance() lookup
|
|
* can therefore resolve to a provider unable to parse keys stored by
|
|
* this KeyStore when wolfJCE is not the highest-priority provider.
|
|
* Prefer wolfJCE explicitly and fall back to a generic lookup when
|
|
* wolfJCE is not registered by name in the Security provider list.
|
|
*
|
|
* @return KeyFactory for ML-DSA
|
|
*
|
|
* @throws NoSuchAlgorithmException if no provider supports ML-DSA
|
|
*/
|
|
private static KeyFactory getMlDsaKeyFactory()
|
|
throws NoSuchAlgorithmException {
|
|
|
|
try {
|
|
return KeyFactory.getInstance("ML-DSA", "wolfJCE");
|
|
} catch (NoSuchProviderException | NoSuchAlgorithmException e) {
|
|
/* wolfJCE not registered by name (or built without ML-DSA) */
|
|
return KeyFactory.getInstance("ML-DSA");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assign the given key to an alias and protects it using the
|
|
* provided password.
|
|
*
|
|
* If the key is of type java.security.PrivateKey, it must be accompanied
|
|
* by a certificate chain which includes the corresponding public key.
|
|
*
|
|
* If the key is of type javax.crypto.SecretKey, no certificate chain
|
|
* should be provided.
|
|
*
|
|
* If the alias already exists, the existing entry is overwritten
|
|
* with the provided key (and cert chain if applicable).
|
|
*
|
|
* @param alias the alias name to associate and store
|
|
* @param key the key to be associated with alias
|
|
* @param password the password used to protect the key. Password cannot
|
|
* be null, but can be empty array. If wolfCrypt FIPS is used,
|
|
* this will cause an error since the minimum HMAC key length is
|
|
* 14, meaning passwords must be at least 14 characters for use
|
|
* with this KeyStore and wolfCrypt FIPS.
|
|
* @param chain the cert chain for the corresponding public key - only
|
|
* required if the key is of type java.security.PrivateKey
|
|
*
|
|
* @throws KeyStoreException if the key cannot be protected or the
|
|
* operation fails.
|
|
*/
|
|
@Override
|
|
public synchronized void engineSetKeyEntry(String alias, Key key,
|
|
char[] password, Certificate[] chain) throws KeyStoreException {
|
|
|
|
byte[] encodedKey = null;
|
|
WKSPrivateKey privKey = null;
|
|
WKSSecretKey secretKey = null;
|
|
Object existingEntry = null;
|
|
|
|
if (alias == null) {
|
|
throw new KeyStoreException("Alias cannot be null");
|
|
}
|
|
|
|
if (key == null) {
|
|
throw new KeyStoreException("Key cannot be null");
|
|
}
|
|
|
|
if (password == null) {
|
|
throw new KeyStoreException("Password cannot be null");
|
|
}
|
|
|
|
checkKeyIsSupported(key);
|
|
|
|
/* Clear old KEK cache entry if we will overwrite one */
|
|
existingEntry = entries.get(alias);
|
|
if (existingEntry != null) {
|
|
if (existingEntry instanceof WKSPrivateKey ||
|
|
existingEntry instanceof WKSSecretKey) {
|
|
clearKekCache();
|
|
}
|
|
}
|
|
|
|
/* PKCS#8 private key (PrivateKey) or raw key bytes (SecretKey) */
|
|
encodedKey = key.getEncoded();
|
|
if (encodedKey == null || encodedKey.length == 0) {
|
|
throw new KeyStoreException(
|
|
"Error getting encoded key bytes from Key");
|
|
}
|
|
|
|
try {
|
|
if (key instanceof PrivateKey) {
|
|
log("inserting PrivateKey at alias: " + alias);
|
|
|
|
/* Sanity check on cert chain, chain is required */
|
|
checkCertificateChain(chain);
|
|
|
|
/* Verify private key matches leaf cert */
|
|
checkCertificateChainMatchesPrivateKey(
|
|
(X509Certificate)chain[0], (PrivateKey)key);
|
|
|
|
/* Protect key and store inside new WKSPrivateKey object,
|
|
* throws KeyStoreException on error */
|
|
privKey = new WKSPrivateKey(encodedKey, password,
|
|
chain, this.rand);
|
|
|
|
/* Store entry into map */
|
|
entries.put(alias, privKey);
|
|
}
|
|
else if (key instanceof SecretKey) {
|
|
log("inserting SecretKey at alias: " + alias);
|
|
|
|
/* Protect secret key inside WKSSecretKey object */
|
|
secretKey = new WKSSecretKey(encodedKey, password,
|
|
key.getAlgorithm(), this.rand);
|
|
|
|
/* Store entry into map */
|
|
entries.put(alias, secretKey);
|
|
}
|
|
|
|
} finally {
|
|
/* Zero out encoded key array */
|
|
Arrays.fill(encodedKey, (byte)0);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Assign the given key to the provided alias, where the key has already
|
|
* been protected.
|
|
*
|
|
* This method is not supported by this KeyStore implementation since
|
|
* key protection method would not normally be known/used by external
|
|
* parties without using this KeyStore.
|
|
*
|
|
* @param alias the alias name to associate and store
|
|
* @param key the key to be associated with the alias, already in
|
|
* protected format.
|
|
* @param chain the cert chain for the corresponding public key - only
|
|
* required if the key is of type java.security.PrivateKey
|
|
*
|
|
* @throws KeyStoreException if the operation fails
|
|
*/
|
|
@Override
|
|
public synchronized void engineSetKeyEntry(String alias, byte[] key,
|
|
Certificate[] chain) throws KeyStoreException {
|
|
|
|
throw new UnsupportedOperationException(
|
|
"WolfSSLKeyStore does not support storing already protected keys");
|
|
}
|
|
|
|
/**
|
|
* Assign a certificate to the provided alias.
|
|
*
|
|
* If the alias already holds an existing entry created by
|
|
* setCertificateEntry() that trusted certificate is overwritten.
|
|
*
|
|
* If the alias already holds an existing entry which is a private key,
|
|
* a KeyStoreException will be thrown since this method cannot overwrite
|
|
* a private key entry.
|
|
*
|
|
* @param alias the alias name to map and store this certificate into
|
|
* @param cert the certificate to store and associate with alias
|
|
*
|
|
* @throws KeyStoreException if the alias alreday exists and does not
|
|
* identify an entry containing a trusted certificate, or this
|
|
* method fails.
|
|
*/
|
|
@Override
|
|
public synchronized void engineSetCertificateEntry(String alias,
|
|
Certificate cert) throws KeyStoreException {
|
|
|
|
Object entry = entries.get(alias);
|
|
if (entry instanceof WKSPrivateKey) {
|
|
throw new KeyStoreException("Cannot overwrite private key entry");
|
|
}
|
|
|
|
log("inserting Certificate at alias: " + alias);
|
|
|
|
WKSCertificate obj = new WKSCertificate();
|
|
obj.cert = cert;
|
|
obj.creationDate = new Date();
|
|
|
|
entries.put(alias, obj);
|
|
}
|
|
|
|
/**
|
|
* Delete the entry associated with the provided alias.
|
|
*
|
|
* @param alias the alias used to delete matching entry
|
|
*
|
|
* @throws KeyStoreException if the operation fails
|
|
*/
|
|
@Override
|
|
public synchronized void engineDeleteEntry(String alias)
|
|
throws KeyStoreException {
|
|
|
|
Object entry = null;
|
|
|
|
log("deleting entry at alias: " + alias);
|
|
|
|
entry = entries.get(alias);
|
|
if (entry != null) {
|
|
if (entry instanceof WKSPrivateKey ||
|
|
entry instanceof WKSSecretKey) {
|
|
clearKekCache();
|
|
}
|
|
}
|
|
|
|
entries.remove(alias);
|
|
}
|
|
|
|
/**
|
|
* Return enumeration of all alias names in this KeyStore.
|
|
*
|
|
* @return enumeration of all aliases
|
|
*/
|
|
@Override
|
|
public Enumeration<String> engineAliases() {
|
|
|
|
log("returning all alias names in KeyStore");
|
|
|
|
return entries.keys();
|
|
}
|
|
|
|
/**
|
|
* Check if an alias is in this KeyStore.
|
|
*
|
|
* @param alias the alias name to check
|
|
*
|
|
* @return true if alias is in KeyStore, otherwise false
|
|
*/
|
|
@Override
|
|
public boolean engineContainsAlias(String alias) {
|
|
|
|
log("checking if KeyStore contains alias: " + alias);
|
|
|
|
return entries.containsKey(alias);
|
|
}
|
|
|
|
/**
|
|
* Return the total number of entries in this KeyStore.
|
|
*
|
|
* @return number of entries
|
|
*/
|
|
@Override
|
|
public int engineSize() {
|
|
|
|
log("returning size of KeyStore: " + entries.size());
|
|
|
|
return entries.size();
|
|
}
|
|
|
|
/**
|
|
* Check if entry associated with alias is a private key entry.
|
|
*
|
|
* Checks if the alias was created by a call to setKeyEntry() with
|
|
* the key object of either PrivateKey or SecretKey.
|
|
*
|
|
* @param alias the alias to check
|
|
*
|
|
* @return true if entry is a key, otherwise false if not a
|
|
* private key entry or alias does not exist
|
|
*/
|
|
@Override
|
|
public boolean engineIsKeyEntry(String alias) {
|
|
|
|
Object entry;
|
|
boolean isKey = false;
|
|
|
|
entry = entries.get(alias);
|
|
if ((entry != null) &&
|
|
(entry instanceof WKSPrivateKey ||
|
|
entry instanceof WKSSecretKey)) {
|
|
isKey = true;
|
|
}
|
|
else {
|
|
isKey = false;
|
|
}
|
|
|
|
log("checking if alias (" + alias + ") is key: " + isKey);
|
|
|
|
return isKey;
|
|
}
|
|
|
|
/**
|
|
* Check if entry associated with alias is a certificate entry.
|
|
*
|
|
* Checks if the alias was created by a call to setCertificateEntry().
|
|
*
|
|
* @param alias the alias to check
|
|
*
|
|
* @return true if entry is a certificate, otherwise false if not a
|
|
* certificate entry or alias does not exist
|
|
*/
|
|
@Override
|
|
public boolean engineIsCertificateEntry(String alias) {
|
|
|
|
Object entry = null;
|
|
boolean isCert = false;
|
|
|
|
entry = entries.get(alias);
|
|
if ((entry != null) && (entry instanceof WKSCertificate)) {
|
|
isCert = true;
|
|
}
|
|
else {
|
|
isCert = false;
|
|
}
|
|
|
|
log("checking if alias (" + alias + ") is certificate: " + isCert);
|
|
|
|
return isCert;
|
|
}
|
|
|
|
/**
|
|
* Return the alias name of the first KeyStore entry that matches the
|
|
* given certificate.
|
|
*
|
|
* If a KeyStore entry was created with setCertificateEntry(), the provided
|
|
* certificate is compared to that entry's certificate.
|
|
*
|
|
* If a KeyStore entry was created with setKeyEntry(), then the certificate
|
|
* provided is compared to the first element of the certificate chain
|
|
* in the key entry's chain.
|
|
*
|
|
* @param cert the certificate to use for matching
|
|
*
|
|
* @return the alias name of the first entry that matches the provided
|
|
* certificate, or null if no entry is found.
|
|
*/
|
|
@Override
|
|
public String engineGetCertificateAlias(Certificate cert) {
|
|
|
|
Certificate tmp = null;
|
|
|
|
if (cert == null) {
|
|
return null;
|
|
}
|
|
|
|
for (Map.Entry<String, Object> entry : entries.entrySet()) {
|
|
if (entry.getValue() instanceof WKSCertificate) {
|
|
tmp = ((WKSCertificate)entry.getValue()).cert;
|
|
}
|
|
else if ((entry.getValue() instanceof WKSPrivateKey) &&
|
|
(((WKSPrivateKey)entry.getValue()).chain != null)) {
|
|
tmp = ((WKSPrivateKey)entry.getValue()).chain[0];
|
|
}
|
|
|
|
if ((tmp != null) && tmp.equals(cert)) {
|
|
return entry.getKey();
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Store this KeyStore into the provided OutputStream, protecting the
|
|
* KeyStore integrity with the given password.
|
|
*
|
|
* KeyStore integrity is protected with PBKDF2-HMAC-SHA512 and HMAC-SHA512.
|
|
*
|
|
* @param stream OutputStream to write this KeyStore to
|
|
* @param password password used to generate the keystore integrity check
|
|
*
|
|
* @throws IOException on I/O problem
|
|
* @throws NoSuchAlgorithmException if integrity algorithm can't be
|
|
* found
|
|
* @throws CertificateException if any of the certificates in this
|
|
* KeyStore could not be stored
|
|
*/
|
|
@Override
|
|
public synchronized void engineStore(OutputStream stream, char[] password)
|
|
throws IOException, NoSuchAlgorithmException, CertificateException {
|
|
|
|
byte[] encoded = null;
|
|
byte[] derivedKey = null;
|
|
byte[] hmac = null;
|
|
byte[] encodedEntry = null;
|
|
byte[] salt = new byte[WKS_PBKDF2_SALT_SIZE];
|
|
Mac mac = null;
|
|
SecretKeySpec keySpec = null;
|
|
ByteArrayOutputStream bos = null;
|
|
DataOutputStream dos = null;
|
|
WKSPrivateKey keyEntry = null;
|
|
WKSSecretKey sKeyEntry = null;
|
|
WKSCertificate certEntry = null;
|
|
|
|
if (stream == null || password == null || password.length == 0) {
|
|
throw new IllegalArgumentException(
|
|
"OutputStream and password cannot be null");
|
|
}
|
|
|
|
log("storing KeyStore to OutputStream");
|
|
|
|
try {
|
|
bos = new ByteArrayOutputStream();
|
|
dos = new DataOutputStream(bos);
|
|
|
|
/* magic number */
|
|
dos.writeInt(WKS_MAGIC_NUMBER);
|
|
|
|
/* keystore version */
|
|
log("KeyStore version: " + WKS_STORE_VERSION);
|
|
dos.writeInt(WKS_STORE_VERSION);
|
|
|
|
/* entry count */
|
|
log("KeyStore entry count: " + entries.size());
|
|
dos.writeInt(entries.size());
|
|
|
|
/* write out entries */
|
|
for (Map.Entry<String, Object> entry : entries.entrySet()) {
|
|
if (entry.getValue() instanceof WKSPrivateKey) {
|
|
keyEntry = (WKSPrivateKey)entry.getValue();
|
|
|
|
log("storing PrivateKey: " + entry.getKey());
|
|
|
|
/* entry ID */
|
|
dos.writeInt(WKS_ENTRY_ID_PRIVATE_KEY);
|
|
|
|
/* alias */
|
|
dos.writeUTF(entry.getKey());
|
|
|
|
/* encoded WKSPrivateKey length and bytes */
|
|
encodedEntry = keyEntry.getEncoded(true);
|
|
dos.writeInt(encodedEntry.length);
|
|
dos.write(encodedEntry);
|
|
Arrays.fill(encodedEntry, (byte)0);
|
|
}
|
|
else if (entry.getValue() instanceof WKSCertificate) {
|
|
certEntry = (WKSCertificate)entry.getValue();
|
|
|
|
log("storing Certificate: " + entry.getKey());
|
|
|
|
/* entry ID */
|
|
dos.writeInt(WKS_ENTRY_ID_CERTIFICATE);
|
|
|
|
/* alias */
|
|
dos.writeUTF(entry.getKey());
|
|
|
|
/* encoded WKSCertificate length and bytes */
|
|
encodedEntry = certEntry.getEncoded();
|
|
dos.writeInt(encodedEntry.length);
|
|
dos.write(encodedEntry);
|
|
Arrays.fill(encodedEntry, (byte)0);
|
|
}
|
|
else if (entry.getValue() instanceof WKSSecretKey) {
|
|
sKeyEntry = (WKSSecretKey)entry.getValue();
|
|
|
|
log("storing SecretKey: " + entry.getKey());
|
|
|
|
/* entry ID */
|
|
dos.writeInt(WKS_ENTRY_ID_SECRET_KEY);
|
|
|
|
/* alias */
|
|
dos.writeUTF(entry.getKey());
|
|
|
|
/* encoded WKSSecretKey length and bytes */
|
|
encodedEntry = sKeyEntry.getEncoded(true);
|
|
dos.writeInt(encodedEntry.length);
|
|
dos.write(encodedEntry);
|
|
Arrays.fill(encodedEntry, (byte)0);
|
|
}
|
|
else {
|
|
throw new IOException(
|
|
"Encountered unsupported entry type when " +
|
|
"storing KeyStore");
|
|
}
|
|
}
|
|
|
|
/* Generate random PBKDF2 salt. Lazily init shared SecureRandom to
|
|
* avoid extra native RNG structs. */
|
|
synchronized (randLock) {
|
|
if (this.rand == null) {
|
|
this.rand = new SecureRandom();
|
|
}
|
|
rand.nextBytes(salt);
|
|
}
|
|
|
|
/* Write salt length and salt */
|
|
dos.writeInt(salt.length);
|
|
dos.write(salt);
|
|
|
|
/* Write PBKDF2 iterations */
|
|
dos.writeInt(WKS_PBKDF2_ITERATION_COUNT);
|
|
|
|
/* Get encoded bytes up to this point */
|
|
dos.flush();
|
|
encoded = bos.toByteArray();
|
|
|
|
/* Derive HMAC key from password with PBKDF2 */
|
|
log("deriving HMAC-SHA512 key with PKCS#5 PBKDF2-HMAC-SHA512");
|
|
derivedKey = Pwdbased.PBKDF2(
|
|
WolfCryptSecretKeyFactory.passwordToByteArray(password),
|
|
salt, WKS_PBKDF2_ITERATION_COUNT, WKS_HMAC_KEY_LENGTH,
|
|
WKS_PBKDF2_TYPE);
|
|
if (derivedKey == null) {
|
|
throw new IOException("Error deriving key with PBKDF2");
|
|
}
|
|
|
|
/* Calculate HMAC-SHA512 of output array, hard coding use of
|
|
* wolfJCE provider to guarantee use when using wolfCrypt FIPS */
|
|
log("calculating HMAC-SHA512 for KeyStore integrity");
|
|
try {
|
|
keySpec = new SecretKeySpec(derivedKey, "SHA512");
|
|
|
|
mac = Mac.getInstance("HmacSHA512", "wolfJCE");
|
|
mac.init(keySpec);
|
|
mac.update(encoded);
|
|
hmac = mac.doFinal();
|
|
|
|
} catch (NoSuchProviderException e) {
|
|
throw new IOException("No Mac.HmacSHA512 found for wolfJCE");
|
|
} catch (InvalidKeyException e) {
|
|
throw new IOException("Invalid HmacSHA512 key");
|
|
}
|
|
|
|
/* Write HMAC to end of encoded store */
|
|
dos.writeInt(hmac.length);
|
|
dos.write(hmac);
|
|
|
|
dos.flush();
|
|
|
|
/* Write final array to provided OutputStream */
|
|
stream.write(bos.toByteArray());
|
|
|
|
} finally {
|
|
if (dos != null) {
|
|
dos.close();
|
|
}
|
|
if (encoded != null) {
|
|
Arrays.fill(encoded, (byte)0);
|
|
}
|
|
if (derivedKey != null) {
|
|
Arrays.fill(derivedKey, (byte)0);
|
|
}
|
|
if (hmac != null) {
|
|
Arrays.fill(hmac, (byte)0);
|
|
}
|
|
}
|
|
|
|
log("KeyStore successfully stored to OutputStream");
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Internal InputStream class used to buffer input data and generate
|
|
* an HMAC-SHA512 integrity check over that data.
|
|
*
|
|
* All data passing though this InputStream will be cached internally
|
|
* for use in HMAC computation, unless data caching is disabled by
|
|
* calling enableCaching(false). If caching is disabled, no future
|
|
* data will be stored until caching is re-enabled.
|
|
*/
|
|
private static class BufferedPbkdf2HmacInputStream extends InputStream {
|
|
|
|
/* InputStream from which data will be read */
|
|
private InputStream is = null;
|
|
|
|
/* Internal OutputStream where all bytes read will be written
|
|
* to be cached for later HMAC operation */
|
|
private ByteArrayOutputStream bos = null;
|
|
|
|
/* Used to pause caching of data if needed, otherwise all bytes
|
|
* read will be copied and stored into ByteArrayOutputStream */
|
|
private boolean cacheData = true;
|
|
|
|
public BufferedPbkdf2HmacInputStream(InputStream stream) {
|
|
|
|
if (stream == null) {
|
|
throw new IllegalArgumentException(
|
|
"InputStream and password cannot be null");
|
|
}
|
|
|
|
this.is = stream;
|
|
this.bos = new ByteArrayOutputStream();
|
|
|
|
}
|
|
|
|
@Override
|
|
public synchronized int read() throws IOException {
|
|
|
|
int rByte = this.is.read();
|
|
|
|
if (this.cacheData && rByte != -1) {
|
|
bos.write(rByte);
|
|
}
|
|
return rByte;
|
|
}
|
|
|
|
@Override
|
|
public synchronized void close() throws IOException {
|
|
|
|
if (this.bos != null) {
|
|
this.bos.reset();
|
|
this.bos.close();
|
|
}
|
|
|
|
super.close();
|
|
}
|
|
|
|
/**
|
|
* Enable or disable caching of data inside this InputStream.
|
|
*
|
|
* Caching is enabled by default, unless explicitly disabled.
|
|
*
|
|
* @param enabled boolean value to enable or disable input caching
|
|
*/
|
|
public synchronized void enableCaching(boolean enabled) {
|
|
this.cacheData = enabled;
|
|
}
|
|
|
|
/**
|
|
* Generate HMAC-SHA512 over cached data, deriving HMAC key from
|
|
* provided password using PBKDF2-HMAC-SHA512.
|
|
*
|
|
* @param password password to use for HMAC key generation
|
|
* @param salt salt to use for PBKDF2 key derivation, cannot be null
|
|
* @param iterations iterations to use for PBKDF2
|
|
*
|
|
* @return HMAC-SHA512 of data cached by this InputStream so far
|
|
*
|
|
* @throws IOException on error getting cached data internally
|
|
*/
|
|
public synchronized byte[] generatePbkdf2Hmac(char[] password,
|
|
byte[] salt, int iterations) throws IOException {
|
|
|
|
Mac mac = null;
|
|
SecretKeySpec keySpec = null;
|
|
byte[] derivedKey = null;
|
|
byte[] buffered = null;
|
|
byte[] hmac = null;
|
|
|
|
if (password == null || password.length == 0 ||
|
|
salt == null || salt.length == 0 ||
|
|
iterations < WKS_PBKDF2_MIN_ITERATIONS) {
|
|
throw new IOException("Invalid password, salt, or iterations");
|
|
}
|
|
|
|
/* Derive HMAC key from password using PBKDF2 */
|
|
derivedKey = Pwdbased.PBKDF2(
|
|
WolfCryptSecretKeyFactory.passwordToByteArray(password),
|
|
salt, iterations, WKS_HMAC_KEY_LENGTH, WKS_PBKDF2_TYPE);
|
|
if (derivedKey == null) {
|
|
throw new IOException("Error deriving key with PBKDF2");
|
|
}
|
|
|
|
/* Get full byte array to generate HMAC over */
|
|
buffered = bos.toByteArray();
|
|
|
|
/* Calculate HMAC-SHA512 of output array, hard coding use of
|
|
* wolfJCE provider here to guarantee use when using FIPS */
|
|
try {
|
|
keySpec = new SecretKeySpec(derivedKey, "SHA512");
|
|
|
|
mac = Mac.getInstance("HmacSHA512", "wolfJCE");
|
|
mac.init(keySpec);
|
|
mac.update(buffered);
|
|
hmac = mac.doFinal();
|
|
|
|
} catch (NoSuchProviderException e) {
|
|
throw new IOException("No Mac.HmacSHA512 found for wolfJCE");
|
|
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new IOException("No Mac.HmacSHA512 found in wolfJCE");
|
|
|
|
} catch (InvalidKeyException e) {
|
|
throw new IOException("Invalid HmacSHA512 key");
|
|
|
|
} finally {
|
|
if (buffered != null) {
|
|
Arrays.fill(buffered, (byte)0);
|
|
}
|
|
}
|
|
|
|
return hmac;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load the KeyStore from the provided InputStream.
|
|
*
|
|
* @param stream InputStream from which to load KeyStore
|
|
* @param password password used to check KeyStore integrity, must not
|
|
* be null
|
|
*
|
|
* @throws IOException on I/O problem or issue with the
|
|
* KeyStore data format
|
|
* @throws NoSuchAlgorithmException if algorithm used to check the
|
|
* KeyStore integrity cannot be found
|
|
* @throws CertificateException if any of the certificates in the
|
|
* KeyStore could not be loaded
|
|
*/
|
|
@Override
|
|
public synchronized void engineLoad(InputStream stream, char[] password)
|
|
throws IOException, NoSuchAlgorithmException, CertificateException {
|
|
|
|
int i;
|
|
int tmp = 0;
|
|
int entryCount = 0;
|
|
int entryType = 0;
|
|
int encodedLen = 0;
|
|
int bytesRead = 0;
|
|
int saltLen = 0;
|
|
int hmacLen = 0;
|
|
int iterations = 0;
|
|
byte[] encodedEntry = null;
|
|
byte[] salt = null;
|
|
byte[] hmac = null;
|
|
byte[] genHmac = null;
|
|
boolean havePass = true;
|
|
|
|
String alias = null;
|
|
BufferedPbkdf2HmacInputStream his = null;
|
|
DataInputStream dis = null;
|
|
|
|
WKSPrivateKey keyEntry = null;
|
|
WKSSecretKey sKeyEntry = null;
|
|
WKSCertificate certEntry = null;
|
|
|
|
/* Parse entries into a local map first. They are only committed
|
|
* into the shared this.entries map after the HMAC integrity check
|
|
* passes, so a failed or tampered load never exposes unverified
|
|
* entries to engineGetKey()/engineGetCertificate() callers. */
|
|
Map<String, Object> loadedEntries = new LinkedHashMap<String, Object>();
|
|
|
|
log("loading KeyStore from InputStream");
|
|
|
|
/* Clear any cached KEK entries from previous keystore */
|
|
clearKekCache();
|
|
|
|
if (password == null || password.length == 0) {
|
|
havePass = false;
|
|
log("KeyStore password not provided, HMAC integrity check " +
|
|
"will be skipped");
|
|
}
|
|
|
|
if (stream == null) {
|
|
log("KeyStore InputStream is null, nothing to load");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (havePass) {
|
|
his = new BufferedPbkdf2HmacInputStream(stream);
|
|
dis = new DataInputStream(his);
|
|
}
|
|
else {
|
|
dis = new DataInputStream(stream);
|
|
}
|
|
|
|
/* magic number */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_MAGIC_NUMBER) {
|
|
throw new IOException(
|
|
"Invalid magic number ( " + tmp + "), " +
|
|
"KeyStore not of type WKS");
|
|
|
|
}
|
|
|
|
/* store version */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_STORE_VERSION) {
|
|
throw new IOException(
|
|
"Invalid WKS KeyStore version: " + tmp);
|
|
}
|
|
log("KeyStore version: " + tmp);
|
|
|
|
/* total entry count */
|
|
entryCount = dis.readInt();
|
|
if (entryCount < 0) {
|
|
throw new IOException("Invalid entry count, negative");
|
|
}
|
|
log("KeyStore entry count: " + entryCount);
|
|
|
|
for (i = 0; i < entryCount; i++) {
|
|
/* entry type */
|
|
entryType = dis.readInt();
|
|
if (entryType < 0) {
|
|
throw new IOException("Invalid entry type, negative");
|
|
}
|
|
|
|
/* alias */
|
|
alias = dis.readUTF();
|
|
|
|
/* encoded entry length */
|
|
encodedLen = dis.readInt();
|
|
if (encodedLen < 0) {
|
|
throw new IOException("Invalid encoded length, negative");
|
|
}
|
|
|
|
if (encodedLen > WKS_MAX_ENTRY_SIZE) {
|
|
throw new IOException("Encoded entry length (" +
|
|
encodedLen + ") is larger than max allowed: " +
|
|
WKS_MAX_ENTRY_SIZE);
|
|
}
|
|
|
|
/* encoded entry */
|
|
encodedEntry = new byte[encodedLen];
|
|
bytesRead = dis.read(encodedEntry);
|
|
if (bytesRead != encodedLen) {
|
|
throw new IOException(
|
|
"Unable to read total encoded entry byte array");
|
|
}
|
|
|
|
switch (entryType) {
|
|
case WKS_ENTRY_ID_PRIVATE_KEY:
|
|
log("loading PrivateKey: " + alias);
|
|
keyEntry = new WKSPrivateKey(encodedEntry);
|
|
loadedEntries.put(alias, keyEntry);
|
|
break;
|
|
|
|
case WKS_ENTRY_ID_SECRET_KEY:
|
|
log("loading SecretKey: " + alias);
|
|
sKeyEntry = new WKSSecretKey(encodedEntry);
|
|
loadedEntries.put(alias, sKeyEntry);
|
|
break;
|
|
|
|
case WKS_ENTRY_ID_CERTIFICATE:
|
|
log("loading Certificate: " + alias);
|
|
certEntry = new WKSCertificate(encodedEntry);
|
|
loadedEntries.put(alias, certEntry);
|
|
break;
|
|
|
|
default:
|
|
throw new IOException(
|
|
"Invalid entry type found: " + entryType);
|
|
}
|
|
}
|
|
|
|
/* PBKDF2 salt len and salt */
|
|
saltLen = dis.readInt();
|
|
if (saltLen != WKS_PBKDF2_SALT_SIZE) {
|
|
throw new IOException("Invalid salt size: " + saltLen);
|
|
}
|
|
|
|
salt = new byte[saltLen];
|
|
saltLen = dis.read(salt);
|
|
if (saltLen != WKS_PBKDF2_SALT_SIZE) {
|
|
throw new IOException("Failed to read entire salt from WKS");
|
|
}
|
|
|
|
/* PBKDF2 iterations */
|
|
iterations = dis.readInt();
|
|
if (iterations < WKS_PBKDF2_MIN_ITERATIONS) {
|
|
throw new IOException(
|
|
"PBKDF2 iterations too small: " + iterations);
|
|
}
|
|
|
|
/* Pause caching of input data, HMAC itself not included in HMAC */
|
|
if (havePass) {
|
|
his.enableCaching(false);
|
|
}
|
|
|
|
/* HMAC len and HMAC */
|
|
hmacLen = dis.readInt();
|
|
if (hmacLen != WKS_HMAC_KEY_LENGTH) {
|
|
throw new IOException(
|
|
"HMAC length (" + hmacLen + ") is different than " +
|
|
"expected (" + WKS_HMAC_KEY_LENGTH + ")");
|
|
}
|
|
hmac = new byte[hmacLen];
|
|
hmacLen = dis.read(hmac);
|
|
if (hmacLen != hmac.length) {
|
|
throw new IOException(
|
|
"Failed to read entire HMAC from WKS stream");
|
|
}
|
|
|
|
/* Regenerate HMAC-SHA512 over bytes read so far */
|
|
if (havePass) {
|
|
genHmac = his.generatePbkdf2Hmac(password, salt, iterations);
|
|
if (genHmac == null || genHmac.length == 0) {
|
|
throw new IOException(
|
|
"Unable to generate HMAC-SHA512 over input WKS stream");
|
|
}
|
|
|
|
if (!WolfCrypt.ConstantCompare(hmac, genHmac)) {
|
|
throw new IOException("Integrity check failed on WKS, " +
|
|
"KeyStore has been tampered with!");
|
|
}
|
|
|
|
log("HMAC-SHA512 integrity verification successful");
|
|
}
|
|
else {
|
|
log("HMAC-SHA512 integrity verification skipped, " +
|
|
"no password provided");
|
|
}
|
|
|
|
/* Commit parsed entries into the shared map, replacing any
|
|
* previous contents */
|
|
this.entries.clear();
|
|
this.entries.putAll(loadedEntries);
|
|
|
|
} finally {
|
|
if (dis != null) {
|
|
dis.close();
|
|
}
|
|
}
|
|
|
|
log("KeyStore successfully loaded from InputStream");
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Probes the specified input stream to determine if it contains a
|
|
* keystore that is supported by this implementation.
|
|
*
|
|
* This method is used by KeyStore.getInstance(File, char[]) and similar
|
|
* methods to auto-detect the keystore type without requiring explicit
|
|
* type specification.
|
|
*
|
|
* Note: engineProbe() was added in JDK 9. No @Override annotation to
|
|
* maintain Java 8 compatibility. On JDK 9+ this method is called by
|
|
* KeyStore for auto-detection; on Java 8 it is simply unused.
|
|
*
|
|
* @param stream the keystore data to be probed. The stream must support
|
|
* mark/reset so the caller can rewind after probing.
|
|
*
|
|
* @return true if the keystore data is supported (has WKS magic number),
|
|
* otherwise false
|
|
*
|
|
* @throws IOException if there is an I/O problem with the keystore data
|
|
* @throws NullPointerException if the stream is null
|
|
*/
|
|
public boolean engineProbe(InputStream stream) throws IOException {
|
|
|
|
int magic = 0;
|
|
DataInputStream dis = null;
|
|
|
|
if (stream == null) {
|
|
throw new NullPointerException("InputStream cannot be null");
|
|
}
|
|
|
|
/* Read first 4 bytes to check magic number */
|
|
stream.mark(4);
|
|
dis = new DataInputStream(stream);
|
|
|
|
try {
|
|
magic = dis.readInt();
|
|
if (magic == WKS_MAGIC_NUMBER) {
|
|
return true;
|
|
}
|
|
}
|
|
catch (IOException e) {
|
|
/* Could not read 4 bytes, not a valid WKS file, swallow
|
|
* exception and return false below. */
|
|
}
|
|
finally {
|
|
try {
|
|
stream.reset();
|
|
}
|
|
catch (IOException e) {
|
|
/* Reset failed, stream may not support mark/reset */
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Internal method for logging output.
|
|
*
|
|
* @param msg message to be logged
|
|
*/
|
|
private static synchronized void log(String msg) {
|
|
WolfCryptDebug.log(WolfSSLKeyStore.class, WolfCryptDebug.INFO,
|
|
() -> msg);
|
|
}
|
|
|
|
/**
|
|
* Inner class representing a private key entry.
|
|
*
|
|
* When encoded to a byte[] for storage (getEncoded()), the following
|
|
* format is used. Items are written out through DataOutputStream
|
|
* backed by ByteArrayOutputStream, thus long values are stored
|
|
* to 8 bytes big endian, int is 4 bytes big endian, etc.
|
|
*
|
|
* The HMAC-SHA512 at the end is calculated over all items above it along
|
|
* with their lengths.
|
|
*
|
|
* creationDate.getTime() (long | 8 bytes)
|
|
* kdfSalt.length (int | 4 bytes)
|
|
* kdfSalt (byte[])
|
|
* kdfIterations (int | 4 bytes)
|
|
* iv.length (int | 4 bytes)
|
|
* iv (byte[])
|
|
* encryptedKey.length (int | 4 bytes)
|
|
* encryptedKey (byte[])
|
|
* chain.length (int | 4 bytes)
|
|
* FOR EACH CERT:
|
|
* chain[i].getType() (UTF String)
|
|
* chain[i].getEncoded().length (int | 4 bytes)
|
|
* chain[i].getEncoced() (byte[])
|
|
* hmac.length (int | 4 bytes)
|
|
* hmac (byte[])
|
|
*/
|
|
private static class WKSPrivateKey {
|
|
|
|
byte[] encryptedKey; /* protected/encrypted key */
|
|
byte[] iv; /* AES-GCM IV */
|
|
byte[] kdfSalt; /* PBKDF2 salt */
|
|
int kdfIterations; /* PBKDF2 iterations */
|
|
Certificate[] chain; /* cert chain matching this private key */
|
|
Date creationDate; /* creation date for this object */
|
|
byte[] hmacSha512; /* HMAC calculated over members and lengths */
|
|
|
|
/**
|
|
* Create new WKSPrivateKey from plaintext key and certificate chain,
|
|
* encrypt/protect plaintext key using provided password.
|
|
*
|
|
* @param plainKey unencrypted private key to protect/encrypt inside
|
|
* this object
|
|
* @param password password to be used for key protection
|
|
* @param chain Certificate array containing cert chain matching key
|
|
*
|
|
* @throws IllegalArgumentException if input arguments are null
|
|
* @throws KeyStoreException if encrypting/protecting private key fails
|
|
*/
|
|
protected WKSPrivateKey(byte[] plainKey, char[] password,
|
|
Certificate[] chain, SecureRandom rand)
|
|
throws IllegalArgumentException, KeyStoreException {
|
|
|
|
byte[] derivedKey = null;
|
|
byte[] protectedKey = null;
|
|
byte[] hmac = null;
|
|
byte[] kek = new byte[WKS_ENC_KEY_LENGTH];
|
|
byte[] hmacKey = new byte[WKS_HMAC_KEY_LENGTH];
|
|
byte[] encoded = null;
|
|
SecureRandom rng = rand;
|
|
|
|
if (plainKey == null || plainKey.length == 0 ||
|
|
password == null || password.length == 0 ||
|
|
chain == null || chain.length == 0) {
|
|
throw new IllegalArgumentException(
|
|
"Invalid null arguments when creating WKSPrivateKey");
|
|
}
|
|
|
|
this.kdfSalt = new byte[WKS_PBKDF2_SALT_SIZE];
|
|
this.iv = new byte[WKS_ENC_IV_LENGTH];
|
|
|
|
/* Generate random salt and IV. Reuses caller SecureRandom when
|
|
* provided, only creates new one if null to minimize native RNG
|
|
* struct init. */
|
|
synchronized (randLock) {
|
|
if (rng == null) {
|
|
rng = new SecureRandom();
|
|
}
|
|
rng.nextBytes(this.kdfSalt);
|
|
rng.nextBytes(this.iv);
|
|
}
|
|
|
|
try {
|
|
/* Derive key encryption key from password using
|
|
* PBKDF2-HMAC-SHA512. Generate a 96 byte key in total, split
|
|
* between 32-byte AES-CBC-256 and 64-byte HMAC-SHA512 keys */
|
|
derivedKey = deriveKeyFromPassword(password, this.kdfSalt,
|
|
WKS_PBKDF2_ITERATION_COUNT,
|
|
WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH);
|
|
|
|
if (derivedKey == null) {
|
|
throw new KeyStoreException(
|
|
"Error deriving key encryption key, got null key");
|
|
}
|
|
if (derivedKey.length !=
|
|
(WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH)) {
|
|
throw new KeyStoreException(
|
|
"Error deriving key encryption key, wrong length. " +
|
|
" actual: " + derivedKey.length + ", needed: " +
|
|
(WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH));
|
|
}
|
|
|
|
/* Split key into encrypt + HMAC keys, erase derivedKey */
|
|
System.arraycopy(derivedKey, 0, kek, 0, kek.length);
|
|
System.arraycopy(derivedKey, kek.length, hmacKey, 0,
|
|
hmacKey.length);
|
|
Arrays.fill(derivedKey, (byte)0);
|
|
|
|
/* Encrypt plain key with KEK */
|
|
protectedKey = encryptKey(plainKey, kek, this.iv);
|
|
if (protectedKey == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to encrypt plaintext key");
|
|
}
|
|
|
|
this.encryptedKey = protectedKey;
|
|
this.kdfIterations = WKS_PBKDF2_ITERATION_COUNT;
|
|
this.chain = chain.clone();
|
|
this.creationDate = new Date();
|
|
|
|
/* Get encoded byte[] of object class variables without HMAC */
|
|
encoded = getEncoded(false);
|
|
if (encoded == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to get encoded WKSPrivateKey for HMAC gen");
|
|
}
|
|
|
|
/* Generate internal HMAC over object contents */
|
|
hmac = generateHmac(hmacKey, encoded);
|
|
if (hmac == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to generate HMAC over WKSPrivateKey");
|
|
}
|
|
|
|
this.hmacSha512 = hmac;
|
|
|
|
} catch (KeyStoreException | IOException |
|
|
CertificateEncodingException e) {
|
|
/* Clear protected key and HMAC in case of error */
|
|
if (protectedKey != null) {
|
|
Arrays.fill(protectedKey, (byte)0);
|
|
}
|
|
if (hmac != null) {
|
|
Arrays.fill(hmac, (byte)0);
|
|
}
|
|
throw new KeyStoreException(e);
|
|
|
|
} finally {
|
|
/* Clear local KEK, HMAC key, and encoded content */
|
|
Arrays.fill(kek, (byte)0);
|
|
Arrays.fill(hmacKey, (byte)0);
|
|
if (encoded != null) {
|
|
Arrays.fill(encoded, (byte)0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create new WKSPrivateKey object from encoded byte array.
|
|
*
|
|
* @param encoded encoded byte array which was created by
|
|
* calling WKSPrivateKey getEncoded() method.
|
|
*
|
|
* @throws IOException on error reading/parsing encoded array
|
|
*/
|
|
protected WKSPrivateKey(byte[] encoded)
|
|
throws IOException, CertificateException {
|
|
|
|
int i;
|
|
int tmp = 0;
|
|
byte[] tmpArr = null;
|
|
String tmpStr = null;
|
|
ByteArrayInputStream bis = null;
|
|
ByteArrayInputStream certStream = null;
|
|
DataInputStream dis = null;
|
|
CertificateFactory cf = null;
|
|
Certificate tmpCert = null;
|
|
|
|
if (encoded == null || encoded.length == 0) {
|
|
throw new IllegalArgumentException(
|
|
"Input byte array cannot be null");
|
|
}
|
|
|
|
try {
|
|
bis = new ByteArrayInputStream(encoded);
|
|
dis = new DataInputStream(bis);
|
|
|
|
/* creationDate */
|
|
this.creationDate = new Date(dis.readLong());
|
|
|
|
/* kdfSalt */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_PBKDF2_SALT_SIZE) {
|
|
throw new IOException(
|
|
"Invalid PBKDF2 salt size: " + tmp);
|
|
}
|
|
this.kdfSalt = new byte[tmp];
|
|
dis.readFully(this.kdfSalt);
|
|
|
|
/* kdfIterations */
|
|
tmp = dis.readInt();
|
|
if (tmp < WKS_PBKDF2_MIN_ITERATIONS) {
|
|
throw new IOException(
|
|
"PBKDF2 iterations too small: " + tmp);
|
|
}
|
|
this.kdfIterations = tmp;
|
|
|
|
/* iv */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_ENC_IV_LENGTH) {
|
|
throw new IOException(
|
|
"Invalid IV size: " + tmp);
|
|
}
|
|
this.iv = new byte[tmp];
|
|
dis.readFully(this.iv);
|
|
|
|
/* encrypted key */
|
|
tmp = dis.readInt();
|
|
if (tmp < 0) {
|
|
throw new IOException(
|
|
"Bad encrypted key length, negative");
|
|
}
|
|
this.encryptedKey = new byte[tmp];
|
|
dis.readFully(this.encryptedKey);
|
|
|
|
/* chain */
|
|
tmp = dis.readInt();
|
|
if (tmp > WKS_MAX_CHAIN_COUNT) {
|
|
throw new IOException(
|
|
"Cert chain count (" + tmp + ") is larger than max " +
|
|
"allowed (" + WKS_MAX_CHAIN_COUNT + ")");
|
|
}
|
|
this.chain = new Certificate[tmp];
|
|
|
|
/* chain certs */
|
|
for (i = 0; i < chain.length; i++) {
|
|
|
|
/* type, get CertificateFactory */
|
|
tmpStr = dis.readUTF();
|
|
if (cf == null || !cf.getType().equals(tmpStr)) {
|
|
cf = CertificateFactory.getInstance(tmpStr);
|
|
}
|
|
|
|
/* encoding length */
|
|
tmp = dis.readInt();
|
|
if (tmp < 0) {
|
|
throw new IOException(
|
|
"Bad encoding length, negative");
|
|
}
|
|
tmpArr = new byte[tmp];
|
|
|
|
/* encoded cert */
|
|
dis.readFully(tmpArr);
|
|
certStream = new ByteArrayInputStream(tmpArr);
|
|
tmpCert = cf.generateCertificate(certStream);
|
|
certStream.close();
|
|
|
|
/* add to chain */
|
|
this.chain[i] = tmpCert;
|
|
}
|
|
|
|
/* HMAC-SHA512, not verifying since we don't have password */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_HMAC_KEY_LENGTH) {
|
|
throw new IOException(
|
|
"HMAC length (" + tmp + ") is different than " +
|
|
"expected (" + WKS_HMAC_KEY_LENGTH + ")");
|
|
}
|
|
this.hmacSha512 = new byte[tmp];
|
|
dis.readFully(this.hmacSha512);
|
|
|
|
} catch (Exception e) {
|
|
|
|
if (this.encryptedKey != null) {
|
|
Arrays.fill(this.encryptedKey, (byte)0);
|
|
this.encryptedKey = null;
|
|
}
|
|
if (this.iv != null) {
|
|
Arrays.fill(this.iv, (byte)0);
|
|
this.iv = null;
|
|
}
|
|
if (this.kdfSalt != null) {
|
|
Arrays.fill(this.kdfSalt, (byte)0);
|
|
this.kdfSalt = null;
|
|
}
|
|
if (this.hmacSha512 != null) {
|
|
Arrays.fill(this.hmacSha512, (byte)0);
|
|
this.hmacSha512 = null;
|
|
}
|
|
this.chain = null;
|
|
this.creationDate = null;
|
|
this.kdfIterations = 0;
|
|
|
|
throw e;
|
|
|
|
} finally {
|
|
if (dis != null) {
|
|
dis.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get encoded byte array representation of this object, optionally
|
|
* including HMAC over contents appended to end.
|
|
*
|
|
* @param withHMAC include HMAC on end of encoded array when true,
|
|
* otherwise return encoded object without HMAC. HMAC
|
|
* is included when writing out / storing, but not included
|
|
* when we are just using this as a helper function to
|
|
* check the HMAC value inside this object in memory.
|
|
*
|
|
* @return byte array representing this object or null on error
|
|
* @throws IOException on error writing to output stream
|
|
* @throws CertificateEncodingException on error getting
|
|
* Certificate encoding
|
|
*/
|
|
protected synchronized byte[] getEncoded(boolean withHMAC)
|
|
throws IOException, CertificateEncodingException {
|
|
|
|
int i;
|
|
byte[] out = null;
|
|
ByteArrayOutputStream bos = null;
|
|
DataOutputStream dos = null;
|
|
|
|
if (this.creationDate == null || this.kdfSalt == null ||
|
|
this.kdfSalt.length == 0 || this.kdfIterations <= 0 ||
|
|
this.iv == null || this.iv.length == 0 ||
|
|
this.encryptedKey == null || this.encryptedKey.length == 0 ||
|
|
this.chain == null) {
|
|
log("invalid WKSPrivateKey class variables, returning null " +
|
|
"from getEncoded()");
|
|
return null;
|
|
}
|
|
|
|
if (withHMAC && (this.hmacSha512 == null ||
|
|
this.hmacSha512.length == 0)) {
|
|
log("WKSPrivateKey HMAC null or zero length, returning null " +
|
|
"from getEncoded()");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
bos = new ByteArrayOutputStream();
|
|
dos = new DataOutputStream(bos);
|
|
|
|
dos.writeLong(this.creationDate.getTime());
|
|
dos.writeInt(this.kdfSalt.length);
|
|
dos.write(this.kdfSalt, 0, this.kdfSalt.length);
|
|
dos.writeInt(this.kdfIterations);
|
|
dos.writeInt(this.iv.length);
|
|
dos.write(this.iv, 0, this.iv.length);
|
|
dos.writeInt(this.encryptedKey.length);
|
|
dos.write(this.encryptedKey, 0, this.encryptedKey.length);
|
|
dos.writeInt(this.chain.length);
|
|
if (this.chain.length > 0) {
|
|
for (i = 0; i < this.chain.length; i++) {
|
|
dos.writeUTF(this.chain[i].getType());
|
|
dos.writeInt(this.chain[i].getEncoded().length);
|
|
dos.write(this.chain[i].getEncoded(), 0,
|
|
this.chain[i].getEncoded().length);
|
|
}
|
|
}
|
|
if (withHMAC) {
|
|
dos.writeInt(this.hmacSha512.length);
|
|
dos.write(this.hmacSha512);
|
|
}
|
|
|
|
dos.flush();
|
|
out = bos.toByteArray();
|
|
|
|
} finally {
|
|
if (dos != null) {
|
|
dos.close();
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Decrypt and return plaintext key encoded in this object,
|
|
* using provided password.
|
|
*
|
|
* Verifies internally-generated HMAC over WKSPrivateKey object
|
|
* contents using password before decrypting key.
|
|
*
|
|
* Other than password, all other information needed should already
|
|
* be stored in this object.
|
|
*
|
|
* @param password password to use for decryption
|
|
* @param keyStore outer KeyStore instance for cache operations
|
|
*/
|
|
protected synchronized byte[] getDecryptedKey(char[] password,
|
|
WolfSSLKeyStore keyStore) throws UnrecoverableKeyException {
|
|
|
|
byte[] plain = null;
|
|
byte[] derivedKey = null;
|
|
byte[] hmac = null;
|
|
byte[] kek = new byte[WKS_ENC_KEY_LENGTH];
|
|
byte[] hmacKey = new byte[WKS_HMAC_KEY_LENGTH];
|
|
byte[] encoded = null;
|
|
boolean fromCache = false;
|
|
|
|
if (password == null || password.length == 0) {
|
|
throw new UnrecoverableKeyException(
|
|
"Unable to decrypt key with null password");
|
|
}
|
|
|
|
try {
|
|
/* Try to get derived key from cache */
|
|
derivedKey = keyStore.getCachedDerivedKey(password,
|
|
this.kdfSalt, this.kdfIterations);
|
|
|
|
if (derivedKey != null) {
|
|
fromCache = true;
|
|
} else {
|
|
/* Cache miss, derive encryption key from password using
|
|
* PBKDF2-HMAC-SHA512. Generate a 96 byte key in total,
|
|
* to split between 32-byte AES-CBC-256 key and 64-byte
|
|
* HMAC-SHA512 key. */
|
|
derivedKey = deriveKeyFromPassword(password, this.kdfSalt,
|
|
this.kdfIterations,
|
|
WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH);
|
|
|
|
if (derivedKey == null) {
|
|
throw new KeyStoreException(
|
|
"Error deriving key decryption key, got null key");
|
|
}
|
|
}
|
|
|
|
/* Split key into decrypt + HMAC keys */
|
|
System.arraycopy(derivedKey, 0, kek, 0, kek.length);
|
|
System.arraycopy(derivedKey, kek.length, hmacKey, 0,
|
|
hmacKey.length);
|
|
|
|
/* Get encoded byte[] of object class variables without HMAC */
|
|
encoded = getEncoded(false);
|
|
if (encoded == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to get encoded WKSPrivateKey for HMAC");
|
|
}
|
|
|
|
/* Re-generate internal HMAC over object contents */
|
|
hmac = generateHmac(hmacKey, encoded);
|
|
if (hmac == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to regenerate HMAC over WKSPrivateKey");
|
|
}
|
|
|
|
/* Verify HMAC first before decrypting key */
|
|
if (!WolfCrypt.ConstantCompare(hmac, this.hmacSha512)) {
|
|
/* HMAC verification failed */
|
|
if (fromCache) {
|
|
/* Invalidate the cache entry that gave us wrong key */
|
|
keyStore.invalidateCacheEntry(password, this.kdfSalt,
|
|
this.kdfIterations);
|
|
}
|
|
throw new KeyStoreException(
|
|
"HMAC verification failed on WKSPrivateKey, entry " +
|
|
"corrupted or wrong password");
|
|
} else {
|
|
log("HMAC verification successful on WKSPrivateKey");
|
|
}
|
|
|
|
/* HMAC verified, cache the derived key */
|
|
if (!fromCache) {
|
|
keyStore.cacheDerivedKey(password, this.kdfSalt,
|
|
this.kdfIterations, derivedKey);
|
|
}
|
|
|
|
/* Decrypt encrypted key with KEK */
|
|
plain = decryptKey(this.encryptedKey, kek, this.iv);
|
|
if (plain == null) {
|
|
throw new UnrecoverableKeyException(
|
|
"Unable to decrypt protected key");
|
|
}
|
|
|
|
} catch (KeyStoreException | IOException |
|
|
CertificateEncodingException e) {
|
|
if (plain != null) {
|
|
Arrays.fill(plain, (byte)0);
|
|
}
|
|
throw new UnrecoverableKeyException(e.getMessage());
|
|
|
|
} finally {
|
|
Arrays.fill(kek, (byte)0);
|
|
Arrays.fill(hmacKey, (byte)0);
|
|
if (derivedKey != null) {
|
|
Arrays.fill(derivedKey, (byte)0);
|
|
}
|
|
if (hmac != null) {
|
|
Arrays.fill(hmac, (byte)0);
|
|
}
|
|
if (encoded != null) {
|
|
Arrays.fill(encoded, (byte)0);
|
|
}
|
|
}
|
|
|
|
return plain;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inner class representing a single certificate-only entry.
|
|
*
|
|
* When encoded to a byte[] for storage (getEncoded()), the following
|
|
* format is used:
|
|
*
|
|
* creationDate.getTime() (long)
|
|
* cert.getType() (UTF String)
|
|
* cert.getEncoded().length (int)
|
|
* cert.getEncoced() (byte[])
|
|
*/
|
|
private static class WKSCertificate {
|
|
|
|
Certificate cert;
|
|
Date creationDate;
|
|
|
|
protected WKSCertificate() {
|
|
}
|
|
|
|
/**
|
|
* Create new WKSCertificate object from encoded byte array.
|
|
*
|
|
* @param encoded encoded byte array obtained by calling WKSCertificate
|
|
* getEncoded() method.
|
|
*
|
|
* @throws IOException on error reading/parsing encoded array
|
|
*/
|
|
protected WKSCertificate(byte[] encoded)
|
|
throws IOException, CertificateException {
|
|
|
|
int tmp = 0;
|
|
byte[] tmpArr = null;
|
|
String tmpStr = null;
|
|
ByteArrayInputStream bis = null;
|
|
ByteArrayInputStream certStream = null;
|
|
DataInputStream dis = null;
|
|
CertificateFactory cf = null;
|
|
|
|
if (encoded == null || encoded.length == 0) {
|
|
throw new IllegalArgumentException(
|
|
"Input byte array cannot be null");
|
|
}
|
|
|
|
try {
|
|
bis = new ByteArrayInputStream(encoded);
|
|
dis = new DataInputStream(bis);
|
|
|
|
/* creationDate */
|
|
this.creationDate = new Date(dis.readLong());
|
|
|
|
/* type, get CertificateFactory */
|
|
tmpStr = dis.readUTF();
|
|
if ((cf == null) ||
|
|
((cf != null) && !cf.getType().equals(tmpStr))) {
|
|
cf = CertificateFactory.getInstance(tmpStr);
|
|
}
|
|
|
|
/* encoding length */
|
|
tmp = dis.readInt();
|
|
if (tmp < 0) {
|
|
throw new IOException("Bad encoding length, negative");
|
|
}
|
|
tmpArr = new byte[tmp];
|
|
|
|
/* encoded cert */
|
|
dis.readFully(tmpArr);
|
|
certStream = new ByteArrayInputStream(tmpArr);
|
|
this.cert = cf.generateCertificate(certStream);
|
|
certStream.close();
|
|
|
|
} catch (Exception e) {
|
|
this.cert = null;
|
|
this.creationDate = null;
|
|
throw e;
|
|
|
|
} finally {
|
|
if (tmpArr != null) {
|
|
Arrays.fill(tmpArr, (byte)0);
|
|
tmpArr = null;
|
|
}
|
|
|
|
if (dis != null) {
|
|
dis.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get encoded byte array representation of this object.
|
|
*
|
|
* @return byte array representing this object or null on error
|
|
* @throws IOException on error writing to output stream
|
|
* @throws CertificateEncodingException on error getting
|
|
* Certificate encoding
|
|
*/
|
|
protected synchronized byte[] getEncoded()
|
|
throws IOException, CertificateEncodingException {
|
|
|
|
byte[] out = null;
|
|
ByteArrayOutputStream bos = null;
|
|
DataOutputStream dos = null;
|
|
|
|
if (this.cert == null || this.creationDate == null ||
|
|
this.cert.getEncoded() == null) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
bos = new ByteArrayOutputStream();
|
|
dos = new DataOutputStream(bos);
|
|
|
|
dos.writeLong(this.creationDate.getTime());
|
|
dos.writeUTF(this.cert.getType());
|
|
dos.writeInt(this.cert.getEncoded().length);
|
|
dos.write(this.cert.getEncoded(), 0,
|
|
this.cert.getEncoded().length);
|
|
|
|
dos.flush();
|
|
out = bos.toByteArray();
|
|
|
|
} finally {
|
|
if (dos != null) {
|
|
dos.close();
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inner class representing a SecretKey entry.
|
|
*
|
|
* When encoded to a byte[] for storage (getEncoded()), the following
|
|
* format is used. Items are written out through DataOutputStream
|
|
* backed by ByteArrayOutputStream, thus long values are stored
|
|
* to 8 bytes big endian, int is 4 bytes big endian, etc.
|
|
*
|
|
* The HMAC-SHA512 at the end is calculated over all items above it along
|
|
* with their lengths.
|
|
*
|
|
* creationDate.getTime() (long | 8 bytes)
|
|
* key.getAlgorithm() (UTF String)
|
|
* kdfSalt.length (int | 4 bytes)
|
|
* kdfSalt (byte[])
|
|
* kdfIterations (int | 4 bytes)
|
|
* iv.length (int | 4 bytes)
|
|
* iv (byte[])
|
|
* encryptedKey.length (int | 4 bytes)
|
|
* encryptedKey (byte[])
|
|
* hmac.length (int | 4 bytes)
|
|
* hmac (byte[])
|
|
*/
|
|
private static class WKSSecretKey {
|
|
|
|
byte[] encryptedKey = null; /* protected/encrypted key */
|
|
byte[] iv = null; /* AES IV */
|
|
byte[] kdfSalt = null; /* PBKDF2 salt */
|
|
int kdfIterations = 0; /* PBKDF2 iterations */
|
|
String keyAlgo = null; /* SecretKey.getAlgorithm() */
|
|
Date creationDate = null; /* creation date for this object */
|
|
byte[] hmacSha512 = null; /* HMAC over members and lengths */
|
|
|
|
/**
|
|
* Create new WKSSecretKey from plaintext key, encrypt/protect using
|
|
* provided password.
|
|
*
|
|
* @param plainKey unencrypted private key to protect/encrypt inside
|
|
* this object
|
|
* @param password password to be used for key protection
|
|
*
|
|
* @throws IllegalArgumentException if input arguments are null
|
|
* @throws KeyStoreException if encrypting/protecting key fails
|
|
*/
|
|
protected WKSSecretKey(byte[] plainKey, char[] password,
|
|
String keyAlgo, SecureRandom rand) throws IllegalArgumentException,
|
|
KeyStoreException {
|
|
|
|
byte[] derivedKey = null;
|
|
byte[] protectedKey = null;
|
|
byte[] hmac = null;
|
|
byte[] kek = new byte[WKS_ENC_KEY_LENGTH];
|
|
byte[] hmacKey = new byte[WKS_HMAC_KEY_LENGTH];
|
|
byte[] encoded = null;
|
|
SecureRandom rng = rand;
|
|
|
|
if (plainKey == null || plainKey.length == 0 ||
|
|
password == null || password.length == 0 ||
|
|
keyAlgo == null || keyAlgo.isEmpty()) {
|
|
throw new IllegalArgumentException(
|
|
"Invalid null arguments when creating WKSSecretKey");
|
|
}
|
|
|
|
this.kdfSalt = new byte[WKS_PBKDF2_SALT_SIZE];
|
|
this.iv = new byte[WKS_ENC_IV_LENGTH];
|
|
|
|
/* Generate random salt and IV. Reuses caller SecureRandom when
|
|
* provided, only creates new one if null to minimize native RNG
|
|
* struct init. */
|
|
synchronized (randLock) {
|
|
if (rng == null) {
|
|
rng = new SecureRandom();
|
|
}
|
|
rng.nextBytes(this.kdfSalt);
|
|
rng.nextBytes(this.iv);
|
|
}
|
|
|
|
try {
|
|
/* Derive key encryption key from password using
|
|
* PBKDF2-HMAC-SHA512. Generate a 96 byte key in total, split
|
|
* between 32-byte AES-CBC-256 and 64-byte HMAC-SHA512 keys */
|
|
derivedKey = deriveKeyFromPassword(password, this.kdfSalt,
|
|
WKS_PBKDF2_ITERATION_COUNT,
|
|
WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH);
|
|
if (derivedKey == null) {
|
|
throw new KeyStoreException(
|
|
"Error deriving key encryption key, got null key");
|
|
}
|
|
|
|
/* Split key into encrypt + HMAC keys, erase derivedKey */
|
|
System.arraycopy(derivedKey, 0, kek, 0, kek.length);
|
|
System.arraycopy(derivedKey, kek.length, hmacKey, 0,
|
|
hmacKey.length);
|
|
Arrays.fill(derivedKey, (byte)0);
|
|
|
|
/* Encrypt plain key */
|
|
protectedKey = encryptKey(plainKey, kek, this.iv);
|
|
if (protectedKey == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to encrypt plaintext key");
|
|
}
|
|
|
|
this.encryptedKey = protectedKey;
|
|
this.kdfIterations = WKS_PBKDF2_ITERATION_COUNT;
|
|
this.keyAlgo = keyAlgo;
|
|
this.creationDate = new Date();
|
|
|
|
/* Get encoded byte[] of object class variables without HMAC */
|
|
encoded = getEncoded(false);
|
|
if (encoded == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to get encoded WKSSecretKey for HMAC");
|
|
}
|
|
|
|
/* Generate internal HMAC over object contents */
|
|
hmac = generateHmac(hmacKey, encoded);
|
|
if (hmac == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to generate HMAC over WKSSecretKey");
|
|
}
|
|
|
|
this.hmacSha512 = hmac;
|
|
|
|
} catch (KeyStoreException | IOException |
|
|
CertificateEncodingException e) {
|
|
/* Clear protected key and HMAC in case of error */
|
|
if (protectedKey != null) {
|
|
Arrays.fill(protectedKey, (byte)0);
|
|
}
|
|
if (hmac != null) {
|
|
Arrays.fill(hmac, (byte)0);
|
|
}
|
|
throw new KeyStoreException(e);
|
|
|
|
} finally {
|
|
/* Clear local KEK, HMAC key, and encoded content */
|
|
Arrays.fill(kek, (byte)0);
|
|
Arrays.fill(hmacKey, (byte)0);
|
|
if (encoded != null) {
|
|
Arrays.fill(encoded, (byte)0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create new WKSSecretKey object from encoded byte array.
|
|
*
|
|
* @param encoded encoded byte array obtained by calling WKSPrivateKey
|
|
* getEncoded() method.
|
|
*
|
|
* @throws IOException on error reading/parsing encoded array
|
|
*/
|
|
protected WKSSecretKey(byte[] encoded)
|
|
throws IOException, CertificateException {
|
|
|
|
int tmp = 0;
|
|
ByteArrayInputStream bis = null;
|
|
DataInputStream dis = null;
|
|
|
|
if (encoded == null || encoded.length == 0) {
|
|
throw new IllegalArgumentException(
|
|
"Input byte array cannot be null");
|
|
}
|
|
|
|
try {
|
|
bis = new ByteArrayInputStream(encoded);
|
|
dis = new DataInputStream(bis);
|
|
|
|
/* creationDate */
|
|
this.creationDate = new Date(dis.readLong());
|
|
|
|
/* SecretKey algorithm */
|
|
this.keyAlgo = dis.readUTF();
|
|
|
|
/* kdfSalt */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_PBKDF2_SALT_SIZE) {
|
|
throw new IOException(
|
|
"Invalid PBKDF2 salt size: " + tmp);
|
|
}
|
|
this.kdfSalt = new byte[tmp];
|
|
dis.readFully(this.kdfSalt);
|
|
|
|
/* kdfIterations */
|
|
tmp = dis.readInt();
|
|
if (tmp < WKS_PBKDF2_MIN_ITERATIONS) {
|
|
throw new IOException(
|
|
"PBKDF2 iterations too small ( " + tmp +
|
|
"), min size: " + WKS_PBKDF2_MIN_ITERATIONS);
|
|
}
|
|
this.kdfIterations = tmp;
|
|
|
|
/* iv */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_ENC_IV_LENGTH) {
|
|
throw new IOException(
|
|
"Invalid IV size: " + tmp);
|
|
}
|
|
this.iv = new byte[tmp];
|
|
dis.readFully(this.iv);
|
|
|
|
/* encrypted key */
|
|
tmp = dis.readInt();
|
|
if (tmp < 0) {
|
|
throw new IOException(
|
|
"Bad encrypted key length, negative");
|
|
}
|
|
this.encryptedKey = new byte[tmp];
|
|
dis.readFully(this.encryptedKey);
|
|
|
|
/* HMAC-SHA512 */
|
|
tmp = dis.readInt();
|
|
if (tmp != WKS_HMAC_KEY_LENGTH) {
|
|
throw new IOException(
|
|
"HMAC length (" + tmp + ") different length than " +
|
|
"expected (" + WKS_HMAC_KEY_LENGTH + ")");
|
|
}
|
|
this.hmacSha512 = new byte[tmp];
|
|
dis.readFully(this.hmacSha512);
|
|
|
|
} catch (Exception e) {
|
|
|
|
if (this.encryptedKey != null) {
|
|
Arrays.fill(this.encryptedKey, (byte)0);
|
|
this.encryptedKey = null;
|
|
}
|
|
if (this.iv != null) {
|
|
Arrays.fill(this.iv, (byte)0);
|
|
this.iv = null;
|
|
}
|
|
if (this.kdfSalt != null) {
|
|
Arrays.fill(this.kdfSalt, (byte)0);
|
|
this.kdfSalt = null;
|
|
}
|
|
if (this.hmacSha512 != null) {
|
|
Arrays.fill(this.hmacSha512, (byte)0);
|
|
this.hmacSha512 = null;
|
|
}
|
|
this.creationDate = null;
|
|
this.kdfIterations = 0;
|
|
|
|
throw e;
|
|
|
|
} finally {
|
|
if (dis != null) {
|
|
dis.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get encoded byte array representation of this object.
|
|
*
|
|
* @param withHMAC include HMAC on end of encoded array when true,
|
|
* otherwise return encoded object without HMAC. HMAC is
|
|
* included when writing out / storing, but not included
|
|
* when we are just using this as a helper method to check
|
|
* the HMAC value inside this object in memory.
|
|
*
|
|
* @return byte array representing this object or null on error
|
|
* @throws IOException on error writing to output stream
|
|
* @throws CertificateEncodingException on error getting
|
|
* Certificate encoding
|
|
*/
|
|
protected synchronized byte[] getEncoded(boolean withHMAC)
|
|
throws IOException, CertificateEncodingException {
|
|
|
|
byte[] out = null;
|
|
ByteArrayOutputStream bos = null;
|
|
DataOutputStream dos = null;
|
|
|
|
if (this.creationDate == null || this.keyAlgo == null ||
|
|
this.kdfSalt == null || this.kdfSalt.length == 0 ||
|
|
this.kdfIterations <= 0 || this.iv == null ||
|
|
this.iv.length == 0 || this.encryptedKey == null ||
|
|
this.encryptedKey.length == 0) {
|
|
log("invalid WKSSecretKey class variables, returning null " +
|
|
"from getEncoded()");
|
|
return null;
|
|
}
|
|
|
|
if (withHMAC && (this.hmacSha512 == null ||
|
|
this.hmacSha512.length == 0)) {
|
|
log("WKSSecretKey HMAC null or zero length, returning null " +
|
|
"from getEncoded()");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
bos = new ByteArrayOutputStream();
|
|
dos = new DataOutputStream(bos);
|
|
|
|
dos.writeLong(this.creationDate.getTime());
|
|
dos.writeUTF(this.keyAlgo);
|
|
dos.writeInt(this.kdfSalt.length);
|
|
dos.write(this.kdfSalt, 0, this.kdfSalt.length);
|
|
dos.writeInt(this.kdfIterations);
|
|
dos.writeInt(this.iv.length);
|
|
dos.write(this.iv, 0, this.iv.length);
|
|
dos.writeInt(this.encryptedKey.length);
|
|
dos.write(this.encryptedKey, 0, this.encryptedKey.length);
|
|
if (withHMAC) {
|
|
dos.writeInt(this.hmacSha512.length);
|
|
dos.write(this.hmacSha512);
|
|
}
|
|
|
|
dos.flush();
|
|
out = bos.toByteArray();
|
|
|
|
} finally {
|
|
if (dos != null) {
|
|
dos.close();
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Decrypt and return plaintext key using provided password.
|
|
*
|
|
* Verifies internally-generated HMAC over WKSSecretKey object
|
|
* contents using password before decrypting key.
|
|
*
|
|
* Other than password, all other information needed should already
|
|
* be stored in this object.
|
|
*
|
|
* @param password password to use for decryption
|
|
* @param keyStore outer KeyStore instance for cache operations
|
|
*/
|
|
protected synchronized byte[] getDecryptedKey(char[] password,
|
|
WolfSSLKeyStore keyStore) throws UnrecoverableKeyException {
|
|
|
|
byte[] plain = null;
|
|
byte[] derivedKey = null;
|
|
byte[] hmac = null;
|
|
byte[] kek = new byte[WKS_ENC_KEY_LENGTH];
|
|
byte[] hmacKey = new byte[WKS_HMAC_KEY_LENGTH];
|
|
byte[] encoded = null;
|
|
boolean fromCache = false;
|
|
|
|
if (password == null || password.length == 0) {
|
|
throw new UnrecoverableKeyException(
|
|
"Unable to decrypt key with null password");
|
|
}
|
|
|
|
try {
|
|
/* Try to get derived key from cache */
|
|
derivedKey = keyStore.getCachedDerivedKey(password,
|
|
this.kdfSalt, this.kdfIterations);
|
|
|
|
if (derivedKey != null) {
|
|
fromCache = true;
|
|
} else {
|
|
/* Cache miss, derive encryption key from password using
|
|
* PBKDF2-HMAC-SHA512. Generate a 96 byte key in total,
|
|
* split between 32-byte AES-CBC-256 and 64-byte
|
|
* HMAC-SHA512 key. */
|
|
derivedKey = deriveKeyFromPassword(password, this.kdfSalt,
|
|
this.kdfIterations,
|
|
WKS_ENC_KEY_LENGTH + WKS_HMAC_KEY_LENGTH);
|
|
|
|
if (derivedKey == null) {
|
|
throw new KeyStoreException(
|
|
"Error deriving key decryption key, got null key");
|
|
}
|
|
}
|
|
|
|
/* Split key into decrypt + HMAC keys */
|
|
System.arraycopy(derivedKey, 0, kek, 0, kek.length);
|
|
System.arraycopy(derivedKey, kek.length, hmacKey, 0,
|
|
hmacKey.length);
|
|
|
|
/* Get encoded byte[] of object class variables without HMAC */
|
|
encoded = getEncoded(false);
|
|
if (encoded == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to get encoded WKSSecretKey for HMAC");
|
|
}
|
|
|
|
/* Re-generate internal HMAC over object contents */
|
|
hmac = generateHmac(hmacKey, encoded);
|
|
if (hmac == null) {
|
|
throw new KeyStoreException(
|
|
"Failed to regenerate HMAC over WKSSecretKey");
|
|
}
|
|
|
|
/* Verify HMAC first before decrypting key */
|
|
if (!WolfCrypt.ConstantCompare(hmac, this.hmacSha512)) {
|
|
if (fromCache) {
|
|
/* Invalidate the cache entry that gave us wrong key */
|
|
keyStore.invalidateCacheEntry(password, this.kdfSalt,
|
|
this.kdfIterations);
|
|
}
|
|
throw new KeyStoreException(
|
|
"HMAC verification failed on WKSSecretKey, entry " +
|
|
"corrupted or wrong password");
|
|
} else {
|
|
log("HMAC verification successful on WKSSecretKey");
|
|
}
|
|
|
|
/* HMAC verified - now safe to cache the derived key */
|
|
if (!fromCache) {
|
|
keyStore.cacheDerivedKey(password, this.kdfSalt,
|
|
this.kdfIterations, derivedKey);
|
|
}
|
|
|
|
/* Decrypt encrypted key with KEK */
|
|
plain = decryptKey(this.encryptedKey, kek, this.iv);
|
|
if (plain == null) {
|
|
throw new UnrecoverableKeyException(
|
|
"Unable to decrypt protected key");
|
|
}
|
|
|
|
} catch (KeyStoreException | IOException |
|
|
CertificateEncodingException e) {
|
|
if (plain != null) {
|
|
Arrays.fill(plain, (byte)0);
|
|
}
|
|
throw new UnrecoverableKeyException(e.getMessage());
|
|
|
|
} finally {
|
|
Arrays.fill(kek, (byte)0);
|
|
Arrays.fill(hmacKey, (byte)0);
|
|
if (derivedKey != null) {
|
|
Arrays.fill(derivedKey, (byte)0);
|
|
}
|
|
if (hmac != null) {
|
|
Arrays.fill(hmac, (byte)0);
|
|
}
|
|
if (encoded != null) {
|
|
Arrays.fill(encoded, (byte)0);
|
|
}
|
|
}
|
|
|
|
return plain;
|
|
}
|
|
}
|
|
}
|
|
|