LtpaKeyUtils.java
/*
* Copyright 2018 Ronny "Sephiroth" Perinke <[email protected]>
*
* 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
*
* https://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 de.sephirothj.spring.security.ltpa2;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.SecretKeySpec;
import lombok.experimental.UtilityClass;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
/**
* Utility class for working with encoded and/or encrpyted keys exported from IBM WebSphere Application Server and Liberty Profile
*
* @author Sephiroth
*/
@UtilityClass
public class LtpaKeyUtils
{
private static final String PASSWORD_MUST_NOT_BE_EMPTY = "password must not be empty";
private static final String KEY_ALGORITHM = "RSA";
/**
* the size of the shared secret key in byte
*/
private static final byte SHARED_KEY_SIZE = 16;
/**
* the length of the field with the public modulus
*/
private static final int PUBLIC_MODULUS_LENGTH = 129;
/**
* the length of the field with the public exponent
*/
private static final byte PUBLIC_EXPONENT_LENGTH = 3;
/**
* the length of the fields for the private factors Q and P
*/
private static final byte PRIVATE_P_Q_LENGTH = 65;
/**
* the length of the field that contains the length of the private exponent
*/
private static final byte PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH = 4;
/**
* decrypts something that was encrypted using the IBM-specific methods, such as the shared secret key and the private key
*
* @param encrypted
* @param password
* @return
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws InvalidKeySpecException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
*/
private byte[] decrypt(@NonNull final byte[] encrypted, @NonNull final String password) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidKeySpecException, IllegalBlockSizeException, BadPaddingException
{
Assert.notNull(encrypted, "encrypted must not be null");
Assert.hasText(password, PASSWORD_MUST_NOT_BE_EMPTY);
final MessageDigest md = MessageDigest.getInstance("SHA");
final byte[] pwdHash = Arrays.copyOfRange(md.digest(password.getBytes()), 0, 24);
final Cipher c = Cipher.getInstance("DESede/ECB/PKCS5Padding");
final Key decryptionKey = SecretKeyFactory.getInstance("DESede").generateSecret(new DESedeKeySpec(pwdHash));
c.init(Cipher.DECRYPT_MODE, decryptionKey);
return c.doFinal(encrypted);
}
/**
* decrypts the shared secret key ({@code com.ibm.websphere.ltpa.3DESKey}) that is used to encrypt a serialized LTPA2 token
*
* @param encryptedKey the base64-encoded and with 3DES encrypted key
* @param password the password for decryption (attribute {@code keysPassword} in your server configuration)
* @return the decrypted key
* @throws GeneralSecurityException if anything went wrong
*/
@NonNull
public SecretKey decryptSharedKey(@NonNull final String encryptedKey, @NonNull final String password) throws GeneralSecurityException
{
Assert.notNull(encryptedKey, "encryptedKey must not be null");
Assert.hasText(password, PASSWORD_MUST_NOT_BE_EMPTY);
try
{
final byte[] decodeFromString = Base64Utils.decodeFromString(encryptedKey);
final byte[] secret = decrypt(decodeFromString, password);
return new SecretKeySpec(secret, 0, SHARED_KEY_SIZE, "AES");
}
catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException ex)
{
throw new GeneralSecurityException("failed to decrypt shared key", ex);
}
}
/**
* decodes an base64-encoded public key {@code com.ibm.websphere.ltpa.PublicKey}
*
* @param encryptedPublicKey the base64-encoded public key which corresponds to the private key that is used to sign an LTPA2 token
* @return the decoded public key
* @throws GeneralSecurityException if anything went wrong
*/
@NonNull
public PublicKey decodePublicKey(@NonNull final String encryptedPublicKey) throws GeneralSecurityException
{
Assert.hasText(encryptedPublicKey, "encryptedPublicKey must not be empty");
try
{
final byte[] parts = Base64Utils.decodeFromString(encryptedPublicKey);
Assert.isTrue(parts.length == PUBLIC_MODULUS_LENGTH + PUBLIC_EXPONENT_LENGTH, "invalid encryptedPublicKey");
final BigInteger modulus = new BigInteger(Arrays.copyOfRange(parts, 0, PUBLIC_MODULUS_LENGTH));
final BigInteger exponent = new BigInteger(Arrays.copyOfRange(parts, PUBLIC_MODULUS_LENGTH, PUBLIC_MODULUS_LENGTH + PUBLIC_EXPONENT_LENGTH));
final RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(modulus, exponent);
final KeyFactory kf = KeyFactory.getInstance(KEY_ALGORITHM);
return kf.generatePublic(pubKeySpec);
}
catch (NoSuchAlgorithmException | InvalidKeySpecException | IllegalArgumentException ex)
{
throw new GeneralSecurityException("failed to decoded public key", ex);
}
}
/**
* decrypt the private key ({@code com.ibm.websphere.ltpa.PrivateKey}) that is used to sign an LTPA2 token
*
* @param encryptedKey the base64-encoded and with 3DES encrypted key
* @param password the password for decryption
* @return the decrypted key
* @throws GeneralSecurityException if anything went wrong
*/
@NonNull
public PrivateKey decryptPrivateKey(@NonNull final String encryptedKey, @NonNull final String password) throws GeneralSecurityException
{
Assert.hasText(encryptedKey, "encryptedKey must not be empty");
Assert.hasText(password, PASSWORD_MUST_NOT_BE_EMPTY);
try
{
final byte[] parts = decrypt(Base64Utils.decodeFromString(encryptedKey), password);
// read the length of the field with the private exponent
final int privateExponentLength = (new BigInteger(Arrays.copyOfRange(parts, 0, PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH))).intValue();
final BigInteger privateExponent = new BigInteger(Arrays.copyOfRange(parts, PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH, PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH + privateExponentLength));
final BigInteger p = new BigInteger(Arrays.copyOfRange(parts, PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH + privateExponentLength + PUBLIC_EXPONENT_LENGTH, PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH + privateExponentLength + PUBLIC_EXPONENT_LENGTH + PRIVATE_P_Q_LENGTH));
final BigInteger q = new BigInteger(Arrays.copyOfRange(parts, PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH + privateExponentLength + PUBLIC_EXPONENT_LENGTH + PRIVATE_P_Q_LENGTH, PRIVATE_EXPONENT_LENGTH_FIELD_LENGTH + privateExponentLength + PUBLIC_EXPONENT_LENGTH + PRIVATE_P_Q_LENGTH + PRIVATE_P_Q_LENGTH));
final BigInteger modulus = p.multiply(q);
final RSAPrivateKeySpec privKeySpec = new RSAPrivateKeySpec(modulus, privateExponent);
final KeyFactory kf = KeyFactory.getInstance(KEY_ALGORITHM);
return kf.generatePrivate(privKeySpec);
}
catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException | ArrayIndexOutOfBoundsException ex)
{
throw new GeneralSecurityException("failed to decrypt private key", ex);
}
}
}