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}