001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.utils;
019
020import io.stallion.exceptions.ClientException;
021import io.stallion.exceptions.DecryptionException;
022import io.stallion.services.Log;
023import org.apache.commons.codec.DecoderException;
024import org.apache.commons.codec.binary.Base32;
025import org.apache.commons.codec.binary.Base64;
026import org.apache.commons.codec.binary.Hex;
027import org.apache.commons.lang3.StringUtils;
028
029
030
031import org.springframework.security.crypto.keygen.KeyGenerators;
032
033
034import javax.crypto.Cipher;
035import javax.crypto.SecretKey;
036import javax.crypto.spec.GCMParameterSpec;
037import javax.crypto.spec.IvParameterSpec;
038import javax.crypto.spec.SecretKeySpec;
039import java.nio.charset.Charset;
040import java.security.NoSuchAlgorithmException;
041import java.security.spec.InvalidKeySpecException;
042import java.security.spec.KeySpec;
043import java.util.UUID;
044import java.security.SecureRandom;
045import javax.crypto.SecretKeyFactory;
046import javax.crypto.spec.PBEKeySpec;
047import javax.xml.bind.DatatypeConverter;
048import java.math.BigInteger;
049
050/**
051 * A simple class for encrypting and decrypting an arbitrary bit of text
052 *
053 * - Uses AES/GCM mode
054 * - Accepts a password, will generate a salt, derive a key using PBKDF2WithHmacSHA1, and append the salt to the output
055 * - returns the encrypted text in base32 format
056 * - Uses 128 big key length
057 *
058 * I would have greatly preferred just to use the Spring Crypto Library, but unfortunately that uses 256-bit keys,
059 * which means the Ulimited Crypto libraries need to be installed, which makes using Stallion out of the box more
060 * painful.
061 *
062 *
063 *
064 */
065public class Encrypter {
066    private static final int ITERATIONS = 1024;
067    private static final int KEY_LENGTH = 128; // bits
068
069    /*
070    public static String encryptString(String password, String salt, String value) {
071        StrongTextEncryptor textEncryptor = new StrongTextEncryptor();
072        textEncryptor.setPassword(password);
073        String myEncryptedText = textEncryptor.encrypt(value);
074        return myEncryptedText;
075        //return Encryptors.text(password, salt).encrypt(value);
076    }
077
078    public static String decryptString(String password, String salt, String encrypted) {
079        StrongTextEncryptor textEncryptor = new StrongTextEncryptor();
080        textEncryptor.setPassword(password);
081        String text = textEncryptor.decrypt(encrypted);
082        return text;
083    }
084   */
085
086    public static String encryptString(String password, String value) {
087        String salt = KeyGenerators.string().generateKey();
088        SecretKeySpec skeySpec = makeKeySpec(password, salt);
089        byte[] iv = KeyGenerators.secureRandom(16).generateKey();
090        String ivString = Hex.encodeHexString(iv);
091
092        try {
093            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
094            cipher.init(Cipher.ENCRYPT_MODE, skeySpec,
095                    new GCMParameterSpec(128, iv));
096                    /*
097            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
098            cipher.init(Cipher.ENCRYPT_MODE, skeySpec,
099                    new IvParameterSpec(iv));
100                    */
101
102            byte[] encrypted = cipher.doFinal(value.getBytes(Charset.forName("UTF-8")));
103            String s = StringUtils.strip(new Base32().encodeAsString(encrypted), "=").toLowerCase();
104            // Strip line breaks
105            s = salt + ivString + s.replaceAll("(\\n|\\r)", "");
106            return s;
107        } catch (Exception e) {
108            throw new RuntimeException(e);
109        }
110    }
111
112    public static String decryptString(String password, String encryptedBase32) {
113        try {
114            return doDecryptString(password, encryptedBase32);
115        } catch (Exception e) {
116            Log.exception(e, "Exception trying to decrypt token");
117            throw new DecryptionException("Error trying to decrypt the token.");
118        }
119    }
120
121    private static String doDecryptString(String password, String encryptedBase32) throws Exception {
122        encryptedBase32 = StringUtils.strip(encryptedBase32, "=");
123        String salt = encryptedBase32.substring(0, 16);
124        String ivString = encryptedBase32.substring(16, 48);
125        byte[] iv = new byte[0];
126        try {
127            iv = Hex.decodeHex(ivString.toCharArray());
128        } catch (DecoderException e) {
129            throw new RuntimeException(e);
130        }
131        encryptedBase32 = encryptedBase32.substring(48);
132        Base32 decoder = new Base32();
133        byte[] encrypted = decoder.decode(encryptedBase32.toUpperCase());
134        SecretKeySpec skeySpec = makeKeySpec(password, salt);
135
136
137
138            /*
139            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
140            cipher.init(Cipher.DECRYPT_MODE, skeySpec,
141                    new IvParameterSpec(iv));
142              */
143        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
144        cipher.init(Cipher.DECRYPT_MODE, skeySpec,
145                new GCMParameterSpec(128, iv));
146
147        byte[] original = cipher.doFinal(encrypted);
148        return new String(original, Charset.forName("UTF-8"));
149
150    }
151
152    private static SecretKeySpec makeKeySpec(String password, String salt) {
153        byte[] saltBytes = new byte[0];
154        try {
155            saltBytes = Hex.decodeHex(salt.toCharArray());
156        } catch (DecoderException e) {
157            throw new RuntimeException(e);
158        }
159        PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray(), saltBytes,
160                ITERATIONS, KEY_LENGTH);
161        SecretKey secretKey;
162        try {
163            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
164            secretKey = factory.generateSecret(keySpec);
165        }
166        catch (NoSuchAlgorithmException e) {
167            throw new IllegalArgumentException("Not a valid encryption algorithm", e);
168        }
169        catch (InvalidKeySpecException e) {
170            throw new IllegalArgumentException("Not a valid secret key", e);
171        }
172        SecretKeySpec skeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
173        return skeySpec;
174    }
175}