org.dcache.xrootd.plugins.authn.gsi.DHSession.java Source code

Java tutorial

Introduction

Here is the source code for org.dcache.xrootd.plugins.authn.gsi.DHSession.java

Source

/**
 * Copyright (C) 2011-2018 dCache.org <support@dcache.org>
 *
 * This file is part of xrootd4j.
 *
 * xrootd4j is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * xrootd4j is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with xrootd4j.  If not, see http://www.gnu.org/licenses/.
 */
package org.dcache.xrootd.plugins.authn.gsi;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.pkcs.DHParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyAgreement;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.util.Arrays;

/**
 * This class represents a Diffie-Hellman (DH) session. After the DH key agreement
 * has been completed, the resulting session key can be used for (symmetric) encryption/
 * decryption.
 *
 * @author radicke
 * @author tzangerl
 *
 */
public class DHSession {
    private static final Logger LOGGER = LoggerFactory.getLogger(DHSession.class);

    private static final String DH_ALGORITHM_NAME = "DH";
    private static final String DH_HEADER = "-----BEGIN DH PARAMETERS-----";
    private static final String DH_FOOTER = "-----END DH PARAMETERS-----";
    private static final String DH_PUBKEY_HEADER = "---BPUB---";
    private static final String DH_PUBKEY_FOOTER = "---EPUB---";

    // The 512-bit prime being part of the DH parameter set.
    // This specific number set was created by using Openssl and passes
    // its validity tests and is therefore considered to be safe.
    private static final String DH_PRIME = ("00:a8:37:9d:6f:ff:e8:63:a0:b1:47:0c:26:dd:1a:"
            + "45:0b:e2:03:9a:f0:83:b1:ba:5b:fa:1d:2f:5b:2a:" + "89:08:02:d8:c4:d4:66:8d:14:8d:35:bb:24:b1:af:"
            + "1a:d3:75:c7:c0:3b:61:aa:85:3f:56:69:ae:f2:67:" + "da:20:87:5d:93").replaceAll("[:\\s]+", "");

    private static String printBytesAsHex(byte[] array) {
        StringBuilder sb = new StringBuilder();
        for (byte b : array) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString();
    }

    // the 512 bit DH parameter set used for all DH sessions, consisting
    // of the prime above and the generator value of 2
    // These default values are only used when dCache acts as the server
    static final DHParameterSpec DH_PARAMETERS = new DHParameterSpec(new BigInteger(DH_PRIME, 16),
            BigInteger.valueOf(2));
    private DHParameterSpec _dhParameterSpec;
    private KeyPair _localDHKeyPair;
    private KeyAgreement _keyAgreement;

    /**
     * Construct new Diffie-Hellman key exchange session
     * @throws InvalidAlgorithmParameterException Invalid DH parameters (primes)
     * @throws NoSuchAlgorithmException DH algorithm not available in VM
     * @throws InvalidKeyException Private key generated by DH generator invalid
     * @throws NoSuchProviderException Bouncy castle provider does not exist
     */
    public DHSession(boolean isServer) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            InvalidKeyException, NoSuchProviderException {
        if (isServer) {
            _dhParameterSpec = DH_PARAMETERS;
            initialize();
        }
    }

    private void initialize() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
            InvalidKeyException, NoSuchProviderException {
        KeyPairGenerator kpairGen = KeyPairGenerator.getInstance(DH_ALGORITHM_NAME, "BC");
        kpairGen.initialize(_dhParameterSpec);
        _localDHKeyPair = kpairGen.generateKeyPair();
        _keyAgreement = KeyAgreement.getInstance(DH_ALGORITHM_NAME, "BC");
        _keyAgreement.init(_localDHKeyPair.getPrivate());
    }

    public String getEncodedDHMaterial() throws IOException {
        String dhparams = CertUtil.toPEM(toDER(_dhParameterSpec), DH_HEADER, DH_FOOTER);
        DHPublicKey pubkey = (DHPublicKey) _localDHKeyPair.getPublic();

        return dhparams + '\n' + DH_PUBKEY_HEADER + pubkey.getY().toString(16) + DH_PUBKEY_FOOTER;
    }

