/* GnuKeyring.java -- minimal, read-only GNU Keyring implementation. Copyright (C) 2003 Free Software Foundation, Inc. This file is part of GNU Classpath. GNU Classpath 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, or (at your option) any later version. GNU Classpath 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 GNU Classpath; see the file COPYING. If not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ package gnu.java.security.provider; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.security.Key; import java.security.KeyStoreSpi; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Vector; import java.util.zip.InflaterInputStream; public class GnuKeyring extends KeyStoreSpi { // Constants and fields. // ------------------------------------------------------------------------- // The value "GKR" | 0x01 private static final int MAGIC = 0x474b5201; // Keyring usage type 2: trusted certificates only. private static final int USAGE = 04; // Packet types. Types not appearing in public keyrings are omitted. private static final int TYPE_PBMAC = 3; private static final int TYPE_COMPRESSED = 4; private static final int TYPE_CERT = 5; private final HashMap entries; // Constructor. // ------------------------------------------------------------------------- public GnuKeyring() { entries = new HashMap(); } // Instance methods. // ------------------------------------------------------------------------- public Key engineGetKey(String alias, char[] password) { throw new UnsupportedOperationException("this is a public keyring"); } public Certificate[] engineGetCertificateChain(String alias) { throw new UnsupportedOperationException("this is a public keyring"); } public Certificate engineGetCertificate(String alias) { Entry e = (Entry) entries.get(canonicalize(alias)); if (e == null) { return null; } return e.getCertificate(); } public Date engineGetCreationDate(String alias) { Entry e = (Entry) entries.get(canonicalize(alias)); if (e == null) { return null; } return e.getCreationDate(); } public void engineSetKeyEntry(String alias, Key key, char[] passwd, Certificate[] chain) { throw new UnsupportedOperationException("this is a read-only keyring"); } public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) { throw new UnsupportedOperationException("this is a read-only keyring"); } public void engineSetCertificateEntry(String alias, Certificate certificate) { throw new UnsupportedOperationException("this is a read-only keyring"); } public void engineDeleteEntry(String alias) { throw new UnsupportedOperationException("this is a read-only keyring"); } public Enumeration engineAliases() { return new Vector(entries.keySet()).elements(); } public boolean engineContainsAlias(String alias) { return entries.containsKey(canonicalize(alias)); } public int engineSize() { return entries.size(); } public boolean engineIsKeyEntry(String alias) { return false; } public boolean engineIsCertificateEntry(String alias) { return engineContainsAlias(alias); } public String engineGetCertificateAlias(Certificate certificate) { for (Iterator it = entries.entrySet().iterator(); it.hasNext(); ) { Map.Entry e = (Map.Entry) it.next(); if (certificate.equals(e.getValue())) { return (String) e.getKey(); } } return null; } public void engineStore(OutputStream out, char[] password) { throw new UnsupportedOperationException("this is a read-only keyring"); } public synchronized void engineLoad(InputStream _in, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException { entries.clear(); DataInputStream din = new DataInputStream(_in); if (din.readInt() != MAGIC) { throw new IOException("expecting magic bytes"); } if (din.read() != USAGE) { throw new IOException("not a public keyring"); } // Top-level is a password-based MAC entry. if (din.read() != TYPE_PBMAC) { throw new IOException("expecting password-based MAC"); } Map props = readProperties(din); int macLen = 0; try { macLen = Integer.parseInt((String) props.get("maclen")); if (macLen <= 0) { throw new NumberFormatException("must be positive"); } } catch (Exception x) { throw new IOException("malformed mac length: " + x.toString()); } int len = din.readInt(); byte[] payload = new byte[len - macLen]; byte[] mac = new byte[macLen]; din.readFully(payload); din.readFully(mac); if (!verifyHash(props, payload, mac, password)) { throw new IOException("MAC could not be verified"); } DataInputStream in2 = new DataInputStream(new ByteArrayInputStream(payload)); CertificateFactory factory = null; Certificate cert = null; Date date = null; while (in2.available() > 0) { int type = in2.read(); switch (type) { case TYPE_COMPRESSED: props = readProperties(in2); String comp = (String) props.get("algorithm"); if (comp == null || !comp.equalsIgnoreCase("DEFLATE")) { throw new NoSuchAlgorithmException(comp); } payload = new byte[in2.readInt()]; in2.readFully(payload); in2 = new DataInputStream(new InflaterInputStream( new ByteArrayInputStream(payload))); break; case TYPE_CERT: props = readProperties(in2); String alias = (String) props.get("alias"); if (alias == null || alias.length() == 0) { throw new IOException("no alias for certificate entry"); } date = null; try { String s = (String) props.get("creation-date"); if (s == null) { throw new IOException("missing creation date"); } date = new Date(Long.parseLong(s)); } catch (NumberFormatException x) { throw new IOException("malformed date: " + x.toString()); } factory = CertificateFactory.getInstance((String) props.get("type")); payload = new byte[in2.readInt()]; in2.readFully(payload); cert = factory.generateCertificate(new ByteArrayInputStream(payload)); entries.put(alias, new Entry(cert, date)); break; case -1: return; default: throw new IOException("unknown packet type: " + type); } } } // Own methods. // ------------------------------------------------------------------------- private String canonicalize(String key) { if (key == null) { return null; } return key.toLowerCase(); } private boolean verifyHash(Map props, byte[] payload, byte[] hash, char[] password) throws IOException, NoSuchAlgorithmException { // If no password is supplied, don't check the hash. if (password == null) { return true; } String macName = (String) props.get("mac"); if (macName == null || !macName.toLowerCase().startsWith("hmac-")) { throw new NoSuchAlgorithmException("invalid MAC"); } HMac mac = new HMac(MessageDigest.getInstance(macName.substring(5), new Gnu())); // Generate a key from the password. byte[] mackey = new byte[mac.getDigestLength()]; HMac kdfmac = new HMac(MessageDigest.getInstance("SHA-1", new Gnu())); kdfmac.setup(new String(password).getBytes("UTF-8")); byte[] salt = decodeSalt((String) props.get("salt")); int limit = salt.length; byte[] in = new byte[limit + 4]; System.arraycopy(salt, 0, in, 0, salt.length); // The count is appended to 'in', but in this case it is always 1. in[in.length - 1] = 1; for (int i = 0; i < 1000; i++) { kdfmac.reset(); kdfmac.update(in, 0, in.length); in = kdfmac.digest(); for (int j = 0; j < mackey.length; j++) { mackey[j] ^= in[j]; } } // Compute and check the MAC. mac.setup(mackey); mac.update(payload, 0, payload.length); byte[] d = mac.digest(); for (int i = 0; i < d.length && i < hash.length; i++) { if (d[i] != hash[i]) { return false; } } return true; } private Map readProperties(DataInputStream in) throws IOException { HashMap props = new HashMap(); int len = in.readInt(); byte[] buf = new byte[len]; in.readFully(buf); DataInputStream in2 = new DataInputStream(new ByteArrayInputStream(buf)); while (in2.available() > 0) { String key = in2.readUTF(); String value = in2.readUTF(); props.put(canonicalize(key), value); } return props; } private byte[] decodeSalt(String salt) { int limit = salt.length(); byte[] result = new byte[((limit + 1) / 2)]; int i = 0, j = 0; if ((limit & 1) == 1) { result[j++] = (byte) Character.digit(salt.charAt(i++), 16); } while (i < limit) { result[j ] = (byte) (Character.digit(salt.charAt(i++), 16) << 4); result[j++] |= (byte) Character.digit(salt.charAt(i++), 16); } return result; } // Inner classes. // ------------------------------------------------------------------------- /** * A single certificate entry. */ private static final class Entry { // Fields. // ----------------------------------------------------------------------- private final Certificate certificate; private final Date creationDate; // Constructor. // ----------------------------------------------------------------------- Entry(Certificate certificate, Date creationDate) { this.certificate = certificate; this.creationDate = creationDate; } // Instance methods. // ----------------------------------------------------------------------- Certificate getCertificate() { return certificate; } Date getCreationDate() { return (Date) creationDate.clone(); } } /** * Minimal HMac implemnentation, derived from GNU Crypto. */ private static final class HMac { // Constants and fields. // ----------------------------------------------------------------------- private MessageDigest md, ipadHash, opadHash; private byte[] ipad; // Constructor. // ----------------------------------------------------------------------- HMac(MessageDigest md) { this.md = md; } // Instance methods. // ----------------------------------------------------------------------- void setup(byte[] key) { if (key.length > 64) { key = md.digest(key); } if (key.length < 64) { byte[] b = new byte[64]; System.arraycopy(key, 0, b, 0, key.length); key = b; } ipad = new byte[64]; for (int i = 0; i < ipad.length; i++) { ipad[i] = (byte) (key[i] ^ 0x36); } md.reset(); try { opadHash = (MessageDigest) md.clone(); } catch (CloneNotSupportedException shouldNotHappen) { throw new Error(shouldNotHappen); } for (int i = 0; i < key.length; i++) { opadHash.update((byte) (key[i] ^ 0x5C)); } md.update(ipad); try { ipadHash = (MessageDigest) md.clone(); } catch (CloneNotSupportedException shouldNotHappen) { throw new Error(shouldNotHappen); } } void update(byte[] buf, int off, int len) { md.update(buf, off, len); } byte[] digest() { byte[] result = md.digest(); try { md = (MessageDigest) opadHash.clone(); } catch (CloneNotSupportedException shouldNotHappen) { throw new Error(shouldNotHappen); } result = md.digest(result); md.reset(); return result; } void reset() { md.reset(); if (ipad == null) { return; } md.update(ipad); try { ipadHash = (MessageDigest) md.clone(); } catch (CloneNotSupportedException shouldNotHappen) { throw new Error(shouldNotHappen); } } int getDigestLength() { return md.getDigestLength(); } } }