/**
* Copyright 2012, Board of Regents of the University of
* Wisconsin System. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Board of Regents of the University of Wisconsin
* System licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package edu.wisc.doit.tcrypt;
import java.io.IOException;
import java.io.Reader;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.regex.Matcher;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.crypto.AsymmetricBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.digests.GeneralDigest;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
/**
* @author Eric Dalquist
*/
public class BouncyCastleTokenDecrypter extends AbstractPublicKeyDecrypter implements TokenDecrypter {
public BouncyCastleTokenDecrypter(AsymmetricKeyParameter publicKeyParam, AsymmetricKeyParameter privateKeyParam) {
super(publicKeyParam, privateKeyParam);
}
public BouncyCastleTokenDecrypter(KeyPair keyPair) throws IOException {
super(keyPair);
}
public BouncyCastleTokenDecrypter(PEMKeyPair keyPair) throws IOException {
super(keyPair);
}
/**
* Create a token encrypter and decrypter using the specified {@link Reader}, note the
* caller is responsible for closing the Reader.
*
* @param privateKeyReader Reader to load the {@link KeyPair} from
*/
@SuppressWarnings("resource")
public BouncyCastleTokenDecrypter(Reader privateKeyReader) throws IOException {
this((PEMKeyPair)new PEMParser(privateKeyReader).readObject());
}
@Override
public boolean isEncryptedToken(String ciphertext) {
return TOKEN_PATTERN.matcher(ciphertext).matches();
}
@Override
public String decrypt(String ciphertext) throws InvalidCipherTextException {
final Matcher tokenMatcher = TOKEN_PATTERN.matcher(ciphertext);
if (tokenMatcher.matches()) {
ciphertext = tokenMatcher.group(1);
}
else {
final Matcher base64Matcher = BASE64_PATTERN.matcher(ciphertext);
if (!base64Matcher.matches()) {
throw new IllegalArgumentException("Specified ciphertext is not valid");
}
}
//Decode the cipher text
final byte[] encryptedTokenWithHash = Base64.decodeBase64(ciphertext);
final AsymmetricBlockCipher e = getDecryptCipher();
final byte[] tokenWithHashBytes = e.processBlock(encryptedTokenWithHash, 0, encryptedTokenWithHash.length);
//Split the decrypted text into the password and the hash
final String tokenWithHash = new String(tokenWithHashBytes, BouncyCastleTokenEncrypter.CHARSET);
final int seperatorIndex = tokenWithHash.lastIndexOf(BouncyCastleTokenEncrypter.SEPARATOR);
if (seperatorIndex < 0) {
throw new IllegalArgumentException("token/hash string doesn't contain seperator: " + BouncyCastleTokenEncrypter.SEPARATOR);
}
final byte[] passwordBytes = tokenWithHash.substring(0, seperatorIndex).getBytes(BouncyCastleTokenEncrypter.CHARSET);
final byte[] passwordHashBytes = Base64.decodeBase64(tokenWithHash.substring(seperatorIndex + 1).getBytes(BouncyCastleTokenEncrypter.CHARSET));
//Generate hash of the decrypted password
final GeneralDigest digest = this.createDigester();
digest.update(passwordBytes, 0, passwordBytes.length);
final byte[] expectedHashBytes = new byte[digest.getDigestSize()];
digest.doFinal(expectedHashBytes, 0);
//Verify the generated hash against the decrypted hash
if (!Arrays.equals(expectedHashBytes, passwordHashBytes)) {
throw new IllegalArgumentException("Hash of the decrypted token does not match the decrypted hash");
}
return new String(passwordBytes, BouncyCastleTokenEncrypter.CHARSET);
}
protected GeneralDigest createDigester() {
return new MD5Digest();
}
}