/***** BEGIN LICENSE BLOCK ***** * Version: EPL 1.0/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Eclipse Public * License Version 1.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.eclipse.org/legal/epl-v10.html * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * Copyright (C) 2006, 2007 Ola Bini <ola@ologix.com> * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the EPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the EPL, the GPL or the LGPL. ***** END LICENSE BLOCK *****/ package org.jruby.ext.openssl; import java.io.PrintStream; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.spec.AlgorithmParameterSpec; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static javax.crypto.Cipher.DECRYPT_MODE; import static javax.crypto.Cipher.ENCRYPT_MODE; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.RC2ParameterSpec; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyBoolean; import org.jruby.RubyClass; import org.jruby.RubyInteger; import org.jruby.RubyModule; import org.jruby.RubyNumeric; import org.jruby.RubyObject; import org.jruby.RubyString; import org.jruby.common.IRubyWarnings.ID; import org.jruby.anno.JRubyMethod; import org.jruby.exceptions.RaiseException; import org.jruby.runtime.Arity; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.runtime.Visibility; import org.jruby.util.ByteList; import static org.jruby.ext.openssl.OpenSSL.*; /** * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a> */ public class Cipher extends RubyObject { private static final long serialVersionUID = -5390983669951165103L; private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { public Cipher allocate(Ruby runtime, RubyClass klass) { return new Cipher(runtime, klass); } }; public static void createCipher(final Ruby runtime, final RubyModule OpenSSL) { final RubyClass Cipher = OpenSSL.defineClassUnder("Cipher", runtime.getObject(), ALLOCATOR); Cipher.defineAnnotatedMethods(Cipher.class); final RubyClass OpenSSLError = OpenSSL.getClass("OpenSSLError"); Cipher.defineClassUnder("CipherError", OpenSSLError, OpenSSLError.getAllocator()); String cipherName; cipherName = "AES"; // OpenSSL::Cipher::AES Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); cipherName = "CAST5"; // OpenSSL::Cipher::CAST5 Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); cipherName = "BF"; // OpenSSL::Cipher::BF Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); cipherName = "DES"; // OpenSSL::Cipher::DES Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); cipherName = "IDEA"; // OpenSSL::Cipher::IDEA Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); cipherName = "RC2"; // OpenSSL::Cipher::RC2 Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); cipherName = "RC4"; // OpenSSL::Cipher::RC4 Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); cipherName = "RC5"; // OpenSSL::Cipher::RC5 Cipher.defineClassUnder(cipherName, Cipher, new NamedCipherAllocator(cipherName)) .defineAnnotatedMethods(Named.class); String keyLength; keyLength = "128"; // OpenSSL::Cipher::AES128 Cipher.defineClassUnder("AES" + keyLength, Cipher, new AESCipherAllocator(keyLength)) .defineAnnotatedMethods(AES.class); keyLength = "192"; // OpenSSL::Cipher::AES192 Cipher.defineClassUnder("AES" + keyLength, Cipher, new AESCipherAllocator(keyLength)) .defineAnnotatedMethods(AES.class); keyLength = "256"; // OpenSSL::Cipher::AES256 Cipher.defineClassUnder("AES" + keyLength, Cipher, new AESCipherAllocator(keyLength)) .defineAnnotatedMethods(AES.class); } static RubyClass _Cipher(final Ruby runtime) { return (RubyClass) runtime.getModule("OpenSSL").getConstantAt("Cipher"); } @JRubyMethod(meta = true) public static IRubyObject ciphers(final ThreadContext context, final IRubyObject self) { final Ruby runtime = context.runtime; final Collection<String> ciphers = Algorithm.allSupportedCiphers().keySet(); final RubyArray result = runtime.newArray( ciphers.size() * 2 ); for ( final String cipher : ciphers ) { result.append( runtime.newString(cipher) ); // upper-case } for ( final String cipher : ciphers ) { // than lower-case OpenSSL compatibility result.append( runtime.newString(cipher.toLowerCase()) ); } return result; } public static boolean isSupportedCipher(final String name) { final String osslName = name.toUpperCase(); // if ( Algorithm.allSupportedCiphers().get( osslName ) != null ) { return true; } // final Algorithm alg = Algorithm.osslToJava(osslName); if ( isDebug() ) debug("isSupportedCipher( "+ name +" ) try new cipher = " + alg.getRealName()); try { return getCipherInstance(alg.getRealName(), true) != null; } catch (GeneralSecurityException e) { return false; } } private static String[] cipherModes(final String alg) { final String[] modes = providerCipherModes(alg); return modes == null ? registeredCipherModes(alg) : modes; } private static String[] providerCipherModes(final String alg) { final Provider.Service service = providerCipherService(alg); if ( service != null ) { final String supportedModes = service.getAttribute("SupportedModes"); if ( supportedModes == null ) return Algorithm.OPENSSL_BLOCK_MODES; return StringHelper.split(supportedModes, '|'); } return null; } private static Provider.Service providerCipherService(final String alg) { if ( SecurityHelper.securityProvider != null ) { return SecurityHelper.securityProvider.getService("Cipher", alg); } return null; } private static String[] registeredCipherModes(final String alg) { // e.g. "AES" /* if ( SecurityHelper.securityProvider != null && ! SecurityHelper.isProviderRegistered() ) { final Provider provider = SecurityHelper.securityProvider; for ( Provider.Service service : provider.getServices() ) { if ( "Cipher".equals( service.getType() ) ) { services.add(service); } } } */ boolean serviceFound = false; final Provider[] providers = java.security.Security.getProviders(); for ( int i = 0; i < providers.length; i++ ) { final Provider provider = providers[i]; final String name = provider.getName() == null ? "" : provider.getName(); // skip those that are known to provide no Cipher engines : if ( name.contains("JGSS") ) continue; // SunJGSS if ( name.contains("SASL") ) continue; // SunSASL if ( name.contains("XMLD") ) continue; // XMLDSig if ( name.contains("PCSC") ) continue; // SunPCSC if ( name.contains("JSSE") ) continue; // SunJSSE final Provider.Service service = provider.getService("Cipher", alg); if ( service != null ) { serviceFound = true; final String supportedModes = service.getAttribute("SupportedModes"); if ( supportedModes != null ) return StringHelper.split(supportedModes, '|'); } } // if a service is found but has no SupportedModes configuration included // e.g. BC provider does not provide those - assume all modes that OpenSSL // supports (and are mappable to JSE) work with the cipher algorithm : return serviceFound ? Algorithm.OPENSSL_BLOCK_MODES : null; } public static final class Algorithm { final String base; // e.g. DES, AES final String version; // e.g. EDE3, 256 final String mode; // CBC (default) private String padding; // PKCS5Padding (default) private String realName; private boolean realNameNeedsPadding; private int keyLength = -1, ivLength = -1; Algorithm(String cryptoBase, String cryptoVersion, String cryptoMode) { this.base = cryptoBase; this.version = cryptoVersion; this.mode = cryptoMode; //this.padding = padding; } private static final Set<String> KNOWN_BLOCK_MODES; // NOTE: CFB1 does not work as (OpenSSL) expects with BC (@see GH-35) private static final String[] OPENSSL_BLOCK_MODES = { "CBC", "CFB", /* "CFB1", */ "CFB8", "ECB", "OFB" // that Java supports }; static { KNOWN_BLOCK_MODES = new HashSet<String>(); for ( String mode : OPENSSL_BLOCK_MODES ) KNOWN_BLOCK_MODES.add(mode); KNOWN_BLOCK_MODES.add("CTR"); KNOWN_BLOCK_MODES.add("CTS"); // not supported by OpenSSL KNOWN_BLOCK_MODES.add("PCBC"); // not supported by OpenSSL KNOWN_BLOCK_MODES.add("NONE"); // valid to pass into JCE } // Ruby to Java name String (or FALSE) static final HashMap<String, String[]> supportedCiphers = new LinkedHashMap<String, String[]>(120, 1); // we're _marking_ unsupported keys with a Boolean.FALSE mapping // all cipher mappings are resolved on `OpenSSL::Cipher.ciphers` // ... probably a slightly _rare_ case to be going for up front static boolean supportedCiphersAll; static Map<String, String[]> allSupportedCiphers() { if ( supportedCiphersAll ) return supportedCiphers; synchronized ( supportedCiphers ) { if ( supportedCiphersAll ) return supportedCiphers; // cleanup all FALSE keys : //for ( String key: supportedCiphers.keySet() ) { //if ( supportedCiphers.get(key) == Boolean.FALSE ) supportedCiphers.remove(key); //} // OpenSSL: all the block ciphers normally use PKCS#5 padding String[] modes; modes = cipherModes("AES"); // null if not supported if ( modes != null ) { for ( final String mode : modes ) { final String realName = "AES/" + mode; // + "/PKCS5Padding" supportedCiphers.put( "AES-128-" + mode, new String[] { "AES", mode, "128", realName } ); supportedCiphers.put( "AES-192-" + mode, new String[] { "AES", mode, "192", realName } ); supportedCiphers.put( "AES-256-" + mode, new String[] { "AES", mode, "256", realName } ); } final String realName = "AES/CBC"; supportedCiphers.put( "AES128", new String[] { "AES", "CBC", "128", realName } ); supportedCiphers.put( "AES192", new String[] { "AES", "CBC", "192", realName } ); supportedCiphers.put( "AES256", new String[] { "AES", "CBC", "256", realName } ); } modes = cipherModes("Blowfish"); if ( modes != null ) { supportedCiphers.put( "BF", new String[] { "BF", "CBC", null, "Blowfish/CBC" }); for ( final String mode : modes ) { supportedCiphers.put( "BF-" + mode, new String[] { "BF", mode, null, "Blowfish/" + mode } ); } } modes = cipherModes("Camellia"); if ( modes != null ) { for ( final String mode : modes ) { final String realName = "Camellia/" + mode; supportedCiphers.put( "CAMELLIA-128-" + mode, new String[] { "CAMELLIA", mode, "128", realName } ); supportedCiphers.put( "CAMELLIA-192-" + mode, new String[] { "CAMELLIA", mode, "192", realName } ); supportedCiphers.put( "CAMELLIA-256-" + mode, new String[] { "CAMELLIA", mode, "256", realName } ); } final String realName = "Camellia/CBC"; supportedCiphers.put( "CAMELLIA128", new String[] { "CAMELLIA", "CBC", "128", realName } ); supportedCiphers.put( "CAMELLIA192", new String[] { "CAMELLIA", "CBC", "192", realName } ); supportedCiphers.put( "CAMELLIA256", new String[] { "CAMELLIA", "CBC", "256", realName } ); } modes = cipherModes("CAST5"); if ( modes != null ) { supportedCiphers.put( "CAST", new String[] { "CAST", "CBC", null, "CAST5/CBC" } ); supportedCiphers.put( "CAST-CBC", supportedCiphers.get("CAST") ); for ( final String mode : modes ) { supportedCiphers.put( "CAST5-" + mode, new String[] { "CAST", mode, null, "CAST5/" + mode }); } } modes = cipherModes("CAST6"); if ( modes != null ) { for ( final String mode : modes ) { supportedCiphers.put( "CAST6-" + mode, new String[] { "CAST6", mode, null, "CAST6/" + mode }); } } modes = cipherModes("DES"); if ( modes != null ) { supportedCiphers.put( "DES", new String[] { "DES", "CBC", null, "DES/CBC" } ); for ( final String mode : modes ) { supportedCiphers.put( "DES-" + mode, new String[] { "DES", mode, null, "DES/" + mode }); } } modes = cipherModes("DESede"); if ( modes != null ) { supportedCiphers.put( "DES-EDE", new String[] { "DES", "ECB", "EDE", "DESede/ECB" } ); supportedCiphers.put( "DES-EDE-CBC", new String[] { "DES", "CBC", "EDE", "DESede/CBC" } ); supportedCiphers.put( "DES-EDE-CFB", new String[] { "DES", "CBC", "EDE", "DESede/CFB" } ); supportedCiphers.put( "DES-EDE-OFB", new String[] { "DES", "CBC", "EDE", "DESede/OFB" } ); supportedCiphers.put( "DES-EDE3", new String[] { "DES", "ECB", "EDE3", "DESede/ECB" }); for ( final String mode : modes ) { supportedCiphers.put( "DES-EDE3-" + mode, new String[] { "DES", mode, "EDE3", "DESede/" + mode }); } supportedCiphers.put( "DES3", new String[] { "DES", "CBC", "EDE3", "DESede/CBC" } ); } modes = cipherModes("RC2"); if ( modes != null ) { supportedCiphers.put( "RC2", new String[] { "RC2", "CBC", null, "RC2/CBC" } ); for ( final String mode : modes ) { supportedCiphers.put( "RC2-" + mode, new String[] { "RC2", mode, null, "RC2/" + mode } ); } supportedCiphers.put( "RC2-40-CBC", new String[] { "RC2", "CBC", "40", "RC2/CBC" } ); supportedCiphers.put( "RC2-64-CBC", new String[] { "RC2", "CBC", "64", "RC2/CBC" } ); } modes = cipherModes("RC4"); // NOTE: stream cipher (BC supported) if ( modes != null ) { supportedCiphers.put( "RC4", new String[] { "RC4", null, null, "RC4" } ); supportedCiphers.put( "RC4-40", new String[] { "RC4", null, "40", "RC4" } ); //supportedCiphers.put( "RC2-HMAC-MD5", new String[] { "RC4", null, null, "RC4" }); } modes = cipherModes("SEED"); if ( modes != null ) { supportedCiphers.put( "SEED", new String[] { "SEED", "CBC", null, "SEED/CBC" } ); for ( final String mode : modes ) { supportedCiphers.put( "SEED-" + mode, new String[] { "SEED", mode, null, "SEED/" + mode }); } } supportedCiphersAll = true; return supportedCiphers; } } @Deprecated public static String jsseToOssl(final String cipherName, final int keyLength) { return javaToOssl(cipherName, keyLength); } public static String javaToOssl(final String cipherName, final int keyLength) { String cryptoBase; String cryptoVersion = null; String cryptoMode = null; final List parts = StringHelper.split((CharSequence) cipherName, '/'); final int partsLength = parts.size(); if ( partsLength != 1 && partsLength != 3 ) return null; if ( partsLength > 2 ) { cryptoMode = (String) parts.get(1); // padding: parts[2] not used } cryptoBase = (String) parts.get(0); if ( ! KNOWN_BLOCK_MODES.contains(cryptoMode) ) { cryptoVersion = cryptoMode; cryptoMode = "CBC"; } if (cryptoMode == null) { cryptoMode = "CBC"; } if ( "DESede".equals(cryptoBase) ) { cryptoBase = "DES"; cryptoVersion = "EDE3"; } else if ( "Blowfish".equals(cryptoBase) ) { cryptoBase = "BF"; } if (cryptoVersion == null) { cryptoVersion = String.valueOf(keyLength); } return cryptoBase + '-' + cryptoVersion + '-' + cryptoMode; } static Algorithm osslToJava(final String osslName) { return osslToJava(osslName, null); // assume PKCS5Padding } private static Algorithm osslToJava(final String osslName, final String padding) { final String[] algVals = supportedCiphers.get(osslName); if ( algVals != null ) { final String cryptoMode = algVals[1]; Algorithm alg = new Algorithm(algVals[0], algVals[2], cryptoMode); alg.realName = algVals[3]; alg.realNameNeedsPadding = true; alg.padding = getPaddingType(padding, cryptoMode); return alg; } String cryptoBase, cryptoVersion = null, cryptoMode, realName; String paddingType = null; // EXPERIMENTAL: if there's '/' assume it's a "real" JCE name : if ( osslName.indexOf('/') != -1 ) { // e.g. "DESedeWrap/CBC/NOPADDING" final List names = StringHelper.split((CharSequence) osslName, '/'); cryptoBase = (String) names.get(0); cryptoMode = names.size() > 1 ? (String) names.get(1) : "CBC"; paddingType = getPaddingType(padding, cryptoMode); if ( names.size() > 2 ) paddingType = (String) names.get(2); Algorithm alg = new Algorithm(cryptoBase, null, cryptoMode); alg.realName = osslName; alg.padding = paddingType; return alg; } int s = osslName.indexOf('-'); int i = 0; if (s == -1) { cryptoBase = osslName; cryptoMode = null; } else { cryptoBase = osslName.substring(i, s); s = osslName.indexOf('-', i = s + 1); if (s == -1) cryptoMode = osslName.substring(i); // "base-mode" else { // two separators : "base-version-mode" cryptoVersion = osslName.substring(i, s); s = osslName.indexOf('-', i = s + 1); if (s == -1) { cryptoMode = osslName.substring(i); } else { cryptoMode = osslName.substring(i, s); } } } cryptoBase = cryptoBase.toUpperCase(); // allways upper e.g. "AES" if ( cryptoMode != null ) cryptoMode = cryptoMode.toUpperCase(); boolean realNameSet = false; boolean setDefaultCryptoMode = true; if ( "BF".equals(cryptoBase) ) realName = "Blowfish"; else if ( "CAST".equals(cryptoBase) ) realName = "CAST5"; else if ( cryptoBase.startsWith("DES") ) { if ( "DES3".equals(cryptoBase) ) { cryptoBase = "DES"; realName = "DESede"; cryptoVersion = "EDE3"; // cryptoMode = "CBC"; } else if ( "EDE3".equalsIgnoreCase(cryptoVersion) || "EDE".equalsIgnoreCase(cryptoVersion) ) { realName = "DESede"; if ( cryptoMode == null ) cryptoMode = "ECB"; } else if ( "EDE3".equalsIgnoreCase(cryptoMode) || "EDE".equalsIgnoreCase(cryptoMode) ) { realName = "DESede"; cryptoVersion = cryptoMode; cryptoMode = "ECB"; } else realName = "DES"; } else if ( cryptoBase.length() > 3 && cryptoBase.startsWith("AES") ) { try { // try parsing e.g. "AES256" final String version = cryptoBase.substring(3); Integer.parseInt( version ); realName = cryptoBase = "AES"; cryptoVersion = version; } catch (NumberFormatException e) { realName = cryptoBase; } } else if ( cryptoBase.length() > 8 && cryptoBase.startsWith("CAMELLIA") ) { try { // try parsing e.g. "CAMELLIA192" final String version = cryptoBase.substring(8); Integer.parseInt( version ); realName = cryptoBase = "CAMELLIA"; cryptoVersion = version; } catch (NumberFormatException e) { realName = cryptoBase; } } else { realName = cryptoBase; // streaming ciphers - no padding/mode to be used ... // e.g. only SecurityHelper.getCipherInstance("RC4") will work if ( "RC4".equals(cryptoBase) ) { if ( ! KNOWN_BLOCK_MODES.contains(cryptoMode) ) { cryptoVersion = cryptoMode; } cryptoMode = null; // padding = null; setDefaultCryptoMode = false; // cryptoMode = "NONE"; paddingType = "NoPadding"; realNameSet = true; } } if ( cryptoMode == null && setDefaultCryptoMode ) cryptoMode = "CBC"; if ( paddingType == null ) paddingType = getPaddingType(padding, cryptoMode); if ( cryptoMode != null ) { //if ( ! KNOWN_BLOCK_MODES.contains(cryptoMode) ) { // if ( ! "XTS".equals(cryptoMode) ) { // // valid but likely not supported by JCE/provider // //cryptoVersion = cryptoMode; cryptoMode = "CBC"; // } //} if ( ! realNameSet ) { realName = realName + '/' + cryptoMode + '/' + paddingType; } } else if ( ! realNameSet ) { if ( padding != null ) { realName = realName + '/' + "NONE" + '/' + paddingType; } //else paddingType = null; // else realName is cryptoBase } Algorithm alg = new Algorithm(cryptoBase, cryptoVersion, cryptoMode); alg.realName = realName; alg.padding = paddingType; return alg; } String getPadding() { if ( mode == null ) return null; // if ( "RC4".equals(base) ) return null; return padding; } private static String getPaddingType(final String padding, final String cryptoMode) { //if ( "ECB".equals(cryptoMode) ) return "NoPadding"; // TODO check cryptoMode CFB/OFB final String defaultPadding = "PKCS5Padding"; if ( padding == null ) { if ( "GCM".equalsIgnoreCase(cryptoMode) ) { return "NoPadding"; } return defaultPadding; } if ( padding.equalsIgnoreCase("PKCS5Padding") ) { return "PKCS5Padding"; } if ( padding.equals("0") || padding.equalsIgnoreCase("NoPadding") ) { return "NoPadding"; } if ( padding.equalsIgnoreCase("ISO10126Padding") ) { return "ISO10126Padding"; } if ( padding.equalsIgnoreCase("PKCS1Padding") ) { return "PKCS1Padding"; } if ( padding.equalsIgnoreCase("SSL3Padding") ) { return "SSL3Padding"; } if (padding.equalsIgnoreCase("OAEPPadding")) { return "OAEPPadding"; } return defaultPadding; // "PKCS5Padding"; // default } String getRealName() { if ( realName != null ) { if ( realNameNeedsPadding ) { final String padding = getPadding(); if ( padding != null ) { realName = realName + '/' + padding; } realNameNeedsPadding = false; } return realName; } return realName = base + '/' + (mode == null ? "NONE" : mode) + '/' + padding; } public static String getAlgorithmBase(javax.crypto.Cipher cipher) { final String algorithm = cipher.getAlgorithm(); final int idx = algorithm.indexOf('/'); if ( idx != -1 ) return algorithm.substring(0, idx); return algorithm; } public static String getRealName(final String osslName) { return osslToJava(osslName).getRealName(); } public int getIvLength() { if ( ivLength != -1 ) return ivLength; getKeyLength(); if ( ivLength == -1 ) { if ( "AES".equals(base) ) { ivLength = 16; // OpenSSL defaults to 12 // NOTE: we can NOT handle 12 for non GCM mode if ( "GCM".equals(mode) || "CCM".equals(mode) ) ivLength = 12; } //else if ( "DES".equals(base) ) { // ivLength = 8; //} //else if ( "RC4".equals(base) ) { // ivLength = 8; //} else if ( "ECB".equals(mode) ) { ivLength = 0; } else { ivLength = 8; } } return ivLength; } public int getKeyLength() { if ( keyLength != -1 ) return keyLength; int keyLen = -1; if ( version != null ) { try { keyLen = Integer.parseInt(version) / 8; } catch (NumberFormatException e) { keyLen = -1; } } if ( keyLen == -1 ) { if ( "DES".equals(base) ) { if ( "EDE".equalsIgnoreCase(version) ) keyLen = 16; else if ( "EDE3".equalsIgnoreCase(version) ) keyLen = 24; else keyLen = 8; } else if ( "RC4".equals(base) ) { keyLen = 16; } else { keyLen = 16; try { final String name = getRealName(); int maxLen = javax.crypto.Cipher.getMaxAllowedKeyLength(name) / 8; if (maxLen < keyLen) keyLen = maxLen; } catch (NoSuchAlgorithmException e) { } } } return keyLength = keyLen; } @Deprecated public static int[] osslKeyIvLength(final String cipherName) { final Algorithm alg = Algorithm.osslToJava(cipherName); return new int[] { alg.getKeyLength(), alg.getIvLength() }; } @Override public String toString() { return getClass().getSimpleName() + '@' + Integer.toHexString(hashCode()) + "<base="+ base + " mode="+ mode +" version="+ version +" padding="+ padding + " realName="+ realName +" realNameNeedsPadding="+ realNameNeedsPadding +">"; } } private static javax.crypto.Cipher getCipherInstance(final String transformation, boolean silent) throws NoSuchAlgorithmException, NoSuchPaddingException { try { return SecurityHelper.getCipher(transformation); // tries BC if it's available } catch (NoSuchAlgorithmException e) { if ( silent ) return null; throw e; } catch (NoSuchPaddingException e) { if ( silent ) return null; throw e; } } public Cipher(Ruby runtime, RubyClass type) { super(runtime, type); } private javax.crypto.Cipher cipher; // the "real" (Java) Cipher object private String name; private String cryptoBase; private String cryptoVersion; private String cryptoMode; private String paddingType; private String realName; // Cipher's "real" (Java) name - used to instantiate private int keyLength = -1; private int generateKeyLength = -1; private int ivLength = -1; private boolean encryptMode = true; //private IRubyObject[] modeParams; private boolean cipherInited = false; private byte[] key; private byte[] realIV; private byte[] orgIV; private String padding; private void dumpVars(final PrintStream out, final String header) { out.println(this.toString() + ' ' + header + "\n" + " name = " + name + " cryptoBase = " + cryptoBase + " cryptoVersion = " + cryptoVersion + " cryptoMode = " + cryptoMode + " padding_type = " + paddingType + " realName = " + realName + " keyLength = " + keyLength + " ivLength = " + ivLength + "\n" + " cipher.alg = " + (cipher == null ? null : cipher.getAlgorithm()) + " cipher.blockSize = " + (cipher == null ? null : cipher.getBlockSize()) + " encryptMode = " + encryptMode + " cipherInited = " + cipherInited + " key.length = " + (key == null ? 0 : key.length) + " iv.length = " + (realIV == null ? 0 : realIV.length) + " padding = " + padding); } @JRubyMethod(required = 1, visibility = Visibility.PRIVATE) public IRubyObject initialize(final ThreadContext context, final IRubyObject name) { initializeImpl(context.runtime, name.toString()); return this; } final void initializeImpl(final Ruby runtime, final String name) { //if ( ! isSupportedCipher(name) ) { // throw newCipherError(runtime, "unsupported cipher algorithm ("+ name +")"); //} if ( cipher != null ) { throw runtime.newRuntimeError("Cipher already inititalized!"); } updateCipher(name, padding); } @Override @JRubyMethod(required = 1, visibility = Visibility.PRIVATE) public IRubyObject initialize_copy(final IRubyObject obj) { if ( this == obj ) return this; checkFrozen(); final Cipher other = (Cipher) obj; cryptoBase = other.cryptoBase; cryptoVersion = other.cryptoVersion; cryptoMode = other.cryptoMode; paddingType = other.paddingType; realName = other.realName; name = other.name; keyLength = other.keyLength; ivLength = other.ivLength; encryptMode = other.encryptMode; cipherInited = false; if ( other.key != null ) { key = Arrays.copyOf(other.key, other.key.length); } else { key = null; } if (other.realIV != null) { realIV = Arrays.copyOf(other.realIV, other.realIV.length); } else { realIV = null; } this.orgIV = this.realIV; padding = other.padding; cipher = getCipherInstance(); return this; } @JRubyMethod public final RubyString name() { return getRuntime().newString(name); } @JRubyMethod public final RubyInteger key_len() { return getRuntime().newFixnum(keyLength); } @JRubyMethod public final RubyInteger iv_len() { return getRuntime().newFixnum(ivLength); } @JRubyMethod(name = "key_len=", required = 1) public final IRubyObject set_key_len(IRubyObject len) { this.keyLength = RubyNumeric.fix2int(len); return len; } @JRubyMethod(name = "key=", required = 1) public IRubyObject set_key(final ThreadContext context, final IRubyObject key) { final ByteList keyBytes; try { keyBytes = key.asString().getByteList(); } catch (Exception e) { final Ruby runtime = context.runtime; debugStackTrace(runtime, e); throw newCipherError(runtime, e); } if ( keyBytes.length() < keyLength ) { throw newCipherError(context.runtime, "key length too short"); } final byte[] k = new byte[keyLength]; System.arraycopy(keyBytes.unsafeBytes(), keyBytes.getBegin(), k, 0, keyLength); this.key = k; return key; } @JRubyMethod(name = "iv=", required = 1) public IRubyObject set_iv(final ThreadContext context, final IRubyObject iv) { final ByteList ivBytes; try { ivBytes = iv.asString().getByteList(); } catch (Exception e) { final Ruby runtime = context.runtime; debugStackTrace(runtime, e); throw newCipherError(runtime, e); } if ( ivBytes.length() < ivLength ) { throw newCipherError(context.runtime, "iv length too short"); } // EVP_CipherInit_ex uses leading IV length of given sequence. final byte[] i = new byte[ivLength]; System.arraycopy(ivBytes.unsafeBytes(), ivBytes.getBegin(), i, 0, ivLength); this.realIV = i; this.orgIV = this.realIV; if ( ! isStreamCipher() ) cipherInited = false; return iv; } @JRubyMethod public IRubyObject block_size(final ThreadContext context) { final Ruby runtime = context.runtime; checkCipherNotNull(runtime); if ( isStreamCipher() ) { // getBlockSize() returns 0 for stream cipher in JCE // OpenSSL returns 1 for RC4. return runtime.newFixnum(1); } return runtime.newFixnum(cipher.getBlockSize()); } private void init(final ThreadContext context, final IRubyObject[] args, final boolean encrypt) { final Ruby runtime = context.runtime; Arity.checkArgumentCount(runtime, args, 0, 2); encryptMode = encrypt; cipherInited = false; if ( args.length > 0 ) { /* * oops. this code mistakes salt for IV. * We deprecated the arguments for this method, but we decided * keeping this behaviour for backward compatibility. */ byte[] pass = args[0].asString().getBytes(); byte[] iv = null; try { iv = "OpenSSL for Ruby rulez!".getBytes("ISO8859-1"); byte[] iv2 = new byte[this.ivLength]; System.arraycopy(iv, 0, iv2, 0, this.ivLength); iv = iv2; } catch (Exception e) { } if ( args.length > 1 && ! args[1].isNil() ) { runtime.getWarnings().warning(ID.MISCELLANEOUS, "key derivation by " + getMetaClass().getRealClass().getName() + "#encrypt is deprecated; use " + getMetaClass().getRealClass().getName() + "::pkcs5_keyivgen instead"); iv = args[1].asString().getBytes(); if (iv.length > this.ivLength) { byte[] iv2 = new byte[this.ivLength]; System.arraycopy(iv, 0, iv2, 0, this.ivLength); iv = iv2; } } final MessageDigest digest = Digest.getDigest(runtime, "MD5"); KeyAndIv result = evpBytesToKey(keyLength, ivLength, digest, iv, pass, 2048); this.key = result.key; this.realIV = iv; this.orgIV = this.realIV; } } @JRubyMethod(optional = 2) public IRubyObject encrypt(final ThreadContext context, IRubyObject[] args) { this.realIV = orgIV; init(context, args, true); return this; } @JRubyMethod(optional = 2) public IRubyObject decrypt(final ThreadContext context, IRubyObject[] args) { this.realIV = orgIV; init(context, args, false); return this; } @JRubyMethod public IRubyObject reset(final ThreadContext context) { final Ruby runtime = context.runtime; checkCipherNotNull(runtime); if ( ! isStreamCipher() ) { this.realIV = orgIV; doInitCipher(runtime); } return this; } private void updateCipher(final String name, final String padding) { // given 'rc4' must be 'RC4' here. OpenSSL checks it as a LN of object // ID and set SN. We don't check 'name' is allowed as a LN in ASN.1 for // the possibility of JCE specific algorithm so just do upperCase here // for OpenSSL compatibility. this.name = name.toUpperCase(); this.padding = padding; final Algorithm alg = Algorithm.osslToJava(this.name, this.padding); cryptoBase = alg.base; cryptoVersion = alg.version; cryptoMode = alg.mode; realName = alg.getRealName(); paddingType = alg.getPadding(); keyLength = alg.getKeyLength(); ivLength = alg.getIvLength(); if ( "DES".equalsIgnoreCase(cryptoBase) ) { generateKeyLength = keyLength / 8 * 7; } cipher = getCipherInstance(); } final javax.crypto.Cipher getCipherInstance() { try { return getCipherInstance(realName, false); } catch (NoSuchAlgorithmException e) { throw newCipherError(getRuntime(), "unsupported cipher algorithm (" + realName + ")"); } catch (NoSuchPaddingException e) { throw newCipherError(getRuntime(), "unsupported cipher padding (" + realName + ")"); } } @JRubyMethod(required = 1, optional = 3) public IRubyObject pkcs5_keyivgen(final ThreadContext context, final IRubyObject[] args) { final Ruby runtime = context.runtime; Arity.checkArgumentCount(runtime, args, 1, 4); byte[] pass = args[0].asString().getBytes(); byte[] salt = null; int iter = 2048; IRubyObject vdigest = runtime.getNil(); if ( args.length > 1 ) { if ( ! args[1].isNil() ) { salt = args[1].asString().getBytes(); } if ( args.length > 2 ) { if ( ! args[2].isNil() ) { iter = RubyNumeric.fix2int(args[2]); } if ( args.length > 3 ) { vdigest = args[3]; } } } if ( salt != null && salt.length != 8 ) { throw newCipherError(runtime, "salt must be an 8-octet string"); } final String algorithm; if ( vdigest.isNil() ) algorithm = "MD5"; else { algorithm = (vdigest instanceof Digest) ? ((Digest) vdigest).getAlgorithm() : vdigest.asJavaString(); } final MessageDigest digest = Digest.getDigest(runtime, algorithm); KeyAndIv result = evpBytesToKey(keyLength, ivLength, digest, salt, pass, iter); this.key = result.key; this.realIV = result.iv; this.orgIV = this.realIV; doInitCipher(runtime); return runtime.getNil(); } private void doInitCipher(final Ruby runtime) { if ( isDebug(runtime) ) { dumpVars( runtime.getOut(), "doInitCipher()" ); } checkCipherNotNull(runtime); if ( key == null ) { //key = emptyKey(keyLength); throw newCipherError(runtime, "key not specified"); } try { // ECB mode is the only mode that does not require an IV if ( "ECB".equalsIgnoreCase(cryptoMode) ) { cipher.init(encryptMode ? ENCRYPT_MODE : DECRYPT_MODE, new SimpleSecretKey(getCipherAlgorithm(), this.key) ); } else { // if no IV yet, start out with all \0s if ( realIV == null ) realIV = new byte[ivLength]; if ( "RC2".equalsIgnoreCase(cryptoBase) ) { cipher.init(encryptMode ? ENCRYPT_MODE : DECRYPT_MODE, new SimpleSecretKey("RC2", this.key), new RC2ParameterSpec(this.key.length * 8, this.realIV) ); } else if ( "RC4".equalsIgnoreCase(cryptoBase) ) { cipher.init(encryptMode ? ENCRYPT_MODE : DECRYPT_MODE, new SimpleSecretKey("RC4", this.key) ); } else { final AlgorithmParameterSpec ivSpec; if ( "GCM".equalsIgnoreCase(cryptoMode) ) { // e.g. 'aes-128-gcm' ivSpec = new GCMParameterSpec(getAuthTagLength() * 8, this.realIV); } else { ivSpec = new IvParameterSpec(this.realIV); } cipher.init(encryptMode ? ENCRYPT_MODE : DECRYPT_MODE, new SimpleSecretKey(getCipherAlgorithm(), this.key), ivSpec ); } } } catch (InvalidKeyException e) { throw newCipherError(runtime, e + "\n possibly you need to install Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files for your JRE"); } catch (Exception e) { debugStackTrace(runtime, e); throw newCipherError(runtime, e); } cipherInited = true; processedDataBytes = 0; //outBuffer = new ByteList(keyLength); } private String getCipherAlgorithm() { final int idx = realName.indexOf('/'); return idx <= 0 ? realName : realName.substring(0, idx); } private int processedDataBytes = 0; //private volatile ByteList outBuffer; private byte[] lastIV; @JRubyMethod public IRubyObject update(final ThreadContext context, final IRubyObject arg) { final Ruby runtime = context.runtime; if ( isDebug(runtime) ) dumpVars( runtime.getOut(), "update()" ); checkCipherNotNull(runtime); checkAuthTag(runtime); final ByteList data = arg.asString().getByteList(); final int length = data.length(); if ( length == 0 ) { throw runtime.newArgumentError("data must not be empty"); } if ( ! cipherInited ) doInitCipher(runtime); final ByteList str; try { updateAuthData(runtime); // if any final byte[] in = data.getUnsafeBytes(); final int offset = data.begin(); final byte[] out = cipher.update(in, offset, length); if ( out != null ) { str = new ByteList(out, false); if ( realIV != null ) { if ( encryptMode ) setLastIVIfNeeded( out ); else setLastIVIfNeeded( in, offset, length ); } processedDataBytes += length; } else { str = new ByteList(ByteList.NULL_ARRAY); } } catch (Exception e) { debugStackTrace( runtime, e ); throw newCipherError(runtime, e); } return RubyString.newString(runtime, str); } @JRubyMethod(name = "<<") public IRubyObject update_deprecated(final ThreadContext context, final IRubyObject data) { context.runtime.getWarnings().warn(ID.DEPRECATED_METHOD, getMetaClass().getRealClass().getName() + "#<< is deprecated; use #update instead"); return update(context, data); } @JRubyMethod(name = "final") public IRubyObject do_final(final ThreadContext context) { final Ruby runtime = context.runtime; checkCipherNotNull(runtime); checkAuthTag(runtime); if ( ! cipherInited ) doInitCipher(runtime); // trying to allow update after final like cruby-openssl. Bad idea. if ( "RC4".equalsIgnoreCase(cryptoBase) ) return runtime.newString(""); final ByteList str; try { if ( isAuthDataMode() ) { str = do_final_with_auth(runtime); } else { final byte[] out = cipher.doFinal(); if ( out != null ) { // TODO: Modifying this line appears to fix the issue, but I do // not have a good reason for why. Best I can tell, lastIv needs // to be set regardless of encryptMode, so we'll go with this // for now. JRUBY-3335. //if ( realIV != null && encryptMode ) ... str = new ByteList(out, false); if ( realIV != null ) setLastIVIfNeeded(out); } else { str = new ByteList(ByteList.NULL_ARRAY); } } //if ( ! isStreamCipher() ) { //if ( str.length() > processedDataBytes && processedDataBytes > 0 ) { // MRI compatibility only trailing bytes : //str.setRealSize(processedDataBytes); //} //} if (realIV != null) { realIV = lastIV; doInitCipher(runtime); } } catch (GeneralSecurityException e) { // cipher.doFinal debugStackTrace(runtime, e); throw newCipherError(runtime, e); } catch (RuntimeException e) { debugStackTrace(runtime, e); throw newCipherError(runtime, e); } return RubyString.newString(runtime, str); } private ByteList do_final_with_auth(final Ruby runtime) throws GeneralSecurityException { updateAuthData(runtime); // if any final ByteList str; // if GCM/CCM is being used, the authentication tag is appended // in the case of encryption, or verified in the case of decryption. // The result is stored in a new buffer. if ( encryptMode ) { final byte[] out = cipher.doFinal(); final int len = getAuthTagLength(); int strLen; if ( ( strLen = out.length - len ) > 0 ) { str = new ByteList(out, 0, strLen, false); } else { str = new ByteList(ByteList.NULL_ARRAY); strLen = 0; } auth_tag = new ByteList(out, strLen, out.length - strLen); return str; } else { final byte[] out; if ( auth_tag != null ) { final byte[] tag = auth_tag.getUnsafeBytes(); out = cipher.doFinal(tag, auth_tag.begin(), auth_tag.length()); } else { out = cipher.doFinal(); } return new ByteList(out, false); } } private void checkAuthTag(final Ruby runtime) { if ( auth_tag != null && encryptMode ) { throw newCipherError(runtime, "authentication tag already generated by cipher"); } } private void setLastIVIfNeeded(final byte[] tmpIV) { setLastIVIfNeeded(tmpIV, 0, tmpIV.length); } private void setLastIVIfNeeded(final byte[] tmpIV, final int offset, final int length) { final int ivLen = this.ivLength; if ( lastIV == null ) lastIV = new byte[ivLen]; if ( length >= ivLen ) { System.arraycopy(tmpIV, offset + (length - ivLen), lastIV, 0, ivLen); } } @JRubyMethod(name = "padding=") public IRubyObject set_padding(IRubyObject padding) { updateCipher(name, padding.toString()); return padding; } private transient ByteList auth_tag; @JRubyMethod(name = "auth_tag") public IRubyObject auth_tag(final ThreadContext context) { if ( auth_tag != null ) { return RubyString.newString(context.runtime, auth_tag); } if ( ! isAuthDataMode() ) { throw newCipherError(context.runtime, "authentication tag not supported by this cipher"); } return context.nil; } @JRubyMethod(name = "auth_tag=") public IRubyObject set_auth_tag(final ThreadContext context, final IRubyObject tag) { if ( ! isAuthDataMode() ) { throw newCipherError(context.runtime, "authentication tag not supported by this cipher"); } final RubyString auth_tag = tag.asString(); this.auth_tag = StringHelper.setByteListShared(auth_tag); return auth_tag; } private boolean isAuthDataMode() { // Authenticated Encryption with Associated Data (AEAD) return "GCM".equalsIgnoreCase(cryptoMode) || "CCM".equalsIgnoreCase(cryptoMode); } private static final int MAX_AUTH_TAG_LENGTH = 16; private int getAuthTagLength() { return Math.min(MAX_AUTH_TAG_LENGTH, this.key.length); // in bytes } private transient ByteList auth_data; @JRubyMethod(name = "auth_data=") public IRubyObject set_auth_data(final ThreadContext context, final IRubyObject data) { if ( ! isAuthDataMode() ) { throw newCipherError(context.runtime, "authentication data not supported by this cipher"); } final RubyString auth_data = data.asString(); this.auth_data = StringHelper.setByteListShared(auth_data); return auth_data; } private boolean updateAuthData(final Ruby runtime) { if ( auth_data == null ) return false; // only to be set if auth-mode //try { final byte[] data = auth_data.getUnsafeBytes(); cipher.updateAAD(data, auth_data.begin(), auth_data.length()); //} //catch (RuntimeException e) { // debugStackTrace( runtime, e ); // throw newCipherError(runtime, e); //} auth_data = null; return true; } @JRubyMethod(name = "authenticated?") public RubyBoolean authenticated_p(final ThreadContext context) { return context.runtime.newBoolean( isAuthDataMode() ); } @JRubyMethod public IRubyObject random_key(final ThreadContext context) { // str = OpenSSL::Random.random_bytes(self.key_len) // self.key = str // return str RubyString str = Random.random_bytes(context.runtime, this.keyLength); this.set_key(context, str); return str; } @JRubyMethod public IRubyObject random_iv(final ThreadContext context) { // str = OpenSSL::Random.random_bytes(self.iv_len) // self.iv = str // return str RubyString str = Random.random_bytes(context.runtime, this.ivLength); this.set_iv(context, str); return str; } //String getAlgorithm() { // return this.cipher.getAlgorithm(); //} final String getName() { return this.name; } //String getCryptoBase() { // return this.cryptoBase; //} //String getCryptoMode() { // return this.cryptoMode; //} final int getKeyLength() { return keyLength; } final int getGenerateKeyLength() { return (generateKeyLength == -1) ? keyLength : generateKeyLength; } private void checkCipherNotNull(final Ruby runtime) { if ( cipher == null ) { throw runtime.newRuntimeError("Cipher not inititalized!"); } } private boolean isStreamCipher() { return cipher.getBlockSize() == 0; } private static RaiseException newCipherError(Ruby runtime, Exception e) { return Utils.newError(runtime, _Cipher(runtime).getClass("CipherError"), e); } private static RaiseException newCipherError(Ruby runtime, String message) { return Utils.newError(runtime, _Cipher(runtime).getClass("CipherError"), message); } private static class KeyAndIv { final byte[] key; final byte[] iv; KeyAndIv(byte[] key, byte[] iv) { this.key = key; this.iv = iv; } } private static KeyAndIv evpBytesToKey( final int key_len, final int iv_len, final MessageDigest md, final byte[] salt, final byte[] data, final int count) { final byte[] key = new byte[key_len]; final byte[] iv = new byte[iv_len]; if ( data == null ) return new KeyAndIv(key, iv); int key_ix = 0; int iv_ix = 0; byte[] md_buf = null; int nkey = key_len; int niv = iv_len; int i; int addmd = 0; for(;;) { md.reset(); if ( addmd++ > 0 ) md.update(md_buf); md.update(data); if ( salt != null ) md.update(salt, 0, 8); md_buf = md.digest(); for ( i = 1; i < count; i++ ) { md.reset(); md.update(md_buf); md_buf = md.digest(); } i = 0; if ( nkey > 0 ) { for(;;) { if ( nkey == 0) break; if ( i == md_buf.length ) break; key[ key_ix++ ] = md_buf[i]; nkey--; i++; } } if ( niv > 0 && i != md_buf.length ) { for(;;) { if ( niv == 0 ) break; if ( i == md_buf.length ) break; iv[ iv_ix++ ] = md_buf[i]; niv--; i++; } } if ( nkey == 0 && niv == 0 ) break; } return new KeyAndIv(key,iv); } private static class NamedCipherAllocator implements ObjectAllocator { private final String cipherBase; NamedCipherAllocator(final String cipherBase) { this.cipherBase = cipherBase; } public Named allocate(Ruby runtime, RubyClass klass) { return new Named(runtime, klass, cipherBase); } }; public static class Named extends Cipher { private static final long serialVersionUID = 5599069534014317221L; final String cipherBase; Named(Ruby runtime, RubyClass type, String cipherBase) { super(runtime, type); this.cipherBase = cipherBase; // e.g. "AES" } /* AES = Class.new(Cipher) do define_method(:initialize) do |*args| cipher_name = args.inject('AES'){|n, arg| "#{n}-#{arg}" } super(cipher_name) end end */ @JRubyMethod(rest = true, visibility = Visibility.PRIVATE) public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) { final StringBuilder name = new StringBuilder(); name.append(cipherBase); if ( args != null ) { for ( int i = 0; i < args.length; i++ ) { name.append('-').append( args[i].asString() ); } } initializeImpl(context.runtime, name.toString()); return this; } } private static class AESCipherAllocator implements ObjectAllocator { private final String keyLength; AESCipherAllocator(final String keyLength) { this.keyLength = keyLength; } public AES allocate(Ruby runtime, RubyClass klass) { return new AES(runtime, klass, keyLength); } }; public static class AES extends Cipher { private static final long serialVersionUID = -3627749495034257750L; final String keyLength; AES(Ruby runtime, RubyClass type, String keyLength) { super(runtime, type); this.keyLength = keyLength; // e.g. "256" } /* AES256 = Class.new(Cipher) do define_method(:initialize) do |mode| mode ||= "CBC" cipher_name = "AES-256-#{mode}" super(cipher_name) end end */ @JRubyMethod(rest = true, visibility = Visibility.PRIVATE) public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) { final String mode; if ( args != null && args.length > 0 ) { mode = args[0].toString(); } else { mode = "CBC"; } initializeImpl(context.runtime, "AES-" + keyLength + '-' + mode); return this; } } }