BksStore.java

/*******************************************************************************
 * Copyright (c) 2019, RISE AB
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions 
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 *
 * 2. 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.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
 * HOLDER OR CONTRIBUTORS 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 se.sics.ace.coap;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.eclipse.californium.scandium.dtls.ConnectionId;
import org.eclipse.californium.scandium.dtls.HandshakeResultHandler;
import org.eclipse.californium.scandium.dtls.PskPublicInformation;
import org.eclipse.californium.scandium.dtls.PskSecretResult;
import org.eclipse.californium.scandium.dtls.pskstore.AdvancedPskStore;
import org.eclipse.californium.scandium.util.ServerNames;

/**
 * A PskStore implementation based on BKS.
 * 
 * This will retrieve keys from a BKS keystore.
 * 
 * In order too keep this manageable all keys will 
 * have the same password as the keystore.
 * 
 * @author Ludwig Seitz
 *
 */
public class BksStore implements AdvancedPskStore {
    
    /**
     * The logger
     */
    private static final Logger LOGGER 
        = Logger.getLogger(BksStore.class.getName());

    /**
     * The underlying BKS keystore
     */
    private KeyStore keystore = null;
    
    /**
     * The in-memory map of addresses to identities
     */
    private Map<String, String> addr2id = new HashMap<>();
    
    /**
     * The file storing the keystore
     */
    private String keystoreFile;
    
    /**
     * The keystore password
     */
    private String keystorePwd;
    
    private String addr2IdFile;
    
    static {
        Security.addProvider(
                new org.bouncycastle.jce.provider.BouncyCastleProvider());
    }
    
    /**
     * Constructor.
     * 
     * @param keystoreLocation  the location of the keystore file
     * @param keystorePwd the password to the keystore
     * @param addr2idFile  the location of the file mapping addresses to identities
     * 
     * @throws IOException 
     * @throws CertificateException 
     * @throws NoSuchAlgorithmException 
     * @throws KeyStoreException 
     * @throws NoSuchProviderException 
     */
    public BksStore(String keystoreLocation, String keystorePwd, String addr2idFile) 
            throws NoSuchAlgorithmException, CertificateException, 
            IOException, KeyStoreException, NoSuchProviderException {
        this.keystoreFile = keystoreLocation;
        this.keystorePwd = keystorePwd;
        InputStream keystoreStream = new FileInputStream(keystoreLocation);
        this.keystore = KeyStore.getInstance("BKS", "BC");
        this.keystore.load(keystoreStream, keystorePwd.toCharArray());
        keystoreStream.close();   
        this.addr2IdFile  = addr2idFile;
        BufferedReader in = new BufferedReader(new FileReader(addr2idFile));
        String line = "";
        while ((line = in.readLine()) != null) {
            String parts[] = line.split(":");
            this.addr2id.put((parts[0].trim() + ":" + parts[1].trim()),
                    parts[2].trim());
        }
        in.close();
    }
    
    /**
     * Create the initial keystore and address2identity mapping file.
     * 
     * @param keystoreLocation  the location of the keystore file
     * @param keystorePwd the password to the keystore
     * @param addr2idFile  the location of the file mapping addresses to identities
     * 
     * @throws NoSuchProviderException 
     * @throws KeyStoreException 
     * @throws IOException 
     * @throws FileNotFoundException 
     * @throws CertificateException 
     * @throws NoSuchAlgorithmException 
     */
    public static void init(String keystoreLocation, String keystorePwd,
            String addr2idFile) throws KeyStoreException, 
            NoSuchProviderException, NoSuchAlgorithmException, 
            CertificateException, FileNotFoundException, IOException {
        KeyStore keyStore = KeyStore.getInstance("BKS", "BC");
        keyStore.load(null, keystorePwd.toCharArray());
        FileOutputStream fo = new FileOutputStream(keystoreLocation);
        keyStore.store(fo, keystorePwd.toCharArray());
        fo.close();   
        File file = new File(addr2idFile);
        file.createNewFile();        
    }

    @Override
    public PskSecretResult requestPskSecretResult(ConnectionId cid, ServerNames serverName,
            PskPublicInformation identity, String hmacAlgorithm, SecretKey otherSecret, byte[] seed,
            boolean useExtendedMasterSecret) {

        String identityStr = identity.getPublicInfoAsString();
        try {
            if (!this.keystore.containsAlias(identityStr)) {
                return null;
            }
        } catch (KeyStoreException e) {
            LOGGER.severe("KeyStoreException: " + e.getMessage());
            return null;
        }

        Key key;
        try {
            // XXX: Note that we use the keystore password for all key passwords
            key = this.keystore.getKey(identityStr, this.keystorePwd.toCharArray());
        } catch (UnrecoverableKeyException | KeyStoreException | NoSuchAlgorithmException e) {
            LOGGER.severe(e.getClass().getName() + ": " + e.getMessage());
            return null;
        }

        return new PskSecretResult(cid, identity, (SecretKey) key);
    }

