001/**
002 * Copyright 2010-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.common.util.encrypt.openssl;
017
018import static java.util.Arrays.copyOfRange;
019import static javax.crypto.Cipher.DECRYPT_MODE;
020import static javax.crypto.Cipher.ENCRYPT_MODE;
021import static org.apache.commons.io.FileUtils.openInputStream;
022import static org.apache.commons.io.FileUtils.openOutputStream;
023import static org.apache.commons.io.IOUtils.closeQuietly;
024import static org.apache.commons.io.IOUtils.toByteArray;
025import static org.codehaus.plexus.util.Base64.decodeBase64;
026import static org.codehaus.plexus.util.Base64.encodeBase64;
027import static org.kuali.common.util.Str.getAsciiBytes;
028import static org.kuali.common.util.Str.getAsciiString;
029import static org.kuali.common.util.Str.getUTF8Bytes;
030import static org.kuali.common.util.Str.getUTF8String;
031import static org.kuali.common.util.base.Exceptions.illegalState;
032import static org.kuali.common.util.base.Precondition.checkNotNull;
033import static org.kuali.common.util.encrypt.openssl.OpenSSL.buildEncryptedContext;
034import static org.kuali.common.util.encrypt.openssl.OpenSSL.checkBase64;
035import static org.kuali.common.util.encrypt.openssl.OpenSSL.combineByteArrays;
036import static org.kuali.common.util.encrypt.openssl.OpenSSL.createSalt;
037import static org.kuali.common.util.encrypt.openssl.OpenSSL.toByteArray;
038import static org.kuali.common.util.encrypt.openssl.OpenSSLContext.buildDefaultOpenSSLContext;
039
040import java.io.File;
041import java.io.IOException;
042import java.io.InputStream;
043import java.io.OutputStream;
044
045import javax.crypto.Cipher;
046import javax.crypto.spec.IvParameterSpec;
047import javax.crypto.spec.SecretKeySpec;
048
049import org.kuali.common.util.encrypt.Encryptor;
050
051/**
052 * 
053 * Encrypt/decrypt using the same techniques as OpenSSL. This enables java code to work with data encrypted by OpenSSL (and vice versa)
054 * 
055 * For example the following commands encrypt/decrypt the text "foo" using the password "bar" via OpenSSL:
056 * 
057 * <pre>
058 * echo -n "foo" | openssl enc -aes128 -e -base64 -A -k "bar"
059 * echo -n "U2FsdGVkX1+VsRny+UbwuLllbAQ5yK/3MenTFJEKRVE=" | openssl enc -aes128 -d -base64 -A -k "bar"
060 * </pre>
061 */
062public final class OpenSSLEncryptor implements Encryptor {
063
064        // Immutable and safe to expose via a getter
065        private final OpenSSLContext context;
066
067        // Internal only. Do not expose via getters
068        private final byte[] password;
069        private final byte[] prefix;
070
071        public OpenSSLEncryptor(String password) {
072                this(buildDefaultOpenSSLContext(), password);
073        }
074
075        public OpenSSLEncryptor(OpenSSLContext context, String password) {
076                this.context = checkNotNull(context, "context");
077                this.password = getUTF8Bytes(checkNotNull(password, "password"));
078                this.prefix = getUTF8Bytes(context.getSaltPrefix());
079        }
080
081        public void encrypt(File src, File dst) {
082                checkNotNull(src, "src");
083                checkNotNull(dst, "dst");
084                InputStream in = null;
085                OutputStream out = null;
086                try {
087                        in = openInputStream(src);
088                        out = openOutputStream(dst);
089                        encrypt(in, out);
090                } catch (IOException e) {
091                        throw illegalState(e);
092                } finally {
093                        closeQuietly(in);
094                        closeQuietly(out);
095                }
096        }
097
098        public void encrypt(InputStream in, OutputStream out) throws IOException {
099                byte[] bytes = toByteArray(in);
100                byte[] encrypted = encrypt(bytes);
101                byte[] base64 = encodeBase64(encrypted, true);
102                out.write(base64);
103        }
104
105        public byte[] encrypt(byte[] bytes) {
106                // Null not allowed
107                checkNotNull(bytes, "bytes");
108
109                // Generate a random salt
110                byte[] salt = createSalt(context.getSaltSize());
111
112                // encrypt the bytes using the salt
113                byte[] encrypted = doCipher(context, ENCRYPT_MODE, salt, bytes, password);
114
115                // Combine the prefix, salt, and encrypted bytes into one array
116                return combineByteArrays(prefix, salt, encrypted);
117        }
118
119        @Override
120        public String encrypt(String text) {
121                // Null not allowed
122                checkNotNull(text, "text");
123
124                // Convert the text into bytes
125                byte[] bytes = getUTF8Bytes(text);
126
127                // Encrypt the bytes
128                byte[] encrypted = encrypt(bytes);
129
130                // Encode as base 64
131                byte[] base64 = encodeBase64(encrypted);
132
133                // Convert the base64 bytes into a string
134                return getAsciiString(base64);
135        }
136
137        @Override
138        public String decrypt(String text) {
139                // Null not allowed
140                checkNotNull(text, "text");
141
142                // Decode the base64 text into bytes
143                byte[] bytes = decodeBase64(getAsciiBytes(checkBase64(text)));
144
145                // OpenSSL inserts the prefix "Salted__" followed by the salt itself
146                int saltOffset = context.getSaltPrefix().length();
147                byte[] salt = copyOfRange(bytes, saltOffset, saltOffset + context.getSaltSize());
148
149                // encrypted bytes come after the prefix and the salt
150                int encryptedBytesOffset = saltOffset + context.getSaltSize();
151
152                // extract the portion of the array containing the encrypted bytes
153                byte[] encrypted = copyOfRange(bytes, encryptedBytesOffset, bytes.length);
154
155                // decrypt the bytes using the salt that was embedded in the text
156                byte[] decrypted = doCipher(context, DECRYPT_MODE, salt, encrypted, password);
157
158                // Construct a string from the decrypted bytes
159                return getUTF8String(decrypted);
160        }
161
162        protected static byte[] doCipher(OpenSSLContext context, int mode, byte[] salt, byte[] bytes, byte[] password) {
163                try {
164                        // specify cipher and digest
165                        Cipher cipher = Cipher.getInstance(context.getTransformation());
166
167                        // the IV length is driven by the cipher block size
168                        int initVectorLength = cipher.getBlockSize();
169
170                        // Calculate the iv + key bytes using the exact same technique OpenSSL does
171                        OpenSSLEncryptedContext oec = buildEncryptedContext(context, initVectorLength, salt, password);
172
173                        // Create java objects from the raw bytes
174                        SecretKeySpec key = new SecretKeySpec(toByteArray(oec.getKey()), context.getAlgorithm());
175                        IvParameterSpec iv = new IvParameterSpec(toByteArray(oec.getInitVector()));
176
177                        // initialize the cipher instance
178                        cipher.init(mode, key, iv);
179
180                        // Return the encrypted/decrypted bytes
181                        return cipher.doFinal(bytes);
182                } catch (Exception e) {
183                        throw illegalState(e);
184                }
185        }
186
187        public OpenSSLContext getContext() {
188                return context;
189        }
190
191}