purge unused ZipSigner code
This includes some references to the "BC" provider that now do not need to be ported: https://android-developers.googleblog.com/2018/03/cryptography-changes-in-android-p.html
This commit is contained in:
parent
2c9737f126
commit
12a677a225
|
@ -18,6 +18,8 @@
|
|||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
import org.bouncycastle.util.encoders.HexEncoder;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
|
|
|
@ -1,182 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner;
|
||||
|
||||
/*
|
||||
This file is a copy of org.bouncycastle.util.encoders.HexEncoder.
|
||||
|
||||
Please note: our license is an adaptation of the MIT X11 License and should be read as such.
|
||||
License
|
||||
|
||||
Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle (http://www.bouncycastle.org)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class HexEncoder {
|
||||
protected final byte[] encodingTable =
|
||||
{
|
||||
(byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7',
|
||||
(byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f'
|
||||
};
|
||||
|
||||
/*
|
||||
* set up the decoding table.
|
||||
*/
|
||||
protected final byte[] decodingTable = new byte[128];
|
||||
|
||||
protected void initialiseDecodingTable() {
|
||||
for (int i = 0; i < encodingTable.length; i++) {
|
||||
decodingTable[encodingTable[i]] = (byte) i;
|
||||
}
|
||||
|
||||
decodingTable['A'] = decodingTable['a'];
|
||||
decodingTable['B'] = decodingTable['b'];
|
||||
decodingTable['C'] = decodingTable['c'];
|
||||
decodingTable['D'] = decodingTable['d'];
|
||||
decodingTable['E'] = decodingTable['e'];
|
||||
decodingTable['F'] = decodingTable['f'];
|
||||
}
|
||||
|
||||
public HexEncoder() {
|
||||
initialiseDecodingTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* encode the input data producing a Hex output stream.
|
||||
*
|
||||
* @return the number of bytes produced.
|
||||
*/
|
||||
public int encode(
|
||||
byte[] data,
|
||||
int off,
|
||||
int length,
|
||||
OutputStream out)
|
||||
throws IOException {
|
||||
for (int i = off; i < (off + length); i++) {
|
||||
int v = data[i] & 0xff;
|
||||
|
||||
out.write(encodingTable[(v >>> 4)]);
|
||||
out.write(encodingTable[v & 0xf]);
|
||||
}
|
||||
|
||||
return length * 2;
|
||||
}
|
||||
|
||||
private boolean ignore(
|
||||
char c) {
|
||||
return (c == '\n' || c == '\r' || c == '\t' || c == ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* decode the Hex encoded byte data writing it to the given output stream,
|
||||
* whitespace characters will be ignored.
|
||||
*
|
||||
* @return the number of bytes produced.
|
||||
*/
|
||||
public int decode(
|
||||
byte[] data,
|
||||
int off,
|
||||
int length,
|
||||
OutputStream out)
|
||||
throws IOException {
|
||||
byte b1, b2;
|
||||
int outLen = 0;
|
||||
|
||||
int end = off + length;
|
||||
|
||||
while (end > off) {
|
||||
if (!ignore((char) data[end - 1])) {
|
||||
break;
|
||||
}
|
||||
|
||||
end--;
|
||||
}
|
||||
|
||||
int i = off;
|
||||
while (i < end) {
|
||||
while (i < end && ignore((char) data[i])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b1 = decodingTable[data[i++]];
|
||||
|
||||
while (i < end && ignore((char) data[i])) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b2 = decodingTable[data[i++]];
|
||||
|
||||
out.write((b1 << 4) | b2);
|
||||
|
||||
outLen++;
|
||||
}
|
||||
|
||||
return outLen;
|
||||
}
|
||||
|
||||
/**
|
||||
* decode the Hex encoded String data writing it to the given output stream,
|
||||
* whitespace characters will be ignored.
|
||||
*
|
||||
* @return the number of bytes produced.
|
||||
*/
|
||||
public int decode(
|
||||
String data,
|
||||
OutputStream out)
|
||||
throws IOException {
|
||||
byte b1, b2;
|
||||
int length = 0;
|
||||
|
||||
int end = data.length();
|
||||
|
||||
while (end > 0) {
|
||||
if (!ignore(data.charAt(end - 1))) {
|
||||
break;
|
||||
}
|
||||
|
||||
end--;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
while (i < end) {
|
||||
while (i < end && ignore(data.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b1 = decodingTable[data.charAt(i++)];
|
||||
|
||||
while (i < end && ignore(data.charAt(i))) {
|
||||
i++;
|
||||
}
|
||||
|
||||
b2 = decodingTable[data.charAt(i++)];
|
||||
|
||||
out.write((b1 << 4) | b2);
|
||||
|
||||
length++;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import kellinwood.security.zipsigner.KeySet;
|
||||
import org.bouncycastle.jce.X509Principal;
|
||||
import org.bouncycastle.x509.X509V3CertificateGenerator;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
/**
|
||||
* All methods create self-signed certificates.
|
||||
*/
|
||||
public class CertCreator {
|
||||
|
||||
/**
|
||||
* Creates a new keystore and self-signed key. The key will have the same password as the key, and will be
|
||||
* RSA 2048, with the cert signed using SHA1withRSA. The certificate will have a validity of
|
||||
* 30 years).
|
||||
*
|
||||
* @param storePath - pathname of the new keystore file
|
||||
* @param password - keystore and key password
|
||||
* @param keyName - the new key will have this as its alias within the keystore
|
||||
* @param distinguishedNameValues - contains Country, State, Locality,...,Common Name, etc.
|
||||
*/
|
||||
public static void createKeystoreAndKey(String storePath, char[] password,
|
||||
String keyName, DistinguishedNameValues distinguishedNameValues) {
|
||||
createKeystoreAndKey(storePath, password, "RSA", 2048, keyName, password, "SHA1withRSA", 30,
|
||||
distinguishedNameValues);
|
||||
}
|
||||
|
||||
|
||||
public static KeySet createKeystoreAndKey(String storePath, char[] storePass,
|
||||
String keyAlgorithm, int keySize, String keyName, char[] keyPass,
|
||||
String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
|
||||
KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
|
||||
distinguishedNameValues);
|
||||
|
||||
|
||||
KeyStore privateKS = KeyStoreFileManager.createKeyStore(storePath, storePass);
|
||||
|
||||
privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
|
||||
keyPass,
|
||||
new java.security.cert.Certificate[]{keySet.getPublicKey()});
|
||||
|
||||
File sfile = new File(storePath);
|
||||
if (sfile.exists()) {
|
||||
throw new IOException("File already exists: " + storePath);
|
||||
}
|
||||
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
|
||||
|
||||
return keySet;
|
||||
} catch (RuntimeException x) {
|
||||
throw x;
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new key and store it in an existing keystore.
|
||||
*/
|
||||
public static KeySet createKey(String storePath, char[] storePass,
|
||||
String keyAlgorithm, int keySize, String keyName, char[] keyPass,
|
||||
String certSignatureAlgorithm, int certValidityYears,
|
||||
DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
|
||||
KeySet keySet = createKey(keyAlgorithm, keySize, keyName, certSignatureAlgorithm, certValidityYears,
|
||||
distinguishedNameValues);
|
||||
|
||||
KeyStore privateKS = KeyStoreFileManager.loadKeyStore(storePath, storePass);
|
||||
|
||||
privateKS.setKeyEntry(keyName, keySet.getPrivateKey(),
|
||||
keyPass,
|
||||
new java.security.cert.Certificate[]{keySet.getPublicKey()});
|
||||
|
||||
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
|
||||
|
||||
return keySet;
|
||||
|
||||
} catch (RuntimeException x) {
|
||||
throw x;
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeySet createKey(String keyAlgorithm, int keySize, String keyName,
|
||||
String certSignatureAlgorithm, int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
|
||||
keyPairGenerator.initialize(keySize);
|
||||
KeyPair KPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
|
||||
|
||||
X509Principal principal = distinguishedNameValues.getPrincipal();
|
||||
|
||||
// generate a postitive serial number
|
||||
BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
||||
while (serialNumber.compareTo(BigInteger.ZERO) < 0) {
|
||||
serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
||||
}
|
||||
v3CertGen.setSerialNumber(serialNumber);
|
||||
v3CertGen.setIssuerDN(principal);
|
||||
v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L));
|
||||
v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + (1000L * 60L * 60L * 24L * 366L * (long) certValidityYears)));
|
||||
v3CertGen.setSubjectDN(principal);
|
||||
|
||||
v3CertGen.setPublicKey(KPair.getPublic());
|
||||
v3CertGen.setSignatureAlgorithm(certSignatureAlgorithm);
|
||||
|
||||
X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(), "BC");
|
||||
|
||||
KeySet keySet = new KeySet();
|
||||
keySet.setName(keyName);
|
||||
keySet.setPrivateKey(KPair.getPrivate());
|
||||
keySet.setPublicKey(PKCertificate);
|
||||
return keySet;
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.jce.X509Principal;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for dealing with the distinguished name RDNs.
|
||||
*/
|
||||
public class DistinguishedNameValues extends LinkedHashMap<ASN1ObjectIdentifier, String> {
|
||||
|
||||
public DistinguishedNameValues() {
|
||||
put(BCStyle.C, null);
|
||||
put(BCStyle.ST, null);
|
||||
put(BCStyle.L, null);
|
||||
put(BCStyle.STREET, null);
|
||||
put(BCStyle.O, null);
|
||||
put(BCStyle.OU, null);
|
||||
put(BCStyle.CN, null);
|
||||
}
|
||||
|
||||
public String put(ASN1ObjectIdentifier oid, String value) {
|
||||
if (value != null && value.equals("")) value = null;
|
||||
if (containsKey(oid)) super.put(oid, value); // preserve original ordering
|
||||
else {
|
||||
super.put(oid, value);
|
||||
// String cn = remove(BCStyle.CN); // CN will always be last.
|
||||
// put(BCStyle.CN,cn);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
put(BCStyle.C, country);
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
put(BCStyle.ST, state);
|
||||
}
|
||||
|
||||
public void setLocality(String locality) {
|
||||
put(BCStyle.L, locality);
|
||||
}
|
||||
|
||||
public void setStreet(String street) {
|
||||
put(BCStyle.STREET, street);
|
||||
}
|
||||
|
||||
public void setOrganization(String organization) {
|
||||
put(BCStyle.O, organization);
|
||||
}
|
||||
|
||||
public void setOrganizationalUnit(String organizationalUnit) {
|
||||
put(BCStyle.OU, organizationalUnit);
|
||||
}
|
||||
|
||||
public void setCommonName(String commonName) {
|
||||
put(BCStyle.CN, commonName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
int result = 0;
|
||||
for (String value : values()) {
|
||||
if (value != null) result += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public X509Principal getPrincipal() {
|
||||
Vector<ASN1ObjectIdentifier> oids = new Vector<ASN1ObjectIdentifier>();
|
||||
Vector<String> values = new Vector<String>();
|
||||
|
||||
for (Map.Entry<ASN1ObjectIdentifier, String> entry : entrySet()) {
|
||||
if (entry.getValue() != null && !entry.getValue().equals("")) {
|
||||
oids.add(entry.getKey());
|
||||
values.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return new X509Principal(oids, values);
|
||||
}
|
||||
}
|
|
@ -1,500 +0,0 @@
|
|||
/* JKS.java -- implementation of the "JKS" key store.
|
||||
Copyright (C) 2003 Casey Marshall <rsdio@metastatic.org>
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and
|
||||
its documentation for any purpose is hereby granted without fee,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation. No representations are made about the
|
||||
suitability of this software for any purpose. It is provided "as is"
|
||||
without express or implied warranty.
|
||||
|
||||
This program was derived by reverse-engineering Sun's own
|
||||
implementation, using only the public API that is available in the 1.4.1
|
||||
JDK. Hence nothing in this program is, or is derived from, anything
|
||||
copyrighted by Sun Microsystems. While the "Binary Evaluation License
|
||||
Agreement" that the JDK is licensed under contains blanket statements
|
||||
that forbid reverse-engineering (among other things), it is my position
|
||||
that US copyright law does not and cannot forbid reverse-engineering of
|
||||
software to produce a compatible implementation. There are, in fact,
|
||||
numerous clauses in copyright law that specifically allow
|
||||
reverse-engineering, and therefore I believe it is outside of Sun's
|
||||
power to enforce restrictions on reverse-engineering of their software,
|
||||
and it is irresponsible for them to claim they can. */
|
||||
|
||||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.KeyStoreSpi;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* This is an implementation of Sun's proprietary key store
|
||||
* algorithm, called "JKS" for "Java Key Store". This implementation was
|
||||
* created entirely through reverse-engineering.
|
||||
* <p>
|
||||
* <p>The format of JKS files is, from the start of the file:
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li>Magic bytes. This is a four-byte integer, in big-endian byte
|
||||
* order, equal to <code>0xFEEDFEED</code>.</li>
|
||||
* <li>The version number (probably), as a four-byte integer (all
|
||||
* multibyte integral types are in big-endian byte order). The current
|
||||
* version number (in modern distributions of the JDK) is 2.</li>
|
||||
* <li>The number of entrires in this keystore, as a four-byte
|
||||
* integer. Call this value <i>n</i></li>
|
||||
* <li>Then, <i>n</i> times:
|
||||
* <ol>
|
||||
* <li>The entry type, a four-byte int. The value 1 denotes a private
|
||||
* key entry, and 2 denotes a trusted certificate.</li>
|
||||
* <li>The entry's alias, formatted as strings such as those written
|
||||
* by <a
|
||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/java/io/DataOutput.html#writeUTF(java.lang.String)">DataOutput.writeUTF(String)</a>.</li>
|
||||
* <li>An eight-byte integer, representing the entry's creation date,
|
||||
* in milliseconds since the epoch.
|
||||
* <p>
|
||||
* <p>Then, if the entry is a private key entry:
|
||||
* <ol>
|
||||
* <li>The size of the encoded key as a four-byte int, then that
|
||||
* number of bytes. The encoded key is the DER encoded bytes of the
|
||||
* <a
|
||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/javax/crypto/EncryptedPrivateKeyInfo.html">EncryptedPrivateKeyInfo</a> structure (the
|
||||
* encryption algorithm is discussed later).</li>
|
||||
* <li>A four-byte integer, followed by that many encoded
|
||||
* certificates, encoded as described in the trusted certificates
|
||||
* section.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* <p>Otherwise, the entry is a trusted certificate, which is encoded
|
||||
* as the name of the encoding algorithm (e.g. X.509), encoded the same
|
||||
* way as alias names. Then, a four-byte integer representing the size
|
||||
* of the encoded certificate, then that many bytes representing the
|
||||
* encoded certificate (e.g. the DER bytes in the case of X.509).
|
||||
* </li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* <li>Then, the signature.</li>
|
||||
* </ol>
|
||||
* </ol>
|
||||
* </li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* <p>(See <a href="genkey.java">this file</a> for some idea of how I
|
||||
* was able to figure out these algorithms)</p>
|
||||
* <p>
|
||||
* <p>Decrypting the key works as follows:
|
||||
* <p>
|
||||
* <ol>
|
||||
* <li>The key length is the length of the ciphertext minus 40. The
|
||||
* encrypted key, <code>ekey</code>, is the middle bytes of the
|
||||
* ciphertext.</li>
|
||||
* <li>Take the first 20 bytes of the encrypted key as a seed value,
|
||||
* <code>K[0]</code>.</li>
|
||||
* <li>Compute <code>K[1] ... K[n]</code>, where
|
||||
* <code>|K[i]| = 20</code>, <code>n = ceil(|ekey| / 20)</code>, and
|
||||
* <code>K[i] = SHA-1(UTF-16BE(password) + K[i-1])</code>.</li>
|
||||
* <li><code>key = ekey ^ (K[1] + ... + K[n])</code>.</li>
|
||||
* <li>The last 20 bytes are the checksum, computed as <code>H =
|
||||
* SHA-1(UTF-16BE(password) + key)</code>. If this value does not match
|
||||
* the last 20 bytes of the ciphertext, output <code>FAIL</code>. Otherwise,
|
||||
* output <code>key</code>.</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* <p>The signature is defined as <code>SHA-1(UTF-16BE(password) +
|
||||
* US_ASCII("Mighty Aphrodite") + encoded_keystore)</code> (yup, Sun
|
||||
* engineers are just that clever).
|
||||
* <p>
|
||||
* <p>(Above, SHA-1 denotes the secure hash algorithm, UTF-16BE the
|
||||
* big-endian byte representation of a UTF-16 string, and US_ASCII the
|
||||
* ASCII byte representation of the string.)
|
||||
* <p>
|
||||
* <p>The source code of this class should be available in the file <a
|
||||
* href="http://metastatic.org/source/JKS.java">JKS.java</a>.
|
||||
*
|
||||
* @author Casey Marshall (rsdio@metastatic.org)
|
||||
* <p>
|
||||
* Changes by Ken Ellinwood:
|
||||
* ** Fixed a NullPointerException in engineLoad(). This method must return gracefully if the keystore input stream is null.
|
||||
* ** engineGetCertificateEntry() was updated to return the first cert in the chain for private key entries.
|
||||
* ** Lowercase the alias names, otherwise keytool chokes on the file created by this code.
|
||||
* ** Fixed the integrity check in engineLoad(), previously the exception was never thrown regardless of password value.
|
||||
*/
|
||||
public class JKS extends KeyStoreSpi {
|
||||
|
||||
// Constants and fields.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ah, Sun. So goddamned clever with those magic bytes.
|
||||
*/
|
||||
private static final int MAGIC = 0xFEEDFEED;
|
||||
|
||||
private static final int PRIVATE_KEY = 1;
|
||||
private static final int TRUSTED_CERT = 2;
|
||||
|
||||
private final Vector aliases;
|
||||
private final HashMap trustedCerts;
|
||||
private final HashMap privateKeys;
|
||||
private final HashMap certChains;
|
||||
private final HashMap dates;
|
||||
|
||||
// Constructor.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public JKS() {
|
||||
super();
|
||||
aliases = new Vector();
|
||||
trustedCerts = new HashMap();
|
||||
privateKeys = new HashMap();
|
||||
certChains = new HashMap();
|
||||
dates = new HashMap();
|
||||
|
||||
}
|
||||
|
||||
// Instance methods.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
public Key engineGetKey(String alias, char[] password)
|
||||
throws NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (!privateKeys.containsKey(alias))
|
||||
return null;
|
||||
byte[] key = decryptKey((byte[]) privateKeys.get(alias),
|
||||
charsToBytes(password));
|
||||
Certificate[] chain = engineGetCertificateChain(alias);
|
||||
if (chain.length > 0) {
|
||||
try {
|
||||
// Private and public keys MUST have the same algorithm.
|
||||
KeyFactory fact = KeyFactory.getInstance(
|
||||
chain[0].getPublicKey().getAlgorithm());
|
||||
return fact.generatePrivate(new PKCS8EncodedKeySpec(key));
|
||||
} catch (InvalidKeySpecException x) {
|
||||
throw new UnrecoverableKeyException(x.getMessage());
|
||||
}
|
||||
} else
|
||||
return new SecretKeySpec(key, alias);
|
||||
}
|
||||
|
||||
public Certificate[] engineGetCertificateChain(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return (Certificate[]) certChains.get(alias);
|
||||
}
|
||||
|
||||
public Certificate engineGetCertificate(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (engineIsKeyEntry(alias)) {
|
||||
Certificate[] certChain = (Certificate[]) certChains.get(alias);
|
||||
if (certChain != null && certChain.length > 0) return certChain[0];
|
||||
}
|
||||
return (Certificate) trustedCerts.get(alias);
|
||||
}
|
||||
|
||||
public Date engineGetCreationDate(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return (Date) dates.get(alias);
|
||||
}
|
||||
|
||||
// XXX implement writing methods.
|
||||
|
||||
public void engineSetKeyEntry(String alias, Key key, char[] passwd, Certificate[] certChain)
|
||||
throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (trustedCerts.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + " is a trusted certificate entry");
|
||||
privateKeys.put(alias, encryptKey(key, charsToBytes(passwd)));
|
||||
if (certChain != null)
|
||||
certChains.put(alias, certChain);
|
||||
else
|
||||
certChains.put(alias, new Certificate[0]);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void engineSetKeyEntry(String alias, byte[] encodedKey, Certificate[] certChain)
|
||||
throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (trustedCerts.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + "\" is a trusted certificate entry");
|
||||
try {
|
||||
new EncryptedPrivateKeyInfo(encodedKey);
|
||||
} catch (IOException ioe) {
|
||||
throw new KeyStoreException("encoded key is not an EncryptedPrivateKeyInfo");
|
||||
}
|
||||
privateKeys.put(alias, encodedKey);
|
||||
if (certChain != null)
|
||||
certChains.put(alias, certChain);
|
||||
else
|
||||
certChains.put(alias, new Certificate[0]);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void engineSetCertificateEntry(String alias, Certificate cert)
|
||||
throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
if (privateKeys.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + "\" is a private key entry");
|
||||
if (cert == null)
|
||||
throw new NullPointerException();
|
||||
trustedCerts.put(alias, cert);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void engineDeleteEntry(String alias) throws KeyStoreException {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
aliases.remove(alias);
|
||||
}
|
||||
|
||||
public Enumeration engineAliases() {
|
||||
return aliases.elements();
|
||||
}
|
||||
|
||||
public boolean engineContainsAlias(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return aliases.contains(alias);
|
||||
}
|
||||
|
||||
public int engineSize() {
|
||||
return aliases.size();
|
||||
}
|
||||
|
||||
public boolean engineIsKeyEntry(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return privateKeys.containsKey(alias);
|
||||
}
|
||||
|
||||
public boolean engineIsCertificateEntry(String alias) {
|
||||
alias = alias.toLowerCase(Locale.ENGLISH);
|
||||
return trustedCerts.containsKey(alias);
|
||||
}
|
||||
|
||||
public String engineGetCertificateAlias(Certificate cert) {
|
||||
for (Iterator keys = trustedCerts.keySet().iterator(); keys.hasNext(); ) {
|
||||
String alias = (String) keys.next();
|
||||
if (cert.equals(trustedCerts.get(alias)))
|
||||
return alias;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void engineStore(OutputStream out, char[] passwd)
|
||||
throws IOException, NoSuchAlgorithmException, CertificateException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
md.update(charsToBytes(passwd));
|
||||
md.update("Mighty Aphrodite".getBytes("UTF-8"));
|
||||
DataOutputStream dout = new DataOutputStream(new DigestOutputStream(out, md));
|
||||
dout.writeInt(MAGIC);
|
||||
dout.writeInt(2);
|
||||
dout.writeInt(aliases.size());
|
||||
for (Enumeration e = aliases.elements(); e.hasMoreElements(); ) {
|
||||
String alias = (String) e.nextElement();
|
||||
if (trustedCerts.containsKey(alias)) {
|
||||
dout.writeInt(TRUSTED_CERT);
|
||||
dout.writeUTF(alias);
|
||||
dout.writeLong(((Date) dates.get(alias)).getTime());
|
||||
writeCert(dout, (Certificate) trustedCerts.get(alias));
|
||||
} else {
|
||||
dout.writeInt(PRIVATE_KEY);
|
||||
dout.writeUTF(alias);
|
||||
dout.writeLong(((Date) dates.get(alias)).getTime());
|
||||
byte[] key = (byte[]) privateKeys.get(alias);
|
||||
dout.writeInt(key.length);
|
||||
dout.write(key);
|
||||
Certificate[] chain = (Certificate[]) certChains.get(alias);
|
||||
dout.writeInt(chain.length);
|
||||
for (int i = 0; i < chain.length; i++)
|
||||
writeCert(dout, chain[i]);
|
||||
}
|
||||
}
|
||||
byte[] digest = md.digest();
|
||||
dout.write(digest);
|
||||
}
|
||||
|
||||
public void engineLoad(InputStream in, char[] passwd)
|
||||
throws IOException, NoSuchAlgorithmException, CertificateException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA");
|
||||
if (passwd != null) md.update(charsToBytes(passwd));
|
||||
md.update("Mighty Aphrodite".getBytes("UTF-8")); // HAR HAR
|
||||
aliases.clear();
|
||||
trustedCerts.clear();
|
||||
privateKeys.clear();
|
||||
certChains.clear();
|
||||
dates.clear();
|
||||
if (in == null) return;
|
||||
DataInputStream din = new DataInputStream(new DigestInputStream(in, md));
|
||||
if (din.readInt() != MAGIC)
|
||||
throw new IOException("not a JavaKeyStore");
|
||||
din.readInt(); // version no.
|
||||
final int n = din.readInt();
|
||||
aliases.ensureCapacity(n);
|
||||
if (n < 0)
|
||||
throw new LoadKeystoreException("Malformed key store");
|
||||
for (int i = 0; i < n; i++) {
|
||||
int type = din.readInt();
|
||||
String alias = din.readUTF();
|
||||
aliases.add(alias);
|
||||
dates.put(alias, new Date(din.readLong()));
|
||||
switch (type) {
|
||||
case PRIVATE_KEY:
|
||||
int len = din.readInt();
|
||||
byte[] encoded = new byte[len];
|
||||
din.read(encoded);
|
||||
privateKeys.put(alias, encoded);
|
||||
int count = din.readInt();
|
||||
Certificate[] chain = new Certificate[count];
|
||||
for (int j = 0; j < count; j++)
|
||||
chain[j] = readCert(din);
|
||||
certChains.put(alias, chain);
|
||||
break;
|
||||
|
||||
case TRUSTED_CERT:
|
||||
trustedCerts.put(alias, readCert(din));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LoadKeystoreException("Malformed key store");
|
||||
}
|
||||
}
|
||||
|
||||
if (passwd != null) {
|
||||
byte[] computedHash = md.digest();
|
||||
byte[] storedHash = new byte[20];
|
||||
din.read(storedHash);
|
||||
if (!MessageDigest.isEqual(storedHash, computedHash)) {
|
||||
throw new LoadKeystoreException("Incorrect password, or integrity check failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Own methods.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
private static Certificate readCert(DataInputStream in)
|
||||
throws IOException, CertificateException, NoSuchAlgorithmException {
|
||||
String type = in.readUTF();
|
||||
int len = in.readInt();
|
||||
byte[] encoded = new byte[len];
|
||||
in.read(encoded);
|
||||
CertificateFactory factory = CertificateFactory.getInstance(type);
|
||||
return factory.generateCertificate(new ByteArrayInputStream(encoded));
|
||||
}
|
||||
|
||||
private static void writeCert(DataOutputStream dout, Certificate cert)
|
||||
throws IOException, CertificateException {
|
||||
dout.writeUTF(cert.getType());
|
||||
byte[] b = cert.getEncoded();
|
||||
dout.writeInt(b.length);
|
||||
dout.write(b);
|
||||
}
|
||||
|
||||
private static byte[] decryptKey(byte[] encryptedPKI, byte[] passwd)
|
||||
throws UnrecoverableKeyException {
|
||||
try {
|
||||
EncryptedPrivateKeyInfo epki =
|
||||
new EncryptedPrivateKeyInfo(encryptedPKI);
|
||||
byte[] encr = epki.getEncryptedData();
|
||||
byte[] keystream = new byte[20];
|
||||
System.arraycopy(encr, 0, keystream, 0, 20);
|
||||
byte[] check = new byte[20];
|
||||
System.arraycopy(encr, encr.length - 20, check, 0, 20);
|
||||
byte[] key = new byte[encr.length - 40];
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
||||
int count = 0;
|
||||
while (count < key.length) {
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(keystream);
|
||||
sha.digest(keystream, 0, keystream.length);
|
||||
for (int i = 0; i < keystream.length && count < key.length; i++) {
|
||||
key[count] = (byte) (keystream[i] ^ encr[count + 20]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(key);
|
||||
if (!MessageDigest.isEqual(check, sha.digest()))
|
||||
throw new UnrecoverableKeyException("checksum mismatch");
|
||||
return key;
|
||||
} catch (Exception x) {
|
||||
throw new UnrecoverableKeyException(x.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptKey(Key key, byte[] passwd)
|
||||
throws KeyStoreException {
|
||||
try {
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
||||
SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
|
||||
byte[] k = key.getEncoded();
|
||||
byte[] encrypted = new byte[k.length + 40];
|
||||
byte[] keystream = rand.getSeed(20);
|
||||
System.arraycopy(keystream, 0, encrypted, 0, 20);
|
||||
int count = 0;
|
||||
while (count < k.length) {
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(keystream);
|
||||
sha.digest(keystream, 0, keystream.length);
|
||||
for (int i = 0; i < keystream.length && count < k.length; i++) {
|
||||
encrypted[count + 20] = (byte) (keystream[i] ^ k[count]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(k);
|
||||
sha.digest(encrypted, encrypted.length - 20, 20);
|
||||
// 1.3.6.1.4.1.42.2.17.1.1 is Sun's private OID for this
|
||||
// encryption algorithm.
|
||||
return new EncryptedPrivateKeyInfo("1.3.6.1.4.1.42.2.17.1.1",
|
||||
encrypted).getEncoded();
|
||||
} catch (Exception x) {
|
||||
throw new KeyStoreException(x.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] charsToBytes(char[] passwd) {
|
||||
byte[] buf = new byte[passwd.length * 2];
|
||||
for (int i = 0, j = 0; i < passwd.length; i++) {
|
||||
buf[j++] = (byte) (passwd[i] >>> 8);
|
||||
buf[j++] = (byte) passwd[i];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
|
||||
import java.security.KeyStore;
|
||||
|
||||
public class JksKeyStore extends KeyStore {
|
||||
|
||||
public JksKeyStore() {
|
||||
super(new JKS(), KeyStoreFileManager.getProvider(), "jks");
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
public class KeyNameConflictException extends Exception {
|
||||
}
|
|
@ -1,285 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
public class KeyStoreFileManager {
|
||||
|
||||
static Provider provider = new BouncyCastleProvider();
|
||||
|
||||
public static Provider getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static void setProvider(Provider provider) {
|
||||
if (KeyStoreFileManager.provider != null) Security.removeProvider(KeyStoreFileManager.provider.getName());
|
||||
KeyStoreFileManager.provider = provider;
|
||||
Security.addProvider(provider);
|
||||
}
|
||||
|
||||
static LoggerInterface logger = LoggerManager.getLogger(KeyStoreFileManager.class.getName());
|
||||
|
||||
static {
|
||||
// Add the bouncycastle version of the BC provider so that the implementation classes returned
|
||||
// from the keystore are all from the bouncycastle libs.
|
||||
Security.addProvider(getProvider());
|
||||
}
|
||||
|
||||
|
||||
public static KeyStore loadKeyStore(String keystorePath, String encodedPassword)
|
||||
throws Exception {
|
||||
char password[] = null;
|
||||
try {
|
||||
if (encodedPassword != null) {
|
||||
password = PasswordObfuscator.getInstance().decodeKeystorePassword(keystorePath, encodedPassword);
|
||||
}
|
||||
return loadKeyStore(keystorePath, password);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyStore createKeyStore(String keystorePath, char[] password)
|
||||
throws Exception {
|
||||
KeyStore ks = null;
|
||||
if (keystorePath.toLowerCase(Locale.ENGLISH).endsWith(".bks")) {
|
||||
ks = KeyStore.getInstance("bks", new BouncyCastleProvider());
|
||||
} else ks = new JksKeyStore();
|
||||
ks.load(null, password);
|
||||
|
||||
return ks;
|
||||
}
|
||||
|
||||
public static KeyStore loadKeyStore(String keystorePath, char[] password)
|
||||
throws Exception {
|
||||
KeyStore ks = null;
|
||||
try {
|
||||
ks = new JksKeyStore();
|
||||
FileInputStream fis = new FileInputStream(keystorePath);
|
||||
ks.load(fis, password);
|
||||
fis.close();
|
||||
return ks;
|
||||
} catch (LoadKeystoreException x) {
|
||||
// This type of exception is thrown when the keystore is a JKS keystore, but the file is malformed
|
||||
// or the validity/password check failed. In this case don't bother to attempt loading it as a BKS keystore.
|
||||
throw x;
|
||||
} catch (Exception x) {
|
||||
// logger.warning( x.getMessage(), x);
|
||||
try {
|
||||
ks = KeyStore.getInstance("bks", getProvider());
|
||||
FileInputStream fis = new FileInputStream(keystorePath);
|
||||
ks.load(fis, password);
|
||||
fis.close();
|
||||
return ks;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to load keystore: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeKeyStore(KeyStore ks, String keystorePath, String encodedPassword)
|
||||
throws Exception {
|
||||
char password[] = null;
|
||||
try {
|
||||
password = PasswordObfuscator.getInstance().decodeKeystorePassword(keystorePath, encodedPassword);
|
||||
writeKeyStore(ks, keystorePath, password);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeKeyStore(KeyStore ks, String keystorePath, char[] password)
|
||||
throws Exception {
|
||||
|
||||
File keystoreFile = new File(keystorePath);
|
||||
try {
|
||||
if (keystoreFile.exists()) {
|
||||
// I've had some trouble saving new versions of the keystore file in which the file becomes empty/corrupt.
|
||||
// Saving the new version to a new file and creating a backup of the old version.
|
||||
File tmpFile = File.createTempFile(keystoreFile.getName(), null, keystoreFile.getParentFile());
|
||||
FileOutputStream fos = new FileOutputStream(tmpFile);
|
||||
ks.store(fos, password);
|
||||
fos.flush();
|
||||
fos.close();
|
||||
/* create a backup of the previous version
|
||||
int i = 1;
|
||||
File backup = new File( keystorePath + "." + i + ".bak");
|
||||
while (backup.exists()) {
|
||||
i += 1;
|
||||
backup = new File( keystorePath + "." + i + ".bak");
|
||||
}
|
||||
renameTo(keystoreFile, backup);
|
||||
*/
|
||||
renameTo(tmpFile, keystoreFile);
|
||||
} else {
|
||||
FileOutputStream fos = new FileOutputStream(keystorePath);
|
||||
ks.store(fos, password);
|
||||
fos.close();
|
||||
}
|
||||
} catch (Exception x) {
|
||||
try {
|
||||
File logfile = File.createTempFile("zipsigner-error", ".log", keystoreFile.getParentFile());
|
||||
PrintWriter pw = new PrintWriter(new FileWriter(logfile));
|
||||
x.printStackTrace(pw);
|
||||
pw.flush();
|
||||
pw.close();
|
||||
} catch (Exception y) {
|
||||
}
|
||||
throw x;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
|
||||
if (destFile.exists() && destFile.isDirectory()) {
|
||||
throw new IOException("Destination '" + destFile + "' exists but is a directory");
|
||||
}
|
||||
|
||||
FileInputStream input = new FileInputStream(srcFile);
|
||||
try {
|
||||
FileOutputStream output = new FileOutputStream(destFile);
|
||||
try {
|
||||
byte[] buffer = new byte[4096];
|
||||
long count = 0;
|
||||
int n = 0;
|
||||
while (-1 != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
count += n;
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
output.close();
|
||||
} catch (IOException x) {
|
||||
} // Ignore
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
input.close();
|
||||
} catch (IOException x) {
|
||||
}
|
||||
}
|
||||
|
||||
if (srcFile.length() != destFile.length()) {
|
||||
throw new IOException("Failed to copy full contents from '" +
|
||||
srcFile + "' to '" + destFile + "'");
|
||||
}
|
||||
if (preserveFileDate) {
|
||||
destFile.setLastModified(srcFile.lastModified());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void renameTo(File fromFile, File toFile)
|
||||
throws IOException {
|
||||
copyFile(fromFile, toFile, true);
|
||||
if (!fromFile.delete()) throw new IOException("Failed to delete " + fromFile);
|
||||
}
|
||||
|
||||
public static void deleteKey(String storePath, String storePass, String keyName)
|
||||
throws Exception {
|
||||
KeyStore ks = loadKeyStore(storePath, storePass);
|
||||
ks.deleteEntry(keyName);
|
||||
writeKeyStore(ks, storePath, storePass);
|
||||
}
|
||||
|
||||
public static String renameKey(String keystorePath, String storePass, String oldKeyName, String newKeyName, String keyPass)
|
||||
throws Exception {
|
||||
char[] keyPw = null;
|
||||
|
||||
try {
|
||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
||||
if (ks instanceof JksKeyStore) newKeyName = newKeyName.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (ks.containsAlias(newKeyName)) throw new KeyNameConflictException();
|
||||
|
||||
keyPw = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, oldKeyName, keyPass);
|
||||
Key key = ks.getKey(oldKeyName, keyPw);
|
||||
Certificate cert = ks.getCertificate(oldKeyName);
|
||||
|
||||
ks.setKeyEntry(newKeyName, key, keyPw, new Certificate[]{cert});
|
||||
ks.deleteEntry(oldKeyName);
|
||||
|
||||
writeKeyStore(ks, keystorePath, storePass);
|
||||
return newKeyName;
|
||||
} finally {
|
||||
PasswordObfuscator.flush(keyPw);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyStore.Entry getKeyEntry(String keystorePath, String storePass, String keyName, String keyPass)
|
||||
throws Exception {
|
||||
char[] keyPw = null;
|
||||
KeyStore.PasswordProtection passwordProtection = null;
|
||||
|
||||
try {
|
||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
||||
keyPw = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, keyName, keyPass);
|
||||
passwordProtection = new KeyStore.PasswordProtection(keyPw);
|
||||
return ks.getEntry(keyName, passwordProtection);
|
||||
} finally {
|
||||
if (keyPw != null) PasswordObfuscator.flush(keyPw);
|
||||
if (passwordProtection != null) passwordProtection.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean containsKey(String keystorePath, String storePass, String keyName)
|
||||
throws Exception {
|
||||
KeyStore ks = loadKeyStore(keystorePath, storePass);
|
||||
return ks.containsAlias(keyName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param keystorePath
|
||||
* @param encodedPassword
|
||||
* @throws Exception if the password is invalid
|
||||
*/
|
||||
public static void validateKeystorePassword(String keystorePath, String encodedPassword)
|
||||
throws Exception {
|
||||
char[] password = null;
|
||||
try {
|
||||
KeyStore ks = KeyStoreFileManager.loadKeyStore(keystorePath, encodedPassword);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keystorePath
|
||||
* @param keyName
|
||||
* @param encodedPassword
|
||||
* @throws java.security.UnrecoverableKeyException if the password is invalid
|
||||
*/
|
||||
public static void validateKeyPassword(String keystorePath, String keyName, String encodedPassword)
|
||||
throws Exception {
|
||||
char[] password = null;
|
||||
try {
|
||||
KeyStore ks = KeyStoreFileManager.loadKeyStore(keystorePath, (char[]) null);
|
||||
password = PasswordObfuscator.getInstance().decodeAliasPassword(keystorePath, keyName, encodedPassword);
|
||||
ks.getKey(keyName, password);
|
||||
} finally {
|
||||
if (password != null) PasswordObfuscator.flush(password);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Thrown by JKS.engineLoad() for errors that occur after determining the keystore is actually a JKS keystore.
|
||||
*/
|
||||
public class LoadKeystoreException extends IOException {
|
||||
|
||||
public LoadKeystoreException() {
|
||||
}
|
||||
|
||||
public LoadKeystoreException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public LoadKeystoreException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public LoadKeystoreException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
|
||||
package kellinwood.security.zipsigner.optional;
|
||||
|
||||
import android.util.Base64;
|
||||
|
||||
import kellinwood.logging.LoggerInterface;
|
||||
import kellinwood.logging.LoggerManager;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
|
||||
public class PasswordObfuscator {
|
||||
|
||||
private static PasswordObfuscator instance = null;
|
||||
|
||||
static final String x = "harold-and-maude";
|
||||
|
||||
LoggerInterface logger;
|
||||
SecretKeySpec skeySpec;
|
||||
|
||||
private PasswordObfuscator() {
|
||||
logger = LoggerManager.getLogger(PasswordObfuscator.class.getName());
|
||||
skeySpec = new SecretKeySpec(x.getBytes(), "AES");
|
||||
}
|
||||
|
||||
public static PasswordObfuscator getInstance() {
|
||||
if (instance == null) instance = new PasswordObfuscator();
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String encodeKeystorePassword(String keystorePath, String password) {
|
||||
return encode(keystorePath, password);
|
||||
}
|
||||
|
||||
public String encodeKeystorePassword(String keystorePath, char[] password) {
|
||||
return encode(keystorePath, password);
|
||||
}
|
||||
|
||||
public String encodeAliasPassword(String keystorePath, String aliasName, String password) {
|
||||
return encode(keystorePath + aliasName, password);
|
||||
}
|
||||
|
||||
public String encodeAliasPassword(String keystorePath, String aliasName, char[] password) {
|
||||
return encode(keystorePath + aliasName, password);
|
||||
}
|
||||
|
||||
public char[] decodeKeystorePassword(String keystorePath, String password) {
|
||||
return decode(keystorePath, password);
|
||||
}
|
||||
|
||||
public char[] decodeAliasPassword(String keystorePath, String aliasName, String password) {
|
||||
return decode(keystorePath + aliasName, password);
|
||||
}
|
||||
|
||||
public String encode(String junk, String password) {
|
||||
if (password == null) return null;
|
||||
char[] c = password.toCharArray();
|
||||
String result = encode(junk, c);
|
||||
flush(c);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>This uses the AES-ECB cipher which is known to be insecure</b>
|
||||
*
|
||||
* @see <a href="https://blog.filippo.io/the-ecb-penguin/">The ECB Penguin</a>
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("GetInstance")
|
||||
public String encode(String junk, char[] password) {
|
||||
if (password == null) return null;
|
||||
try {
|
||||
// Instantiate the cipher
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
Writer w = new OutputStreamWriter(baos);
|
||||
w.write(junk);
|
||||
w.write(password);
|
||||
w.flush();
|
||||
byte[] encoded = cipher.doFinal(baos.toByteArray());
|
||||
return Base64.encodeToString(encoded, Base64.NO_WRAP);
|
||||
} catch (Exception x) {
|
||||
logger.error("Failed to obfuscate password", x);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>This uses the AES-ECB cipher which is known to be insecure</b>
|
||||
*
|
||||
* @see <a href="https://blog.filippo.io/the-ecb-penguin/">The ECB Penguin</a>
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("GetInstance")
|
||||
public char[] decode(String junk, String password) {
|
||||
if (password == null) return null;
|
||||
try {
|
||||
// Instantiate the cipher
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
|
||||
SecretKeySpec skeySpec = new SecretKeySpec(x.getBytes(), "AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
|
||||
byte[] bytes = cipher.doFinal(Base64.decode(password.getBytes(), Base64.NO_WRAP));
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
|
||||
char[] cb = new char[128];
|
||||
int length = 0;
|
||||
int numRead;
|
||||
while ((numRead = r.read(cb, length, 128 - length)) != -1) {
|
||||
length += numRead;
|
||||
}
|
||||
|
||||
if (length <= junk.length()) return null;
|
||||
|
||||
char[] result = new char[length - junk.length()];
|
||||
int j = 0;
|
||||
for (int i = junk.length(); i < length; i++) {
|
||||
result[j] = cb[i];
|
||||
j += 1;
|
||||
}
|
||||
flush(cb);
|
||||
return result;
|
||||
|
||||
} catch (Exception x) {
|
||||
logger.error("Failed to decode password", x);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void flush(char[] charArray) {
|
||||
if (charArray == null) return;
|
||||
for (int i = 0; i < charArray.length; i++) {
|
||||
charArray[i] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
public static void flush(byte[] charArray) {
|
||||
if (charArray == null) return;
|
||||
for (int i = 0; i < charArray.length; i++) {
|
||||
charArray[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,5 @@
|
|||
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
|
||||
"http://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
|
||||
<suppressions>
|
||||
<suppress checks="." files="[\\/]kellinwood[\\/].*\.java$"/>
|
||||
<suppress checks="." files="[\\/]vendored[\\/].*\.java$"/>
|
||||
</suppressions>
|
||||
|
|
Loading…
Reference in New Issue