    public SecretKey getKey(PskPublicInformation info) {
        PskSecretResult result = requestPskSecretResult(ConnectionId.EMPTY, null, info, null, null, null, false);

        if (result == null) {
            return null;
        } else {
            return result.getSecret();
        }

    }

    @Override
    public PskPublicInformation getIdentity(InetSocketAddress inetAddress, ServerNames virtualHost) {
        String id = inetAddress.getHostString() + ":" + inetAddress.getPort();
        String identity = this.addr2id.get(id);
        if (identity != null) {
            return new PskPublicInformation(identity);
        }
        return null;

    }

    public PskPublicInformation getIdentity(InetSocketAddress inetAddress) {
        return getIdentity(inetAddress, null);

    }

    /**
     * Add a new symmetric key to the keystore or overwrite the existing
     * one associated to this identity.
     * 
     * @param key  the bytes of java.security.Key.getEncoded()
     * @param identity  the key identity
     * @param address  the address to associate with this key (can be null)
     * @throws KeyStoreException 
     * @throws IOException 
     * @throws FileNotFoundException 
     * @throws CertificateException 
     * @throws NoSuchAlgorithmException 
     */
    public void addKey(byte[] key, String identity) 
            throws KeyStoreException, NoSuchAlgorithmException, 
            CertificateException, FileNotFoundException, IOException {
        if (identity == null || key == null) {
            throw new KeyStoreException("Key and identity must not be null");
        }
        if (this.keystore != null) {
            Key k = new SecretKeySpec(key, PskSecretResult.ALGORITHM_PSK);
            //XXX: Note that we use the keystore password for all key passwords
            this.keystore.setKeyEntry(identity, k, 
                    this.keystorePwd.toCharArray(), null);
            FileOutputStream fos = new FileOutputStream(this.keystoreFile);
            this.keystore.store(fos, this.keystorePwd.toCharArray());
            fos.close();
        }
    }
    
    /**
     * Add a new mapping of key identity to Internet address.
     * 
     * @param address the Internet address
     * @param identity  the key identity
     * @throws KeyStoreException 
     * @throws IOException 
     */
    public void addAddress(InetSocketAddress address, String identity) 
            throws KeyStoreException, IOException {
        if (hasKey(identity)) {
            this.addr2id.put(address.getHostString()+":"+address.getPort(),
                    identity);
        }
        PrintWriter writer = new PrintWriter(this.addr2IdFile, "UTF-8");
        for (Map.Entry<String, String> e 
                : this.addr2id.entrySet()) {        
            String line = e.getKey() + ":" + e.getValue();
            writer.println(line);  
        }
        writer.close();      
    }
    
    /**
     * Checks if a key for a certain identity is present.
     * 
     * @param identity  the key identity
     * 
     * @return  true if the identity is in the keystore, false otherwise
     * 
     * @throws KeyStoreException 
     */
    public boolean hasKey(String identity) throws KeyStoreException {
        if (identity != null) {
            if (this.keystore != null) {
                return this.keystore.isKeyEntry(identity);
            }
            throw new KeyStoreException("No keystore loaded");
        }
        throw new KeyStoreException("Key identity can not be null");
    }
    
    /**
     * Remove a symmetric key from the keystore, will do nothing if the
     * key doesn't exist.
     * 
     * @param identity  the key identity
     * @throws KeyStoreException 
     * @throws IOException 
     * @throws CertificateException 
     * @throws NoSuchAlgorithmException 
     */
    public void removeKey(String identity) throws KeyStoreException, 
           NoSuchAlgorithmException, CertificateException, IOException {
        if (identity != null) {
            if (this.keystore != null) {
                if (this.keystore.isKeyEntry(identity)) {
                    this.keystore.deleteEntry(identity);
                    FileOutputStream fos = new FileOutputStream(this.keystoreFile);
                    this.keystore.store(fos, this.keystorePwd.toCharArray());
                    fos.close();
                }
                return;
            }
            throw new KeyStoreException("No keystore loaded");
        }
        throw new KeyStoreException("Key identity can not be null");
    }

    @Override
    public boolean hasEcdhePskSupported() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public void setResultHandler(HandshakeResultHandler resultHandler) {
        // TODO Auto-generated method stub

    }

}