001package org.apache.archiva.redback.authentication;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.commons.codec.binary.Base64;
023import org.apache.commons.lang3.ArrayUtils;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026import org.springframework.stereotype.Service;
027
028import javax.annotation.PostConstruct;
029import javax.crypto.BadPaddingException;
030import javax.crypto.Cipher;
031import javax.crypto.IllegalBlockSizeException;
032import javax.crypto.KeyGenerator;
033import javax.crypto.NoSuchPaddingException;
034import javax.crypto.SecretKey;
035import javax.crypto.spec.IvParameterSpec;
036import java.io.ByteArrayInputStream;
037import java.io.ByteArrayOutputStream;
038import java.io.IOException;
039import java.io.InvalidClassException;
040import java.io.ObjectInputStream;
041import java.io.ObjectOutputStream;
042import java.security.InvalidAlgorithmParameterException;
043import java.security.InvalidKeyException;
044import java.security.NoSuchAlgorithmException;
045import java.security.SecureRandom;
046import java.util.Arrays;
047
048
049/**
050 *
051 * Class that manages tokens that are encrypted with a dynamic key. The tokens
052 * are converted into BASE64 strings.
053 *
054 * Each token contains information about username,
055 *
056 * Created by Martin Stockhammer on 03.02.17.
057 */
058@Service("tokenManager#jce")
059public class TokenManager {
060
061    private final ThreadLocal<SecureRandom> rd = new ThreadLocal<SecureRandom>();
062    private final Logger log = LoggerFactory.getLogger(getClass());
063    private String algorithm = "AES/CBC/PKCS5Padding";
064    private int keySize = -1;
065    private int ivSize = -1;
066    private SecretKey secretKey;
067
068    boolean paddingUsed = true;
069
070
071    @PostConstruct
072    public void initialize() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, EncryptionFailedException, InvalidAlgorithmParameterException {
073        log.debug("Initializing key for token generator");
074        try {
075            rd.set(new SecureRandom());
076            Cipher enCipher = Cipher.getInstance(algorithm);
077            String[] keyAlg = enCipher.getAlgorithm().split("/");
078            if (keyAlg.length<1) {
079                throw new EncryptionFailedException("Initialization of key failed. Not algorithm found.");
080            }
081            String encryptionAlgorithm = keyAlg[0];
082            KeyGenerator keyGen = KeyGenerator.getInstance(encryptionAlgorithm);
083            if (keySize>0) {
084                keyGen.init(keySize);
085            }
086            if (keyAlg.length==3 && keyAlg[2].equals("NoPadding")) {
087                paddingUsed=false;
088            }
089            this.secretKey = keyGen.generateKey();
090            enCipher.init(Cipher.ENCRYPT_MODE, secretKey);
091            // We have to provide the IV depending on the algorithm used
092            // CBC needs an IV, ECB not.
093            if (enCipher.getIV()==null) {
094                ivSize=-1;
095            } else {
096                ivSize=enCipher.getIV().length;
097            }
098        } catch (NoSuchAlgorithmException e) {
099            log.error("Error occurred during key initialization. Requested algorithm not available. "+e.getMessage());
100            throw e;
101        } catch (NoSuchPaddingException e) {
102            log.error("Error occurred during key initialization. Requested padding not available. "+e.getMessage());
103            throw e;
104        } catch (InvalidKeyException e) {
105            log.error("The key is not valid.");
106            throw e;
107        }
108    }
109
110    public String encryptToken(String user, long lifetime) throws EncryptionFailedException {
111        return encryptToken(new SimpleTokenData(user, lifetime, createNonce()));
112    }
113
114    public String encryptToken(TokenData tokenData) throws EncryptionFailedException {
115        try {
116            return encode(encrypt(tokenData));
117        } catch (IOException e) {
118            log.error("Error during object conversion: "+e.getMessage());
119            throw new EncryptionFailedException(e);
120        } catch (BadPaddingException e) {
121            log.error("Padding invalid");
122            throw new EncryptionFailedException(e);
123        } catch (IllegalBlockSizeException e) {
124            log.error("Block size invalid");
125            throw new EncryptionFailedException(e);
126        } catch (NoSuchPaddingException e) {
127            log.error("Padding not available "+algorithm);
128            throw new EncryptionFailedException(e);
129        } catch (InvalidKeyException e) {
130            log.error("Bad encryption key");
131            throw new EncryptionFailedException(e);
132        } catch (NoSuchAlgorithmException e) {
133            log.error("Bad encryption algorithm "+algorithm);
134            throw new EncryptionFailedException(e);
135        } catch (InvalidAlgorithmParameterException e) {
136            log.error("Invalid encryption parameters");
137            throw new EncryptionFailedException(e);
138        }
139    }
140
141    public TokenData decryptToken(String token) throws InvalidTokenException {
142        try {
143            return decrypt(decode(token));
144        } catch (IOException ex) {
145            log.error("Error during data read. " + ex.getMessage());
146            throw new InvalidTokenException(ex);
147        } catch (ClassNotFoundException ex) {
148            log.error("Token data invalid.");
149            throw new InvalidTokenException(ex);
150        } catch (BadPaddingException ex) {
151            log.error("The encrypted token has the wrong padding.");
152            throw new InvalidTokenException(ex);
153        } catch (IllegalBlockSizeException ex) {
154            log.error("The encrypted token has the wrong block size.");
155            throw new InvalidTokenException(ex);
156        } catch (NoSuchPaddingException e) {
157            log.error("Padding not available "+algorithm);
158            throw new InvalidTokenException(e);
159        } catch (InvalidKeyException e) {
160            log.error("Invalid decryption key");
161            throw new InvalidTokenException(e);
162        } catch (NoSuchAlgorithmException e) {
163            log.error("Encryption algorithm not available "+algorithm);
164            throw new InvalidTokenException(e);
165        } catch (InvalidAlgorithmParameterException e) {
166            log.error("Invalid encryption parameters");
167            throw new InvalidTokenException(e);
168        }
169    }
170
171    private long createNonce() {
172        if (rd.get()==null) {
173            rd.set(new SecureRandom());
174        }
175        return rd.get().nextLong();
176    }
177
178    protected byte[] encrypt(TokenData info) throws IOException, BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
179        return doEncrypt(convertToByteArray(info), info.getNonce());
180    }
181
182    private byte[] getIv(long nonce) {
183        byte[] iv = new byte[ivSize];
184        SecureRandom sr = getRandomGenerator();
185        sr.setSeed(nonce);
186        sr.nextBytes(iv);
187        return iv;
188    }
189
190    protected byte[] doEncrypt(byte[] data, long nonce) throws BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
191        Cipher cipher = getEnCipher();
192        byte[] encData;
193        byte[] iv;
194        if (ivSize>0) {
195            iv = getIv(nonce);
196            cipher.init(Cipher.ENCRYPT_MODE,this.secretKey,new IvParameterSpec(iv));
197        } else {
198            iv = new byte[0];
199            cipher.init(Cipher.ENCRYPT_MODE,this.secretKey);
200        }
201        if (!paddingUsed && (data.length % cipher.getBlockSize())!=0) {
202            int blocks = data.length / cipher.getBlockSize();
203            encData = Arrays.copyOf(data, cipher.getBlockSize()*(blocks+1));
204        } else {
205            encData = data;
206        }
207        byte[] encrypted = cipher.doFinal(encData);
208        // prepending the iv to the token
209        return ArrayUtils.addAll(iv,encrypted);
210    }
211
212    protected TokenData decrypt(byte[] token) throws BadPaddingException, IllegalBlockSizeException, IOException, ClassNotFoundException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException {
213        Object result = convertFromByteArray(doDecrypt(token));
214        if (!(result instanceof TokenData)) {
215            throw new InvalidClassException("No TokenData found in decrypted token");
216        }
217        return (TokenData)result;
218    }
219
220    protected byte[] doDecrypt(byte[] encryptedData) throws BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
221        Cipher cipher = getDeCipher();
222        if (ivSize>0) {
223            byte[] iv = Arrays.copyOfRange(encryptedData,0,ivSize);
224            cipher.init(Cipher.DECRYPT_MODE,this.secretKey,new IvParameterSpec(iv));
225            return cipher.doFinal(encryptedData,ivSize,encryptedData.length-ivSize);
226        } else {
227            cipher.init(Cipher.DECRYPT_MODE,this.secretKey);
228            return cipher.doFinal(encryptedData);
229        }
230    }
231
232    private SecureRandom getRandomGenerator() {
233        if (rd.get()==null) {
234            rd.set(new SecureRandom());
235        }
236        return rd.get();
237    }
238
239    private Cipher getEnCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
240        return Cipher.getInstance(algorithm);
241    }
242
243    private Cipher getDeCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
244        return Cipher.getInstance(algorithm);
245    }
246
247    private String encode(byte[] token) {
248        return Base64.encodeBase64String(token);
249    }
250
251    private byte[] decode(String token) {
252        return Base64.decodeBase64(token);
253    }
254
255
256    private Object convertFromByteArray(byte[] byteObject) throws IOException,
257            ClassNotFoundException {
258        ByteArrayInputStream bais;
259        ObjectInputStream in;
260        bais = new ByteArrayInputStream(byteObject);
261        in = new ObjectInputStream(bais);
262        Object o = in.readObject();
263        in.close();
264        return o;
265
266    }
267
268
269    private byte[] convertToByteArray(Object complexObject) throws IOException {
270        ByteArrayOutputStream baos;
271        ObjectOutputStream out;
272        baos = new ByteArrayOutputStream();
273        out = new ObjectOutputStream(baos);
274        out.writeObject(complexObject);
275        out.close();
276        return baos.toByteArray();
277    }
278
279    public String getAlgorithm() {
280        return algorithm;
281    }
282
283    /**
284     * Sets the encryption algorithm and resets the key size. You may change the key size after
285     * calling this method.
286     * Additionally run the initialize() method after setting algorithm and keysize.
287     *
288     *
289     * @param algorithm The encryption algorithm to use.
290     */
291    public void setAlgorithm(String algorithm) {
292        if (!this.algorithm.equals(algorithm)) {
293            this.algorithm = algorithm;
294            this.keySize=-1;
295        }
296    }
297
298    public int getKeySize() {
299        return keySize;
300    }
301
302    /**
303     * Sets the key size for the encryption. This method must be called after
304     * setting the algorithm. The keysize will be reset by calling <code>setAlgorithm()</code>
305     *
306     * The key size must be valid for the given algorithm.
307     *
308     * @param keySize The size of the encryption key
309     */
310    public void setKeySize(int keySize) {
311        this.keySize = keySize;
312    }
313}