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}