    public void finaliseKeyAgreement(String dhmessage)
            throws IOException, GeneralSecurityException, IllegalStateException {
        int delimitingIndex = dhmessage.indexOf(DH_PUBKEY_HEADER);

        if (delimitingIndex < 0 || delimitingIndex >= dhmessage.length()) {
            throw new IllegalArgumentException("Illegal DH message: " + dhmessage);
        }

        String dhparams = dhmessage.substring(0, delimitingIndex);
        String remotePubKeyString = dhmessage.substring(delimitingIndex);

        DHParameterSpec params = fromDER(CertUtil.fromPEM(dhparams, DH_HEADER, DH_FOOTER));

        LOGGER.trace("Remote endpoint sent: P = {}, G = {}, L = {},", params.getP(), params.getG(), params.getL());

        if (_keyAgreement == null) {
            int l = params.getL();
            /*
             * Note:  setting the value of L to the non-zero bit length of P
             * seems to be required for the key pair generation to work.
             */
            _dhParameterSpec = new DHParameterSpec(params.getP(), params.getG(),
                    l == 0 ? params.getP().bitLength() : l);
            initialize();
        } else if (!(_dhParameterSpec.getP().equals(params.getP())
                && _dhParameterSpec.getG().equals(params.getG()))) {
            throw new GeneralSecurityException("remote DH parameters differ from local ones");
        }

        removeCharFromString(remotePubKeyString, '\n');

        int envLength = DH_PUBKEY_HEADER.length();
        remotePubKeyString = remotePubKeyString.substring(envLength, remotePubKeyString.length() - envLength);

        // parse hex String into a BigInt
        BigInteger remoteY = new BigInteger(remotePubKeyString, 16);

        // convert into a public key
        KeyFactory keyfac = KeyFactory.getInstance(DH_ALGORITHM_NAME, "BC");
        PublicKey remotePubKey = keyfac.generatePublic(new DHPublicKeySpec(remoteY, params.getP(), params.getG()));

        // finalise DH key agreement
        _keyAgreement.doPhase(remotePubKey, true);
    }

    public byte[] decrypt(String cipherSpec, String keySpec, int blocksize, byte[] encrypted)
            throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchPaddingException,
            IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException,
            NoSuchProviderException {
        return translate(cipherSpec, keySpec, blocksize, encrypted, Cipher.DECRYPT_MODE);
    }

    public byte[] encrypt(String cipherSpec, String keySpec, int blocksize, byte[] unencrypted)
            throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchPaddingException,
            IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException,
            NoSuchProviderException {
        return translate(cipherSpec, keySpec, blocksize, unencrypted, Cipher.ENCRYPT_MODE);
    }

    private byte[] translate(String cipherSpec, String keySpec, int blocksize, byte[] buffer, int mode)
            throws InvalidKeyException, IllegalStateException, NoSuchAlgorithmException, NoSuchPaddingException,
            IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException,
            NoSuchProviderException {
        byte[] iv = new byte[blocksize];
        Arrays.fill(iv, (byte) 0);
        IvParameterSpec paramSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance(cipherSpec, "BC");

        /**
         * "TlsPremasterSecret" algorithm forces pre 1.50
         * bouncy castle behavior ofgeneration of secret
         * for compatibility with xroord client
         */
        byte[] encoded = _keyAgreement.generateSecret("TlsPremasterSecret").getEncoded();

        /**
         * Use of the TlsPremasterSecret encoding on encryption can sometimes produce
         * an array where the final 0's have been truncated.  This unfortunately
         * does not play with the key finalization.   Here we simply add
         * back the missing padding.
         *
         * Note:  the non-Tls encoding pads by prepending, not appending; this
         * seems to be unacceptable to servers using the ssl DH_compute_key
         * (unpadded) method.
         */
        if (encoded.length < blocksize && mode == Cipher.ENCRYPT_MODE) {
            byte[] defective = encoded;
            encoded = Arrays.copyOf(defective, blocksize);
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("Adjusting truncated encoded array by appending 0s; " + "old {}, " + "new {}.",
                        printBytesAsHex(defective), printBytesAsHex(encoded));
            }
        }

        /* need a 128-bit key, that's the way to get it */
        SecretKey sessionKey = new SecretKeySpec(_keyAgreement.generateSecret("TlsPremasterSecret").getEncoded(), 0,
                blocksize, keySpec);
        cipher.init(mode, sessionKey, paramSpec);
        return cipher.doFinal(buffer);
    }

    /**
     * remove all occurences of a character
     *
     * @param s
     *            the string
     * @param c
     *            the char to be removed
     * @return the resulting string
     */
    private static String removeCharFromString(String s, char c) {
        return s.replaceAll(String.valueOf(c), "");
    }

    /**
     * Creates an DHParameterSpec object from the DER-encoded byte sequence
     * @param der the DER-encoded byte sequence
     * @return the DHParameterSpec object
     * @throws IOException if the deserialisation goes wrong
     */
    private static DHParameterSpec fromDER(byte[] der) throws IOException {
        ByteArrayInputStream inStream = new ByteArrayInputStream(der);
        ASN1InputStream derInputStream = new ASN1InputStream(inStream);
        DHParameter dhparam = DHParameter.getInstance(derInputStream.readObject());
        return new DHParameterSpec(dhparam.getP(), dhparam.getG());
    }

    /**
     * Creates an DER-encoded byte sequence from the DHParameter object
     * @param paramspec the DH parameter object
     * @return the DER-encoded byte sequence of the DH Parameter object
     */
    private static byte[] toDER(DHParameterSpec paramspec) throws IOException {
        DHParameter derParams = new DHParameter(paramspec.getP(), // Prime
                // (public
                // key)
                paramspec.getG(), // generator
                paramspec.getP().bitLength()); // keylength of Prime

        return derParams.getEncoded("DER");
    }
}