org.artifactory.security.crypto.CryptoHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.artifactory.security.crypto.CryptoHelper.java

Source

/*
 * Artifactory is a binaries repository manager.
 * Copyright (C) 2012 JFrog Ltd.
 *
 * Artifactory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Artifactory is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Artifactory.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.artifactory.security.crypto;

import com.thoughtworks.xstream.core.util.Base64Encoder;
import org.apache.commons.io.FileUtils;
import org.artifactory.common.ArtifactoryHome;
import org.artifactory.common.ConstantValues;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.security.*;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * Helper class for encrypting/decrypting passwords.
 *
 * @author Yossi Shaul
 */
public abstract class CryptoHelper {
    private static final Logger log = LoggerFactory.getLogger(CryptoHelper.class);

    static final String ASYM_ALGORITHM = "RSA";
    private static final String UTF8 = "UTF-8";

    static final String SYM_ALGORITHM = "PBEWithSHA1AndDESede";
    private static final byte[] PBE_SALT = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE,
            (byte) 0xEB, (byte) 0xAB, (byte) 0xEF, (byte) 0xAC };
    private static final int PBE_ITERATION_COUNT = 20;

    private CryptoHelper() {
        // utility class
    }

    public static String convertToString(Key key) {
        return convertToString(key.getEncoded(), true);
    }

    // Use whenever byte array needs to be converted to a readable string
    public static String convertToString(byte[] encrypted, boolean master) {
        if (!master && ConstantValues.securityUseBase64.getBoolean()) {
            // Master encryption supported only in base58
            return ArtifactoryBase64.convertToString(encrypted, false);
        } else {
            return ArtifactoryBase58.convertToString(encrypted, master);
        }
    }

    // Use whenever a string needs to be converted to the original byte array
    public static byte[] convertToBytes(String encrypted, boolean master) {
        byte[] bytes = ArtifactoryBase58.extractBytes(encrypted);
        if (!master && bytes == null) {
            bytes = ArtifactoryBase64.extractBytes(encrypted);
        }
        if (bytes == null) {
            throw new IllegalArgumentException("String " + encrypted + " was not encrypted by Artifactory!");
        }
        return bytes;
    }

    public static boolean isMasterEncrypted(String in) {
        if (in == null || in.length() == 0) {
            return false;
        }
        // Master encryption supported only in base58
        return ArtifactoryBase58.isMasterEncrypted(in);
    }

    public static boolean isPasswordEncrypted(String in) {
        if (in == null || in.length() == 0) {
            return false;
        }

        return ArtifactoryBase58.isPasswordEncrypted(in) || ArtifactoryBase64.isPasswordEncrypted(in);
    }

    public static String needsEscaping(String encrypted) {
        if (ConstantValues.securityUseBase64.getBoolean()
                && ArtifactoryBase64.isEncryptedPasswordPrefixedWithDefault(encrypted)) {
            return ArtifactoryBase64.escapeEncryptedPassword(encrypted);
        }
        return encrypted;
    }

    public static KeyPair generateKeyPair() {
        try {
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ASYM_ALGORITHM);
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            keyGen.initialize(512, random);
            return keyGen.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("No such algorithm:" + e.getMessage());
        }
    }

    static KeyPair createKeyPair(byte[] encodedPrivateKey, byte[] encodedPublicKey) {
        try {
            EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedPrivateKey);
            KeyFactory generator = KeyFactory.getInstance(ASYM_ALGORITHM);
            PrivateKey privateKey = generator.generatePrivate(privateKeySpec);

            EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encodedPublicKey);
            PublicKey publicKey = generator.generatePublic(publicKeySpec);
            return new KeyPair(publicKey, privateKey);
        } catch (Exception e) {
            throw new IllegalArgumentException("Failed to create KeyPair from provided encoded keys", e);
        }
    }

    public static KeyPair createKeyPair(String stringBasePrivateKey, String stringBasePublicKey, boolean master) {
        byte[] privateKeyEncoded = convertToBytes(stringBasePrivateKey, master);
        byte[] publicKeyEncoded = convertToBytes(stringBasePublicKey, master);
        return createKeyPair(privateKeyEncoded, publicKeyEncoded);
    }

    public static SecretKey generatePbeKeyFromKeyPair(String privateKey, String publicKey, boolean master) {
        KeyPair keyPair = createKeyPair(privateKey, publicKey, master);
        // Always use Base64 encoding (Historical and why not keep it reason)
        return generatePbeKeyFromKeyPair(keyPair);
    }

    public static SecretKey generatePbeKeyFromKeyPair(KeyPair keyPair) {
        return generatePbeKey(ArtifactoryBase64.toBase64(keyPair.getPrivate().getEncoded()));
    }

    private static SecretKey generatePbeKey(String password) {
        try {
            PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(SYM_ALGORITHM);
            SecretKey secretKey = keyFactory.generateSecret(pbeKeySpec);
            return secretKey;
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("No such algorithm: " + e.getMessage());
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("Unexpected exception: ", e);
        }
    }

    public static String encryptSymmetric(String plainText, SecretKey pbeKey, boolean master) {
        try {
            Cipher pbeCipher = Cipher.getInstance(SYM_ALGORITHM);
            PBEParameterSpec pbeParamSpec = new PBEParameterSpec(PBE_SALT, PBE_ITERATION_COUNT);
            pbeCipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
            byte[] encrypted = pbeCipher.doFinal(stringToBytes(plainText));
            return convertToString(encrypted, master);
        } catch (Exception e) {
            throw new UnsupportedOperationException(e);
        }
    }

    public static String generateUniqueApiKey() throws GeneralSecurityException {
        byte[] hmacData;

        try {
            SecretKeySpec secretKey = new SecretKeySpec("secretKey".getBytes("UTF-8"), "HmacSHA256");
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(secretKey);
            String data = UUID.randomUUID().toString();
            hmacData = mac.doFinal(data.getBytes("UTF-8"));
            return new Base64Encoder().encode(hmacData);
        } catch (UnsupportedEncodingException e) {
            throw new GeneralSecurityException(e);
        }
    }

    public static String decryptSymmetric(String encrypted, SecretKey pbeKey, boolean master) {
        try {
            byte[] bytes = convertToBytes(encrypted, master);
            Cipher pbeCipher = Cipher.getInstance(SYM_ALGORITHM);
            PBEParameterSpec pbeParamSpec = new PBEParameterSpec(PBE_SALT, PBE_ITERATION_COUNT);
            pbeCipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);
            byte[] decryptedBytes = pbeCipher.doFinal(bytes);
            return bytesToString(decryptedBytes);
        } catch (Exception e) {
            throw new UnsupportedOperationException(e);
        }
    }

    static String bytesToString(byte[] bytes) {
        try {
            return new String(bytes, UTF8);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Unexpected exception", e);
        }
    }

    static byte[] stringToBytes(String string) {
        try {
            return (string.getBytes(UTF8));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Unexpected exception", e);
        }
    }

    public static String encryptIfNeeded(String password) {
        // If password null => no encryption
        if (password == null || password.length() == 0) {
            return password;
        }
        // If already encrypted => no encrypt again
        if (isMasterEncrypted(password)) {
            return password;
        }

        File keyFile = getMasterKeyFile();
        if (!keyFile.exists()) {
            return password;
        }
        SecretKey secretKey = getSecretKeyFromFile(keyFile);
        return encryptSymmetric(password, secretKey, true);
    }

    public static String decryptIfNeeded(String password) {
        if (isMasterEncrypted(password)) {
            File keyFile = getMasterKeyFile();
            if (!keyFile.exists()) {
                throw new IllegalArgumentException("The Password is encrypted.\n"
                        + "And no Master Key file found at " + keyFile.getAbsolutePath());
            }
            SecretKey secretKey = getSecretKeyFromFile(keyFile);
            password = decryptSymmetric(password, secretKey, true);
        }
        return password;
    }

    /**
     * Renames the master key file, effectively disabling encryption.
     */
    public static void removeMasterKeyFile() {
        File keyFile = getMasterKeyFile();
        if (!keyFile.exists()) {
            throw new RuntimeException(
                    "Cannot remove master key file if it does not exists at " + keyFile.getAbsolutePath());
        }
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmsssSSS");
        File renamed = new File(
                keyFile + "." + new Random().nextInt((10000 - 1) + 1) + "." + format.format(new Date()));

        try {
            FileUtils.moveFile(keyFile, renamed);
        } catch (IOException e) {
            throw new RuntimeException("Could not rename master key file at " + keyFile.getAbsolutePath() + " to "
                    + renamed.getAbsolutePath(), e);
        }
    }

    /**
     * Creates a master encryption key file. Throws an exception if the key file already exists of on any failure with
     * file or key creation.
     */
    public static void createMasterKeyFile() {
        File keyFile = getMasterKeyFile();
        if (keyFile.exists()) {
            throw new IllegalStateException(
                    "Cannot create new master key file if it already exists at " + keyFile.getAbsolutePath());
        }
        log.info("Creating master encryption key at {}", keyFile.getAbsolutePath());
        KeyPair keyPair = generateKeyPair();
        try {
            File securityFolder = keyFile.getParentFile();
            if (!securityFolder.exists()) {
                if (!securityFolder.mkdirs()) {
                    throw new RuntimeException("Could not create the folder containing the key file "
                            + securityFolder.getAbsolutePath());
                }
                setPermissionsOnSecurityFolder(securityFolder);
            }

            checkPermissionsOnSecurityFolder(securityFolder);
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(keyFile))) {
                writer.write(convertToString(keyPair.getPrivate().getEncoded(), true));
                writer.newLine();
                writer.write(convertToString(keyPair.getPublic().getEncoded(), true));
                writer.newLine();
            }
        } catch (IOException e) {
            throw new RuntimeException("Could not write the key into " + keyFile.getAbsolutePath(), e);
        }
    }

    /**
     * @return Master encryption file configured location. The file might not exist.
     */
    public static File getMasterKeyFile() {
        ArtifactoryHome home = ArtifactoryHome.get();
        String keyFileLocation = ConstantValues.securityMasterKeyLocation.getString();
        File keyFile = new File(keyFileLocation);
        if (!keyFile.isAbsolute()) {
            keyFile = new File(home.getHaAwareEtcDir(), keyFileLocation);
        }
        return keyFile;
    }

    public static void checkPermissionsOnSecurityFolder(File securityFolder) throws IOException {
        if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
            Set<PosixFilePermission> filePermissions = Files.getPosixFilePermissions(securityFolder.toPath());
            if (filePermissions.contains(PosixFilePermission.GROUP_READ)
                    || filePermissions.contains(PosixFilePermission.OTHERS_READ)) {
                throw new RuntimeException("The folder containing the key file " + securityFolder.getAbsolutePath()
                        + " has too broad permissions!\n" + "Please limit access to the Artifactory user only!");
            }
        }
    }

    public static void setPermissionsOnSecurityFolder(File securityFolder) throws IOException {
        // The security folder should accessible only by the owner
        if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
            Files.setPosixFilePermissions(securityFolder.toPath(), EnumSet.of(PosixFilePermission.OWNER_EXECUTE,
                    PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_READ));
        }
    }

    private static SecretKey getSecretKeyFromFile(File keyFile) {
        try (BufferedReader reader = new BufferedReader(new FileReader(keyFile))) {
            String privateKey = reader.readLine();
            String publicKey = reader.readLine();
            return generatePbeKeyFromKeyPair(privateKey, publicKey, true);
        } catch (IOException e) {
            throw new RuntimeException(
                    "Could not read master key " + keyFile.getAbsolutePath() + " to decrypt password!", e);
        }
    }

    public static boolean hasMasterKey() {
        return getMasterKeyFile().exists();
    }

    public static String getKeyFilePath() {
        return getMasterKeyFile().getPath();
    }
}