See Bugzilla #182.
authorwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Mon, 22 Nov 2004 21:50:45 +0000 (21:50 +0000)
committerwassa <wassa@ab3bd59b-922f-494d-bb5f-6f0a3c29deca>
Mon, 22 Nov 2004 21:50:45 +0000 (21:50 +0000)
Integrated changes from Derek Morr to:
1) create a more compact handle with the crypto handle generator
2) allow overriding of the crypto algorithms used to generate the handles.

Added unit tests for item #2.

Added unit tests to ensure that the NameIdentifierMapping implementations correctly verify SAML Name Qualifiers.

git-svn-id: https://subversion.switch.ch/svn/shibboleth/java-idp/trunk@1197 ab3bd59b-922f-494d-bb5f-6f0a3c29deca

src/edu/internet2/middleware/shibboleth/hs/provider/CryptoShibHandle.java
src/edu/internet2/middleware/shibboleth/utils/Base32.java [new file with mode: 0644]
tests/edu/internet2/middleware/shibboleth/hs/provider/NameMapperTests.java

index 701eaa8..b05c4a7 100644 (file)
@@ -2,37 +2,34 @@
  * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
  * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
  * provided that the following conditions are met: Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the
- * above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other
- * materials provided with the distribution, if any, must include the following acknowledgment: "This product includes
- * software developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2
- * Project. Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
- * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2,
- * nor the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
- * products derived from this software without specific prior written permission. For written permission, please
- * contact shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2,
- * UCAID, or the University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name,
- * without prior written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS
- * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
- * NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS
- * WITH LICENSEE. IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED
- * INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
- * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
- * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
+ * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
+ * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
+ * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
+ * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
+ * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
+ * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
+ * products derived from this software without specific prior written permission. For written permission, please contact
+ * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
+ * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
+ * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
+ * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
+ * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 package edu.internet2.middleware.shibboleth.hs.provider;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
 import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutput;
-import java.io.ObjectOutputStream;
-import java.io.Serializable;
 import java.io.StreamCorruptedException;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
@@ -60,8 +57,6 @@ import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 
-import sun.misc.BASE64Decoder;
-import sun.misc.BASE64Encoder;
 import edu.internet2.middleware.shibboleth.common.AuthNPrincipal;
 import edu.internet2.middleware.shibboleth.common.IdentityProvider;
 import edu.internet2.middleware.shibboleth.common.InvalidNameIdentifierException;
@@ -70,54 +65,74 @@ import edu.internet2.middleware.shibboleth.common.NameIdentifierMappingException
 import edu.internet2.middleware.shibboleth.common.ServiceProvider;
 import edu.internet2.middleware.shibboleth.common.ShibResource;
 import edu.internet2.middleware.shibboleth.hs.HSNameIdentifierMapping;
