/*
* Copyright 2014 Stormpath, Inc.
*
* Licensed 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 com.stormpath.sdk.impl.security;
import com.stormpath.sdk.impl.util.Base64;
import com.stormpath.sdk.lang.Assert;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.security.spec.KeySpec;
/**
* @since 1.0.RC
*/
public class ApiKeySecretEncryptionService implements EncryptionService {
private static String ALGORITHM = "PBKDF2WithHmacSHA1";
private static final Charset UTF_8 = Charset.forName("UTF-8");
private static final int BITS_PER_BYTE = 8;
private final SecretKey key;
private final Builder builder;
private final Cipher cipher;
private ApiKeySecretEncryptionService(Builder builder) {
char[] password = builder.password;
byte[] base64Salt = builder.base64Salt;
int keySize = builder.keySize;
int iterations = builder.iterations;
this.builder = builder;
Assert.state(password != null && password.length > 0, "password cannot be null or empty.");
Assert.state(base64Salt != null && base64Salt.length > 0, "salt cannot be null or empty.");
Assert.state(keySize > 0, "the key size must be greater than zero.");
Assert.state(iterations > 0, "the number of iterations must be greater than zero.");
key = initKey(password, Base64.decodeBase64(base64Salt), keySize, iterations);
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private SecretKey initKey(char[] password, byte[] salt, int keySize, int iterations) {
try {
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(ALGORITHM);
KeySpec keySpec = new PBEKeySpec(password, salt, iterations, keySize);
SecretKey secretKey = secretKeyFactory.generateSecret(keySpec);
SecretKey secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
return secret;
} catch (Exception e) {
throw new IllegalStateException("Could not create the encryption key.", e);
}
}
@Override
public String decryptBase64String(String base64EncryptedValue) {
Assert.hasText(base64EncryptedValue);
Assert.isTrue(Base64.isBase64(base64EncryptedValue.getBytes()), "encryptedValue argument must be Base64.");
byte[] encryptedValue = Base64.decodeBase64(base64EncryptedValue);
return new String(decrypt(encryptedValue), UTF_8);
}
private byte[] decrypt(byte[] encryptedValue) {
try {
int ivSize = builder.keySize;
int ivByteSize = ivSize / BITS_PER_BYTE;
byte[] iv = new byte[ivByteSize];
System.arraycopy(encryptedValue, 0, iv, 0, ivByteSize);
byte[] rawEncryptedValue = new byte[encryptedValue.length - ivByteSize];
int encryptedSize = encryptedValue.length - ivByteSize;
System.arraycopy(encryptedValue, ivByteSize, rawEncryptedValue, 0, encryptedSize);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] plainTxtBytes = cipher.doFinal(rawEncryptedValue);
return plainTxtBytes;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static class Builder {
private char[] password;
private byte[] base64Salt;
private int keySize;
private int iterations;
public Builder setPassword(char[] password) {
this.password = password;
return this;
}
public Builder setBase64Salt(byte[] base64Salt) {
this.base64Salt = base64Salt;
return this;
}
public Builder setKeySize(int keySize) {
this.keySize = keySize;
return this;
}
public Builder setIterations(int iterations) {
this.iterations = iterations;
return this;
}
public EncryptionService build() {
return new ApiKeySecretEncryptionService(this);
}
}
}