+import edu.internet2.middleware.shibboleth.utils.Base32;
 
 /**
  * {@link HSNameIdentifierMapping}implementation that uses symmetric encryption to store principal data inside
  * Shibboleth Attribute Query Handles.
  * 
  * @author Walter Hoehn
+ * @author Derek Morr
  */
 public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSNameIdentifierMapping {
 
-       private static Logger   log             = Logger.getLogger(CryptoShibHandle.class.getName());
-       protected SecretKey             secret;
-       private SecureRandom    random  = new SecureRandom();
+       private static Logger log = Logger.getLogger(CryptoShibHandle.class.getName());
+       protected SecretKey secret;
+       private SecureRandom random = new SecureRandom();
+       private String cipherAlgorithm = "DESede/CBC/PKCS5Padding";
+       private String macAlgorithm = "HmacSHA1";
+       private String storeType = "JCEKS";
 
        public CryptoShibHandle(Element config) throws NameIdentifierMappingException {
+
                super(config);
                try {
 
-                       String keyStorePath = getElementConfigData(config, "KeyStorePath");
-                       String keyStorePassword = getElementConfigData(config, "KeyStorePassword");
-                       String keyStoreKeyAlias = getElementConfigData(config, "KeyStoreKeyAlias");
-                       String keyStoreKeyPassword = getElementConfigData(config, "KeyStoreKeyPassword");
+                       String keyStorePath = getElementConfigData(config, "KeyStorePath", true);
+                       String keyStorePassword = getElementConfigData(config, "KeyStorePassword", true);
+                       String keyStoreKeyAlias = getElementConfigData(config, "KeyStoreKeyAlias", true);
+                       String keyStoreKeyPassword = getElementConfigData(config, "KeyStoreKeyPassword", true);
 
-                       KeyStore keyStore = KeyStore.getInstance("JCEKS");
+                       String rawStoreType = getElementConfigData(config, "KeyStoreType", false);
+                       if (rawStoreType != null && !rawStoreType.equals("")) {
+                               storeType = rawStoreType;
+                       }
+                       String rawCipherAlgorithm = getElementConfigData(config, "Cipher", false);
+                       if (rawCipherAlgorithm != null && !rawCipherAlgorithm.equals("")) {
+                               cipherAlgorithm = rawCipherAlgorithm;
+                       }
+                       String rawMacAlgorithm = getElementConfigData(config, "MAC", false);
+                       if (rawMacAlgorithm != null && !rawMacAlgorithm.equals("")) {
+                               macAlgorithm = rawMacAlgorithm;
+                       }
+
+                       KeyStore keyStore = KeyStore.getInstance(storeType);
                        keyStore.load(new ShibResource(keyStorePath, this.getClass()).getInputStream(), keyStorePassword
                                        .toCharArray());
                        secret = (SecretKey) keyStore.getKey(keyStoreKeyAlias, keyStoreKeyPassword.toCharArray());
 
-                       //Before we finish initilization, make sure that things are
-                       // working
+                       // Before we finish initilization, make sure that things are working
                        testEncryption();
 
                        if (usingDefaultSecret()) {
-                               log
-                                               .warn("You are running Crypto AQH Name Mapping with the default secret key.  This is UNSAFE!  Please change "
-                                                               + "this configuration and restart the origin.");
+                               log.warn("You are running Crypto AQH Name Mapping with the "
+                                               + "default secret key.  This is UNSAFE!  Please change "
+                                               + "this configuration and restart the origin.");
                        }
                } catch (StreamCorruptedException e) {
                        if (System.getProperty("java.version").startsWith("1.4.2")) {
-                               log.error("There is a bug in Java 1.4.2 that prevents JCEKS keystores from being loaded properly.  "
+                               log.error("There is a bug in some versions of Java 1.4.2.x that "
+                                               + "prevent JCEKS keystores from being loaded properly.  "
                                                + "You probably need to upgrade or downgrade your JVM in order to make this work.");
                        }
-                       log.error("An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping: "
-                                       + e);
+                       log.error("An error occurred while loading the java keystore.  Unable to initialize "
+                                       + "Crypto Name Mapping: " + e);
                        throw new NameIdentifierMappingException(
-                                       "An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping.");
+                                       "An error occurred while loading the java keystore.  Unable to initialize Crypto "
+                                                       + "Name Mapping.");
                } catch (KeyStoreException e) {
-                       log.error("An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping: "
-                                       + e);
+                       log.error("An error occurred while loading the java keystore.  Unable to initialize Crypto "
+                                       + "Name Mapping: " + e);
                        throw new NameIdentifierMappingException(
                                        "An error occurred while loading the java keystore.  Unable to initialize Crypto Name Mapping.");
                } catch (CertificateException e) {
@@ -125,76 +140,96 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
                        throw new NameIdentifierMappingException(
                                        "The java keystore contained corrupted data.  Unable to initialize Crypto Name Mapping.");
                } catch (NoSuchAlgorithmException e) {
-                       log
-                                       .error("Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Name Mapping: "
-                                                       + e);
+                       log.error("Appropriate JCE provider not found in the java environment. Unable "
+                                       + "to initialize Crypto Name Mapping: " + e);
                        throw new NameIdentifierMappingException(
                                        "Appropriate JCE provider not found in the java environment. Unable to initialize Crypto Name Mapping.");
                } catch (IOException e) {
-                       log.error("An error accessing while loading the java keystore.  Unable to initialize Crypto Name Mapping: "
-                                       + e);
+                       log.error("An error accessing while loading the java keystore.  Unable to initialize Crypto Name "
+                                       + "Mapping: " + e);
                        throw new NameIdentifierMappingException(
                                        "An error occurred while accessing the java keystore.  Unable to initialize Crypto Name Mapping.");
                } catch (UnrecoverableKeyException e) {
-                       log
-                                       .error("Secret could not be loaded from the java keystore.  Verify that the alias and password are correct: "
-                                                       + e);
+                       log.error("Secret could not be loaded from the java keystore.  Verify that the alias and "
+                                       + "password are correct: " + e);
                        throw new NameIdentifierMappingException(
                                        "Secret could not be loaded from the java keystore.  Verify that the alias and password are correct. ");
                }
        }
 
+       /**
+        * Decode an encrypted handle back into a principal
+        */
        public AuthNPrincipal getPrincipal(SAMLNameIdentifier nameId, ServiceProvider sProv, IdentityProvider idProv)
                        throws NameIdentifierMappingException, InvalidNameIdentifierException {
 
                verifyQualifier(nameId, idProv);
-               
+
                try {
-                       //Separate the IV and handle
-                       byte[] in = new BASE64Decoder().decodeBuffer(nameId.getName());
-                       if (in.length < 9) {
+                       byte[] in = Base32.decode(nameId.getName());
+
+                       Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+                       int ivSize = cipher.getBlockSize();
+                       byte[] iv = new byte[ivSize];
+
+                       Mac mac = Mac.getInstance(macAlgorithm);
+                       mac.init(secret);
+                       int macSize = mac.getMacLength();
+
+                       if (in.length < ivSize) {
                                log.debug("Attribute Query Handle is malformed (not enough bytes).");
                                throw new NameIdentifierMappingException("Attribute Query Handle is malformed (not enough bytes).");
                        }
-                       byte[] iv = new byte[8];
-                       System.arraycopy(in, 0, iv, 0, 8);
-                       byte[] encryptedHandle = new byte[in.length - iv.length];
-                       System.arraycopy(in, 8, encryptedHandle, 0, in.length - iv.length);
 
-                       Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
+                       // extract the IV, setup the cipher and extract the encrypted handle
+                       System.arraycopy(in, 0, iv, 0, ivSize);
                        IvParameterSpec ivSpec = new IvParameterSpec(iv);
                        cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
 
-                       byte[] objectArray = cipher.doFinal(encryptedHandle);
-                       GZIPInputStream zipBytesIn = new GZIPInputStream(new ByteArrayInputStream(objectArray));
-
-                       ObjectInputStream objectStream = new ObjectInputStream(zipBytesIn);
+                       byte[] encryptedHandle = new byte[in.length - iv.length];
+                       System.arraycopy(in, ivSize, encryptedHandle, 0, in.length - iv.length);
+
+                       // decrypt the rest of the data and setup the streams
+                       byte[] decryptedBytes = cipher.doFinal(encryptedHandle);
+                       ByteArrayInputStream byteStream = new ByteArrayInputStream(decryptedBytes);
+                       GZIPInputStream compressedData = new GZIPInputStream(byteStream);
+                       DataInputStream dataStream = new DataInputStream(compressedData);
+
+                       // extract the components
+                       byte[] decodedMac = new byte[macSize];
+                       int bytesRead = dataStream.read(decodedMac);
+                       if (bytesRead != macSize) {
+                               log.error("Error parsing handle: Unable to extract HMAC.");
+                               throw new NameIdentifierMappingException("Error parsing handle: Unable to extract HMAC.");
+                       }
+                       long decodedExpirationTime = dataStream.readLong();
+                       String decodedPrincipal = dataStream.readUTF();
 
-                       HMACHandleEntry handleEntry = (HMACHandleEntry) objectStream.readObject();
-                       objectStream.close();
+                       HMACHandleEntry macHandleEntry = new HMACHandleEntry(
+                                       createHandleEntry(new AuthNPrincipal(decodedPrincipal)));
+                       macHandleEntry.setExpirationTime(decodedExpirationTime);
+                       byte[] generatedMac = macHandleEntry.getMAC(mac);
 
-                       if (handleEntry.isExpired()) {
+                       if (macHandleEntry.isExpired()) {
                                log.debug("Attribute Query Handle is expired.");
                                throw new InvalidNameIdentifierException("Attribute Query Handle is expired.", errorCodes);
                        }
 
-                       Mac mac = Mac.getInstance("HmacSHA1");
-                       mac.init(secret);
-                       if (!handleEntry.isValid(mac)) {
+                       if (!Arrays.equals(decodedMac, generatedMac)) {
                                log.warn("Attribute Query Handle failed integrity check.");
                                throw new NameIdentifierMappingException("Attribute Query Handle failed integrity check.");
                        }
 
                        log.debug("Attribute Query Handle recognized.");
-                       return handleEntry.principal;
+                       return macHandleEntry.principal;
 
                } catch (NoSuchAlgorithmException e) {
                        log.error("Appropriate JCE provider not found in the java environment.  Could not load Algorithm: " + e);
                        throw new NameIdentifierMappingException(
                                        "Appropriate JCE provider not found in the java environment.  Could not load Algorithm.");
                } catch (NoSuchPaddingException e) {
-                       log.error("Appropriate JCE provider not found in the java environment.  Could not load Padding method: "
-                                       + e);
+                       log.error("Appropriate JCE provider not found in the java environment.  Could not load Padding "
+                                       + "method: " + e);
                        throw new NameIdentifierMappingException(
                                        "Appropriate JCE provider not found in the java environment.  Could not load Padding method.");
                } catch (InvalidKeyException e) {
@@ -203,17 +238,20 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
                } catch (GeneralSecurityException e) {
                        log.warn("Unable to decrypt the supplied Attribute Query Handle: " + e);
                        throw new NameIdentifierMappingException("Unable to decrypt the supplied Attribute Query Handle.");
-               } catch (ClassNotFoundException e) {
-                       log.warn("The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal: " + e);
-                       throw new NameIdentifierMappingException(
-                                       "The supplied Attribute Query Handle does not represent a serialized AuthNPrincipal.");
                } catch (IOException e) {
-                       log.warn("The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle: " + e);
-                       throw new NameIdentifierMappingException(
-                                       "The AuthNPrincipal could not be de-serialized from the supplied Attribute Query Handle.");
+                       log.warn("IO error while decoding handle.");
+                       throw new NameIdentifierMappingException("IO error while decoding handle.");
                }
        }
 
+       /**
+        * Encodes a principal into a cryptographic handle Format of encoded handle: [IV][HMAC][TTL][principal] where: [IV] =
+        * the Initialization Vector; byte-array [HMAC] = the HMAC; byte array [exprTime] = expiration time of the handle; 8
+        * bytes; Big-endian [principal] = the principal; a UTF-8-encoded string The [HMAC][exprTime][princLen][principal]
+        * byte stream is GZIPped. The IV is pre-pended to this byte stream, and the result is Base32-encoded. We don't need
+        * to encode the IV or MAC's lengths. They can be obtained from Cipher.getBlockSize() and Mac.getMacLength(),
+        * respectively.
+        */
        public SAMLNameIdentifier getNameIdentifierName(AuthNPrincipal principal, ServiceProvider sProv,
                        IdentityProvider idProv) throws NameIdentifierMappingException {
 
@@ -223,33 +261,37 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
                                throw new IllegalArgumentException("A principal must be supplied for Attribute Query Handle creation.");
                        }
 
-                       HandleEntry handleEntry = createHandleEntry(principal);
-
-                       Mac mac = Mac.getInstance("HmacSHA1");
+                       Mac mac = Mac.getInstance(macAlgorithm);
                        mac.init(secret);
-                       HMACHandleEntry macHandleEntry = new HMACHandleEntry(handleEntry, mac);
-
-                       ByteArrayOutputStream outStream = new ByteArrayOutputStream();
-                       ByteArrayOutputStream encStream = new ByteArrayOutputStream();
+                       HandleEntry handleEntry = createHandleEntry(principal);
+                       HMACHandleEntry macHandleEntry = new HMACHandleEntry(handleEntry);
 
-                       Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");
-                       byte[] iv = new byte[8];
+                       Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+                       byte[] iv = new byte[cipher.getBlockSize()];
                        random.nextBytes(iv);
                        IvParameterSpec ivSpec = new IvParameterSpec(iv);
                        cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
 
-                       //Handle contains 8 byte IV, followed by cipher text
-                       outStream.write(cipher.getIV());
+                       ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+                       GZIPOutputStream compressedStream = new GZIPOutputStream(byteStream);
+                       DataOutputStream dataStream = new DataOutputStream(compressedStream);
 
-                       ObjectOutput objectStream = new ObjectOutputStream(new GZIPOutputStream(encStream));
-                       objectStream.writeObject(macHandleEntry);
-                       objectStream.close();
+                       dataStream.write(macHandleEntry.getMAC(mac));
+                       dataStream.writeLong(macHandleEntry.getExpirationTime());
+                       dataStream.writeUTF(principal.getName());
 
-                       outStream.write(cipher.doFinal(encStream.toByteArray()));
-                       encStream.close();
+                       dataStream.flush();
+                       compressedStream.flush();
+                       compressedStream.finish();
+                       byteStream.flush();
 
-                       String handle = new BASE64Encoder().encode(outStream.toByteArray());
-                       outStream.close();
+                       byte[] encryptedData = cipher.doFinal(byteStream.toByteArray());
+
+                       byte[] handleBytes = new byte[iv.length + encryptedData.length];
+                       System.arraycopy(iv, 0, handleBytes, 0, iv.length);
+                       System.arraycopy(encryptedData, 0, handleBytes, iv.length, encryptedData.length);
+
+                       String handle = Base32.encode(handleBytes);
 
                        try {
                                return new SAMLNameIdentifier(handle.replaceAll(System.getProperty("line.separator"), ""), idProv
@@ -266,19 +308,25 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
                        throw new NameIdentifierMappingException(
                                        "Appropriate JCE provider not found in the java environment.  Could not load Cipher.");
                } catch (IOException e) {
-                       log.error("Could not serialize Principal for handle creation: " + e);
-                       throw new NameIdentifierMappingException(
-                                       "Could not serialize Principal for Attribute Query Handle creation.");
+                       log.warn("IO error while decoding handle.");
+                       throw new NameIdentifierMappingException("IO error while decoding handle.");
                }
+
        }
 
-       private String getElementConfigData(Element e, String itemName) throws NameIdentifierMappingException {
+       private String getElementConfigData(Element e, String itemName, boolean required)
+                       throws NameIdentifierMappingException {
 
                NodeList itemElements = e.getElementsByTagNameNS(NameIdentifierMapping.mappingNamespace, itemName);
 
                if (itemElements.getLength() < 1) {
-                       log.error(itemName + " not specified.");
-                       throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName + "> specification.");
+                       if (required) {
+                               log.error(itemName + " not specified.");
+                               throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName
+                                               + "> specification.");
+                       } else {
+                               return null;
+                       }
                }
 
                if (itemElements.getLength() > 1) {
@@ -292,7 +340,8 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
                }
                if (item == null || item.equals("")) {
                        log.error(itemName + " not specified.");
-                       throw new NameIdentifierMappingException("Crypto Name Mapping requires a <" + itemName + "> specification.");
+                       throw new NameIdentifierMappingException("Crypto Name Mapping requires a valid <" + itemName
+                                       + "> specification.");
                }
                return item;
        }
@@ -301,11 +350,14 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
 
                String decrypted;
                try {
-                       Cipher cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
-                       cipher.init(Cipher.ENCRYPT_MODE, secret);
+                       Cipher cipher = Cipher.getInstance(cipherAlgorithm);
+                       byte[] iv = new byte[cipher.getBlockSize()];
+                       random.nextBytes(iv);
+                       IvParameterSpec ivSpec = new IvParameterSpec(iv);
+                       cipher.init(Cipher.ENCRYPT_MODE, secret, ivSpec);
                        byte[] cipherText = cipher.doFinal("test".getBytes());
-                       cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
-                       cipher.init(Cipher.DECRYPT_MODE, secret);
+                       cipher = Cipher.getInstance(cipherAlgorithm);
+                       cipher.init(Cipher.DECRYPT_MODE, secret, ivSpec);
                        decrypted = new String(cipher.doFinal(cipherText));
                } catch (Exception e) {
                        log.error("Round trip encryption/decryption test unsuccessful: " + e);
@@ -319,7 +371,7 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
 
                byte[] code;
                try {
-                       Mac mac = Mac.getInstance("HmacSHA1");
+                       Mac mac = Mac.getInstance(macAlgorithm);
                        mac.init(secret);
                        mac.update("foo".getBytes());
                        code = mac.doFinal();
@@ -336,6 +388,7 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
        }
 
        private boolean usingDefaultSecret() {
+
                byte[] defaultKey = new byte[]{(byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
                                (byte) 0x61, (byte) 0xEF, (byte) 0x25, (byte) 0x5D, (byte) 0xE3, (byte) 0x2F, (byte) 0x57, (byte) 0x51,
                                (byte) 0x20, (byte) 0x15, (byte) 0xC7, (byte) 0x49, (byte) 0x80, (byte) 0xD3, (byte) 0x02, (byte) 0x4A,
@@ -350,29 +403,49 @@ public class CryptoShibHandle extends AQHNameIdentifierMapping implements HSName
  * <code>HandleEntry</code> extension class that performs message authentication.
  */
 
-class HMACHandleEntry extends HandleEntry implements Serializable {
+class HMACHandleEntry extends HandleEntry {
 
-       static final long       serialVersionUID        = 1L;
-       protected byte[]        code;
+       protected HMACHandleEntry(AuthNPrincipal principal, long TTL) {
 
-       protected HMACHandleEntry(AuthNPrincipal principal, long TTL, Mac mac) {
                super(principal, TTL);
-               mac.update(this.principal.getName().getBytes());
-               mac.update(new Long(this.expirationTime).byteValue());
-               code = mac.doFinal();
        }
 
-       protected HMACHandleEntry(HandleEntry handleEntry, Mac mac) {
+       protected HMACHandleEntry(HandleEntry handleEntry) {
+
                super(handleEntry.principal, handleEntry.expirationTime);
-               mac.update(this.principal.getName().getBytes());
-               mac.update(new Long(this.expirationTime).byteValue());
-               code = mac.doFinal();
        }
 
-       boolean isValid(Mac mac) {
-               mac.update(this.principal.getName().getBytes());
-               mac.update(new Long(this.expirationTime).byteValue());
-               byte[] validationCode = mac.doFinal();
-               return Arrays.equals(code, validationCode);
+       private static byte[] getLongBytes(long longValue) {
+
+               try {
+                       ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+                       DataOutputStream dataStream = new DataOutputStream(byteStream);
+
+                       dataStream.writeLong(longValue);
+                       dataStream.flush();
+                       byteStream.flush();
+
+                       return byteStream.toByteArray();
+               } catch (IOException ex) {
+                       return null;
+               }
        }
-}
+
+       public byte[] getMAC(Mac mac) {
+
+               mac.update(principal.getName().getBytes());
+               mac.update(getLongBytes(expirationTime));
+
+               return mac.doFinal();
+       }
+
+       public long getExpirationTime() {
+
+               return expirationTime;
+       }
+
+       public void setExpirationTime(long expr) {
+
+               expirationTime = expr;
+       }
+}
\ No newline at end of file
diff --git a/src/edu/internet2/middleware/shibboleth/utils/Base32.java b/src/edu/internet2/middleware/shibboleth/utils/Base32.java
new file mode 100644 (file)
index 0000000..9abcae7
--- /dev/null
@@ -0,0 +1,278 @@
+/* (PD) 2001 The Bitzi Corporation
+ * Please see http://bitzi.com/publicdomain for more info.
+ *
+ * Base32.java
+ *
+ */
+
+package edu.internet2.middleware.shibboleth.utils;
+
+/**
+ * Base32 - encodes and decodes 'Canonical' Base32
+ *
+ * @author  Robert Kaye & Gordon Mohr
+ */
+public class Base32 {
+
+       /* lookup table used to encode() groups of 5 bits of data */
+       private static final String base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+       /* lookup table used to decode() characters in Base32 strings */
+       private static final byte[] base32Lookup =
+               { 26,27,28,29,30,31,-1,-1,-1,-1,-1,-1,-1,-1,       //   23456789:;<=>?
+                 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, // @ABCDEFGHIJKLMNO
+                 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, // PQRSTUVWXYZ[\]^_
+                 -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, // `abcdefghijklmno
+                 15,16,17,18,19,20,21,22,23,24,25                 // pqrstuvwxyz
+               };
+
+       /* Messsages for Illegal Parameter Exceptions in decode() */
+       private static final String errorCanonicalLength = "non canonical Base32 string length";
+       private static final String errorCanonicalEnd    = "non canonical bits at end of Base32 string";
+       private static final String errorInvalidChar     = "invalid character in Base32 string";
+
+       /**
+        * Encode an array of binary bytes into a Base32 string.
+        * Should not fail (the only possible exception is that the
+        * returned string cannot be allocated in memory)
+        */    
+       static public String encode(final byte[] bytes) {
+    
+               StringBuffer base32 = new StringBuffer((bytes.length * 8 + 4) / 5);
+               int currByte, digit, i = 0;
+
+               while (i < bytes.length) {
+               
+                       // INVARIANTS FOR EACH STEP n in [0..5[; digit in [0..31[; 
+                       // The remaining n bits are already aligned on top positions
+                       // of the 5 least bits of digit, the other bits are 0.
+                       
+                       // STEP n = 0; insert new 5 bits, leave 3 bits
+                       currByte = bytes[i++] & 255;
+                       base32.append(base32Chars.charAt(currByte >> 3));
+                       digit = (currByte & 7) << 2;
+                       if (i >= bytes.length) { // put the last 3 bits
+                               base32.append(base32Chars.charAt(digit));
+                               break;
+                       }
+            
+
+                       // STEP n = 3: insert 2 new bits, then 5 bits, leave 1 bit
+                       currByte = bytes[i++] & 255;
+                       base32.append(base32Chars.charAt(digit | (currByte >> 6)));
+                       base32.append(base32Chars.charAt((currByte >> 1) & 31));
+                       digit = (currByte & 1) << 4;
+                       if (i >= bytes.length) { // put the last 1 bit
+                               base32.append(base32Chars.charAt(digit));
+                               break;
+                       }
+
+                       // STEP n = 1: insert 4 new bits, leave 4 bit
+                       currByte = bytes[i++] & 255;
+                       base32.append(base32Chars.charAt(digit | (currByte >> 4)));
+                       digit = (currByte & 15) << 1;
+                       if (i >= bytes.length) { // put the last 4 bits
+                               base32.append(base32Chars.charAt(digit));
+                               break;
+                       }
+
+                       // STEP n = 4: insert 1 new bit, then 5 bits, leave 2 bits
+                       currByte = bytes[i++] & 255;
+                       base32.append(base32Chars.charAt(digit | (currByte >> 7)));
+                       base32.append(base32Chars.charAt((currByte >> 2) & 31));
+                       digit = (currByte & 3) << 3;
+                       if (i >= bytes.length) { // put the last 2 bits
+                               base32.append(base32Chars.charAt(digit));
+                               break;
+                       }
+
+                       // STEP n = 2: insert 3 new bits, then 5 bits, leave 0 bit
+                       currByte = bytes[i++] & 255;
+                       base32.append(base32Chars.charAt(digit | (currByte >> 5)));
+                       base32.append(base32Chars.charAt(currByte & 31));
+                       //// This point is reached for bytes.length multiple of 5
+               }
+       
+               return base32.toString();
+       }
+
+   
+       /**
+        * Decode a Base32 string into an array of binary bytes.
+        * May fail if the parameter is a non canonical Base32 string   
+        * (the only other possible exception is that the
+        * returned array cannot be allocated in memory)
+        */    
+       static public byte[] decode(final String base32) throws IllegalArgumentException {
+
+       // Note that the code below detects could detect non canonical
+       // Base32 length within the loop. However canonical Base32 length
+       // can be tested before entering the loop.
+       // A canonical Base32 length modulo 8 cannot be:
+       // 1 (aborts discarding 5 bits at STEP n=0 which produces no byte),
+       // 3 (aborts discarding 7 bits at STEP n=2 which produces no byte),
+       // 6 (aborts discarding 6 bits at STEP n=1 which produces no byte)
+       // So these tests could be avoided within the loop.
+       switch (base32.length() % 8) { // test the length of last subblock
+
+               case 1: //  5 bits in subblock:  0 useful bits but 5 discarded
+               case 3: // 15 bits in subblock:  8 useful bits but 7 discarded
+               case 6: // 30 bits in subblock: 24 useful bits but 6 discarded
+          
+               throw new IllegalArgumentException(errorCanonicalLength);
+       }
+
+       byte[] bytes = new byte[base32.length() * 5 / 8];
+       int offset = 0, i = 0, lookup;
+       byte nextByte, digit;
+
+       // Also the code below does test that other discarded bits
+       // (1 to 4 bits at end) are effectively 0.
+       while (i < base32.length()) {
+               // Read the 1st char in a 8-chars subblock
+               // check that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 0: leave 5 bits
+               nextByte = (byte)(digit << 3);
+               // Assert(i < base32.length) // tested before loop
+               // Read the 2nd char in a 8-chars subblock
+               // Check that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 5: insert 3 bits, leave 2 bits
+               bytes[offset++] = (byte)(nextByte | (digit >> 2));
+               nextByte = (byte)((digit & 3) << 6);
+               if (i >= base32.length()) {
+                       if (nextByte != (byte)0) {
+                               throw new IllegalArgumentException(errorCanonicalEnd);
+                       }
+                       break; // discard the remaining 2 bits
+               }
+
+               // Read the 3rd char in a 8-chars subblock
+               // Check that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 2: leave 7 bits
+               nextByte |= (byte)(digit << 1);
+               // Assert(i < base32.length) // tested before loop
+               // Read the 4th char in a 8-chars subblock
+               // Check that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 7: insert 1 bit, leave 4 bits
+               bytes[offset++] = (byte)(nextByte | (digit >> 4));
+               nextByte = (byte)((digit & 15) << 4);
+               if (i >= base32.length()) {
+                       if (nextByte != (byte)0) {
+                               throw new IllegalArgumentException(errorCanonicalEnd);
+                       }
+                       break; // discard the remaining 4 bits
+               }
+
+               // Read the 5th char in a 8-chars subblock
+               // Assert that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 4: insert 4 bits, leave 1 bit
+               bytes[offset++] = (byte)(nextByte | (digit >> 1));
+               nextByte = (byte)((digit & 1) << 7);
+               if (i >= base32.length()) {
+                       if (nextByte != (byte)0) {
+                               throw new IllegalArgumentException(errorCanonicalEnd);
+                       }
+                       break; // discard the remaining 1 bit
+               }
+
+               // Read the 6th char in a 8-chars subblock
+               // Check that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 1: leave 6 bits
+               nextByte |= (byte)(digit << 2);
+               // Assert(i < base32.length) // tested before loop
+               // Read the 7th char in a 8-chars subblock
+               // Check that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 6: insert 2 bits, leave 3 bits
+               bytes[offset++] = (byte)(nextByte | (digit >> 3));
+               nextByte = (byte)((digit & 7) << 5);
+               if (i >= base32.length()) {
+                       if (nextByte != (byte)0) {
+                               throw new IllegalArgumentException(errorCanonicalEnd);
+                       }
+                       break; // discard the remaining 3 bits
+               }
+        
+               // Read the 8th char in a 8-chars subblock
+               // Check that chars are not outside the lookup table and valid
+               lookup = base32.charAt(i++) - '2';
+               if (lookup < 0 || lookup >= base32Lookup.length) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+               digit = base32Lookup[lookup];
+               if (digit == -1) {
+                       throw new IllegalArgumentException(errorInvalidChar);
+               }
+
+               // STEP n = 3: insert 5 bits, leave 0 bit
+               bytes[offset++] = (byte)(nextByte | digit);
+               // possible end of string here with no trailing bits
+       }
+
+               // On loop exit, discard trialing n bits.
+               return bytes;
+       }
+}
+
index 73dc1b6..b9e709c 100644 (file)
@@ -1,48 +1,26 @@
 /*
- * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation
- * for Advanced Internet Development, Inc. All rights reserved
- * 
- * 
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * 
- * Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- * 
- * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution, if any, must include
- * the following acknowledgment: "This product includes software developed by
- * the University Corporation for Advanced Internet Development
- * <http://www.ucaid.edu> Internet2 Project. Alternately, this acknowledegement
- * may appear in the software itself, if and wherever such third-party
- * acknowledgments normally appear.
- * 
- * Neither the name of Shibboleth nor the names of its contributors, nor
- * Internet2, nor the University Corporation for Advanced Internet Development,
- * Inc., nor UCAID may be used to endorse or promote products derived from this
- * software without specific prior written permission. For written permission,
- * please contact shibboleth@shibboleth.org
- * 
- * Products derived from this software may not be called Shibboleth, Internet2,
- * UCAID, or the University Corporation for Advanced Internet Development, nor
- * may Shibboleth appear in their name, without prior written permission of the
- * University Corporation for Advanced Internet Development.
- * 
- * 
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
- * PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK
- * OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE.
- * IN NO EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY
- * CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC. BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * The Shibboleth License, Version 1. Copyright (c) 2002 University Corporation for Advanced Internet Development, Inc.
+ * All rights reserved Redistribution and use in source and binary forms, with or without modification, are permitted
+ * provided that the following conditions are met: Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials
+ * provided with the distribution, if any, must include the following acknowledgment: "This product includes software
+ * developed by the University Corporation for Advanced Internet Development <http://www.ucaid.edu> Internet2 Project.
+ * Alternately, this acknowledegement may appear in the software itself, if and wherever such third-party
+ * acknowledgments normally appear. Neither the name of Shibboleth nor the names of its contributors, nor Internet2, nor
+ * the University Corporation for Advanced Internet Development, Inc., nor UCAID may be used to endorse or promote
+ * products derived from this software without specific prior written permission. For written permission, please contact
+ * shibboleth@shibboleth.org Products derived from this software may not be called Shibboleth, Internet2, UCAID, or the
+ * University Corporation for Advanced Internet Development, nor may Shibboleth appear in their name, without prior
+ * written permission of the University Corporation for Advanced Internet Development. THIS SOFTWARE IS PROVIDED BY THE
+ * COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT ARE
+ * DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO
+ * EVENT SHALL THE COPYRIGHT OWNER, CONTRIBUTORS OR THE UNIVERSITY CORPORATION FOR ADVANCED INTERNET DEVELOPMENT, INC.
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
 package edu.internet2.middleware.shibboleth.hs.provider;
@@ -87,34 +65,36 @@ public class NameMapperTests extends TestCase {
        private DOMParser parser = new DOMParser();
 
        public NameMapperTests(String name) {
+
                super(name);
                BasicConfigurator.resetConfiguration();
                BasicConfigurator.configure();
-               Logger.getRootLogger().setLevel(Level.OFF);
+               Logger.getRootLogger().setLevel(Level.DEBUG);
        }
 
        public static void main(String[] args) {
+
                junit.textui.TestRunner.run(NameMapperTests.class);
                BasicConfigurator.configure();
-               Logger.getRootLogger().setLevel(Level.OFF);
+               Logger.getRootLogger().setLevel(Level.DEBUG);
        }
 
        protected void setUp() throws Exception {
+
                super.setUp();
                try {
 
                        parser.setFeature("http://xml.org/sax/features/validation", true);
                        parser.setFeature("http://apache.org/xml/features/validation/schema", true);
                        parser.setEntityResolver(new EntityResolver() {
+
                                public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
 
                                        if (systemId.endsWith("namemapper.xsd")) {
                                                InputStream stream;
                                                try {
                                                        stream = new FileInputStream("src/schemas/namemapper.xsd");
-                                                       if (stream != null) {
-                                                               return new InputSource(stream);
-                                                       }
+                                                       if (stream != null) { return new InputSource(stream); }
                                                        throw new SAXException("Could not load entity: Null input stream");
                                                } catch (FileNotFoundException e) {
                                                        throw new SAXException("Could not load entity: " + e);
@@ -126,13 +106,19 @@ public class NameMapperTests extends TestCase {
                        });
 
                        parser.setErrorHandler(new ErrorHandler() {
+
                                public void error(SAXParseException arg0) throws SAXException {
+
                                        throw new SAXException("Error parsing xml file: " + arg0);
                                }
+
                                public void fatalError(SAXParseException arg0) throws SAXException {
+
                                        throw new SAXException("Error parsing xml file: " + arg0);
                                }
+
                                public void warning(SAXParseException arg0) throws SAXException {
+
                                        throw new SAXException("Error parsing xml file: " + arg0);
                                }
                        });
@@ -140,6 +126,7 @@ public class NameMapperTests extends TestCase {
                        fail("Failed to setup xml parser: " + e);
                }
        }
+
        public void testCryptoMapping() {
 
                try {
@@ -148,33 +135,69 @@ public class NameMapperTests extends TestCase {
 
                        File file = new File("data/handle.jks");
 
-                       String rawConfig =
-                               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                                        + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
                                        + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                                        + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
                                        + "                     id=\"cryptotest\" format=\"urn:mace:shibboleth:1.0:nameIdentifier\" "
-                                       + "                     type=\"CryptoHandleGenerator\" handleTTL=\"1800\">"
-                                       + "             <KeyStorePath>"
-                                       + file.toURL().toString()
-                                       + "</KeyStorePath>"
+                                       + "                     type=\"CryptoHandleGenerator\" handleTTL=\"1800\">" 
+                                       + "             <KeyStorePath>" + file.toURL().toString() + "</KeyStorePath>" 
                                        + "             <KeyStorePassword>shibhs</KeyStorePassword>"
                                        + "             <KeyStoreKeyAlias>handlekey</KeyStoreKeyAlias>"
-                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>"
+                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>" 
                                        + "     </NameMapping>";
 
                        parser.parse(new InputSource(new StringReader(rawConfig)));
                        nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
 
-                       SAMLNameIdentifier nameId =
-                               nameMapper.getNameIdentifierName(
-                                       "cryptotest",
-                                       new AuthNPrincipal("testprincipal"),
-                                       new BasicServiceProvider(),
+                       SAMLNameIdentifier nameId = nameMapper.getNameIdentifierName("cryptotest", new AuthNPrincipal(
+                                       "testprincipal"), new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
+
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
                                        new BasicIdentityProvider("urn-x:testid"));
+                       assertEquals("Round-trip handle validation failed.", principal.getName(), "testprincipal");
+
+               } catch (MalformedURLException e) {
+                       fail("Error in test specification: " + e.getMessage());
+               } catch (NameIdentifierMappingException e) {
+                       fail("Error exercising NameMaper: " + e.getMessage());
+               } catch (Exception e) {
+                       fail("Error exercising NameMaper: " + e.getMessage());
+               }
+
+       }
+
+       public void testCryptoMappingWithOverriddenAlgorithms() {
+
+               try {
+
+                       HSNameMapper nameMapper = new HSNameMapper();
 
-                       AuthNPrincipal principal =
-                               nameMapper.getPrincipal(nameId, new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
+                       File file = new File("data/handle.jks");
+
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                                       + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
+                                       + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+                                       + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
+                                       + "                     id=\"cryptotest\" format=\"urn:mace:shibboleth:1.0:nameIdentifier\" "
+                                       + "                     type=\"CryptoHandleGenerator\" handleTTL=\"1800\">" 
+                                       + "             <KeyStorePath>"+ file.toURL().toString() + "</KeyStorePath>" 
+                                       + "             <KeyStorePassword>shibhs</KeyStorePassword>"
+                                       + "             <KeyStoreKeyAlias>handlekey</KeyStoreKeyAlias>"
+                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>"
+                                       + "             <Cipher>DESede/CBC/PKCS5Padding</Cipher>" 
+                                       + "             <MAC>HmacSHA1</MAC>"
+                                       + "             <KeyStoreType>JCEKS</KeyStoreType>" 
+                                       + "     </NameMapping>";
+
+                       parser.parse(new InputSource(new StringReader(rawConfig)));
+                       nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
+
+                       SAMLNameIdentifier nameId = nameMapper.getNameIdentifierName("cryptotest", new AuthNPrincipal(
+                                       "testprincipal"), new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
+
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:testid"));
                        assertEquals("Round-trip handle validation failed.", principal.getName(), "testprincipal");
 
                } catch (MalformedURLException e) {
@@ -186,21 +209,61 @@ public class NameMapperTests extends TestCase {
                }
 
        }
+       
+       public void testCryptoMappingBadQualifier() {
+
+               try {
+
+                       HSNameMapper nameMapper = new HSNameMapper();
+
+                       File file = new File("data/handle.jks");
+
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                                       + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
+                                       + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+                                       + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
+                                       + "                     id=\"cryptotest\" format=\"urn:mace:shibboleth:1.0:nameIdentifier\" "
+                                       + "                     type=\"CryptoHandleGenerator\" handleTTL=\"1800\">" 
+                                       + "             <KeyStorePath>" + file.toURL().toString() + "</KeyStorePath>" 
+                                       + "             <KeyStorePassword>shibhs</KeyStorePassword>"
+                                       + "             <KeyStoreKeyAlias>handlekey</KeyStoreKeyAlias>"
+                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>" 
+                                       + "     </NameMapping>";
+
+                       parser.parse(new InputSource(new StringReader(rawConfig)));
+                       nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
+
+                       SAMLNameIdentifier nameId = nameMapper.getNameIdentifierName("cryptotest", new AuthNPrincipal(
+                                       "testprincipal"), new BasicServiceProvider(), new BasicIdentityProvider("urn-x:good"));
+
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:bad"));
+               
+                       fail("Expected failure for bad name qualifier.");
+                       
+               } catch (NameIdentifierMappingException e) {
+                       //This exception should be generated by this test
+                       
+               } catch (MalformedURLException e) {
+                       fail("Error in test specification: " + e.getMessage());
+               
+               } catch (Exception e) {
+                       fail("Error exercising NameMaper: " + e.getMessage());
+               }
+
+       }
+
        public void testDefaultConfig() {
 
                try {
 
                        HSNameMapper nameMapper = new HSNameMapper();
 
-                       SAMLNameIdentifier nameId =
-                               nameMapper.getNameIdentifierName(
-                                       null,
-                                       new AuthNPrincipal("testprincipal"),
-                                       new BasicServiceProvider(),
-                                       new BasicIdentityProvider("urn-x:testid"));
+                       SAMLNameIdentifier nameId = nameMapper.getNameIdentifierName(null, new AuthNPrincipal("testprincipal"),
+                                       new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
 
-                       AuthNPrincipal principal =
-                               nameMapper.getPrincipal(nameId, new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:testid"));
 
                        assertEquals("Round-trip handle validation failed.", principal.getName(), "testprincipal");
 
@@ -219,33 +282,26 @@ public class NameMapperTests extends TestCase {
 
                        File file = new File("data/handle.jks");
 
-                       String rawConfig =
-                               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                                        + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
                                        + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                                        + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
                                        + "                     format=\"urn:mace:shibboleth:1.0:nameIdentifier\""
-                                       + "             type=\"CryptoHandleGenerator\" handleTTL=\"1800\">"
-                                       + "             <KeyStorePath>"
-                                       + file.toURL().toString()
-                                       + "</KeyStorePath>"
+                                       + "             type=\"CryptoHandleGenerator\" handleTTL=\"1800\">" 
+                                       + "             <KeyStorePath>" + file.toURL().toString() + "</KeyStorePath>" 
                                        + "             <KeyStorePassword>shibhs</KeyStorePassword>"
                                        + "             <KeyStoreKeyAlias>handlekey</KeyStoreKeyAlias>"
-                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>"
+                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>" 
                                        + "     </NameMapping>";
 
                        parser.parse(new InputSource(new StringReader(rawConfig)));
                        nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
 
-                       SAMLNameIdentifier nameId =
-                               nameMapper.getNameIdentifierName(
-                                       null,
-                                       new AuthNPrincipal("testprincipal"),
-                                       new BasicServiceProvider(),
-                                       new BasicIdentityProvider("urn-x:testid"));
+                       SAMLNameIdentifier nameId = nameMapper.getNameIdentifierName(null, new AuthNPrincipal("testprincipal"),
+                                       new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
 
-                       AuthNPrincipal principal =
-                               nameMapper.getPrincipal(nameId, new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:testid"));
 
                        assertEquals("Round-trip handle validation failed.", principal.getName(), "testprincipal");
 
@@ -269,48 +325,39 @@ public class NameMapperTests extends TestCase {
 
                        File file = new File("data/handle.jks");
 
-                       String rawConfig =
-                               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                                        + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
                                        + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                                        + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
                                        + "                     format=\"urn:mace:shibboleth:1.0:nameIdentifier\""
-                                       + "             type=\"CryptoHandleGenerator\" handleTTL=\"1800\">"
-                                       + "             <KeyStorePath>"
-                                       + file.toURL().toString()
-                                       + "</KeyStorePath>"
+                                       + "             type=\"CryptoHandleGenerator\" handleTTL=\"1800\">" 
+                                       + "             <KeyStorePath>" + file.toURL().toString() + "</KeyStorePath>" 
                                        + "             <KeyStorePassword>shibhs</KeyStorePassword>"
                                        + "             <KeyStoreKeyAlias>handlekey</KeyStoreKeyAlias>"
-                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>"
+                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>" 
                                        + "     </NameMapping>";
 
                        parser.parse(new InputSource(new StringReader(rawConfig)));
                        nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
 
-                       String rawConfig2 =
-                               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                       String rawConfig2 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                                        + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
                                        + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                                        + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
-                                       + "                     format=\"urn-x:testNameIdentifier\""
+                                       + "                     format=\"urn-x:testNameIdentifier\"" 
                                        + "             type=\"CryptoHandleGenerator\" handleTTL=\"1800\">"
-                                       + "             <KeyStorePath>"
-                                       + file.toURL().toString()
-                                       + "</KeyStorePath>"
+                                       + "             <KeyStorePath>" + file.toURL().toString() + "</KeyStorePath>"
                                        + "             <KeyStorePassword>shibhs</KeyStorePassword>"
                                        + "             <KeyStoreKeyAlias>handlekey</KeyStoreKeyAlias>"
-                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>"
+                                       + "             <KeyStoreKeyPassword>shibhs</KeyStoreKeyPassword>" 
                                        + "     </NameMapping>";
 
                        parser.parse(new InputSource(new StringReader(rawConfig2)));
 
                        nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
 
-                       nameMapper.getNameIdentifierName(
-                               null,
-                               new AuthNPrincipal("testprincipal"),
-                               new BasicServiceProvider(),
-                               new BasicIdentityProvider("urn-x:testid"));
+                       nameMapper.getNameIdentifierName(null, new AuthNPrincipal("testprincipal"), new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:testid"));
 
                        fail("HSNameMapper defaulted to incorrect name mapping.");
 
@@ -322,33 +369,29 @@ public class NameMapperTests extends TestCase {
                        fail("Error exercising NameMaper: " + e.getMessage());
                }
        }
+
        public void testMemoryMapping() {
 
                try {
 
                        HSNameMapper nameMapper = new HSNameMapper();
 
-                       String rawConfig =
-                               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                                        + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
                                        + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
                                        + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
-                                       + "                     id=\"memorytest\" "
+                                       + "                     id=\"memorytest\" " 
                                        + "             format=\"urn:mace:shibboleth:1.0:nameIdentifier\""
                                        + "             type=\"SharedMemoryShibHandle\" handleTTL=\"1800\"/>";
 
                        parser.parse(new InputSource(new StringReader(rawConfig)));
                        nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
 
-                       SAMLNameIdentifier nameId =
-                               nameMapper.getNameIdentifierName(
-                                       "memorytest",
-                                       new AuthNPrincipal("testprincipal"),
-                                       new BasicServiceProvider(),
-                                       new BasicIdentityProvider("urn-x:testid"));
+                       SAMLNameIdentifier nameId = nameMapper.getNameIdentifierName("memorytest", new AuthNPrincipal(
+                                       "testprincipal"), new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
 
-                       AuthNPrincipal principal =
-                               nameMapper.getPrincipal(nameId, new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:testid"));
 
                        assertEquals("Round-trip handle validation failed.", principal.getName(), "testprincipal");
 
@@ -360,6 +403,42 @@ public class NameMapperTests extends TestCase {
                        fail("Error exercising NameMaper: " + e.getMessage());
                }
        }
+       
+       public void testMemoryMappingBadQualifier() {
+
+               try {
+
+                       HSNameMapper nameMapper = new HSNameMapper();
+
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                                       + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
+                                       + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+                                       + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
+                                       + "                     id=\"memorytest\" " 
+                                       + "             format=\"urn:mace:shibboleth:1.0:nameIdentifier\""
+                                       + "             type=\"SharedMemoryShibHandle\" handleTTL=\"1800\"/>";
+
+                       parser.parse(new InputSource(new StringReader(rawConfig)));
+                       nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
+
+                       SAMLNameIdentifier nameId = nameMapper.getNameIdentifierName("memory", new AuthNPrincipal(
+                                       "testprincipal"), new BasicServiceProvider(), new BasicIdentityProvider("urn-x:good"));
+
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:bad"));
+               
+                       fail("Expected failure for bad name qualifier.");
+                       
+               } catch (NameIdentifierMappingException e) {
+                       //This exception should be generated by this test
+                       
+               } catch (MalformedURLException e) {
+                       fail("Error in test specification: " + e.getMessage());
+               
+               } catch (Exception e) {
+                       fail("Error exercising NameMaper: " + e.getMessage());
+               }
+       }
 
        public void testPrincipalMapping() {
 
@@ -368,22 +447,18 @@ public class NameMapperTests extends TestCase {
                        NameMapper nameMapper = new NameMapper();
 
                        String format = "urn-x:test:NameIdFormat1";
-                       String rawConfig =
-                               "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
                                        + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
                                        + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
-                                       + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" "
-                                       + "                     format=\""
-                                       + format
-                                       + "\""
-                                       + "             type=\"Principal\"/>";
+                                       + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" " 
+                                       + "                     format=\"" + format + "\"" + "          type=\"Principal\"/>";
 
                        parser.parse(new InputSource(new StringReader(rawConfig)));
                        nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
 
                        SAMLNameIdentifier nameId = new SAMLNameIdentifier("testprincipal", "urn-x:testid", format);
-                       AuthNPrincipal principal =
-                               nameMapper.getPrincipal(nameId, new BasicServiceProvider(), new BasicIdentityProvider("urn-x:testid"));
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:testid"));
 
                        assertEquals("Round-trip handle validation failed.", principal.getName(), "testprincipal");
 
@@ -396,28 +471,68 @@ public class NameMapperTests extends TestCase {
                }
 
        }
+       
+       public void testPrincipalMappingBadQualifier() {
+
+               try {
+
+                       NameMapper nameMapper = new NameMapper();
+
+                       String format = "urn-x:test:NameIdFormat1";
+                       String rawConfig = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+                                       + "<NameMapping xmlns=\"urn:mace:shibboleth:namemapper:1.0\""
+                                       + "             xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""
+                                       + "             xsi:schemaLocation=\"urn:mace:shibboleth:namemapper:1.0 namemapper.xsd\" " 
+                                       + "                     format=\"" + format + "\"" + "          type=\"Principal\"/>";
+                       parser.parse(new InputSource(new StringReader(rawConfig)));
+                       nameMapper.addNameMapping(parser.getDocument().getDocumentElement());
+
+                       SAMLNameIdentifier nameId = new SAMLNameIdentifier("testprincipal", "urn-x:good", format);
+
+                       AuthNPrincipal principal = nameMapper.getPrincipal(nameId, new BasicServiceProvider(),
+                                       new BasicIdentityProvider("urn-x:bad"));
+               
+                       fail("Expected failure for bad name qualifier.");
+                       
+               } catch (NameIdentifierMappingException e) {
+                       //This exception should be generated by this test
+                       
+               } catch (MalformedURLException e) {
+                       fail("Error in test specification: " + e.getMessage());
+               
+               } catch (Exception e) {
+                       fail("Error exercising NameMaper: " + e.getMessage());
+               }
+
+       }
 }
 
 class BasicIdentityProvider implements IdentityProvider {
+
        String id;
 
        public BasicIdentityProvider(String id) {
+
                this.id = id;
        }
 
        public String getProviderId() {
+
                return id;
        }
 
        public Credential getResponseSigningCredential() {
+
                return null;
        }
 
        public Credential getAssertionSigningCredential() {
+
                return null;
        }
 
 }
+
 class BasicServiceProvider implements ServiceProvider {
 
        public String getProviderId() {
@@ -425,4 +540,4 @@ class BasicServiceProvider implements ServiceProvider {
                return null;
        }
 
-}
+}
\ No newline at end of file