/**
* Copyright 2013 Apigee Corporation.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.apigee.trireme.core.modules;
import io.apigee.trireme.core.internal.Charsets;
import io.apigee.trireme.core.internal.CryptoAlgorithms;
import io.apigee.trireme.core.internal.CryptoService;
import io.apigee.trireme.core.InternalNodeModule;
import io.apigee.trireme.core.Utils;
import io.apigee.trireme.core.NodeRuntime;
import io.apigee.trireme.core.modules.crypto.CipherImpl;
import io.apigee.trireme.core.modules.crypto.ConnectionImpl;
import io.apigee.trireme.core.modules.crypto.DHGroupImpl;
import io.apigee.trireme.core.modules.crypto.DHImpl;
import io.apigee.trireme.core.modules.crypto.DecipherImpl;
import io.apigee.trireme.core.modules.crypto.HashImpl;
import io.apigee.trireme.core.modules.crypto.MacImpl;
import io.apigee.trireme.core.modules.crypto.SecureContextImpl;
import io.apigee.trireme.core.modules.crypto.SignImpl;
import io.apigee.trireme.core.modules.crypto.VerifyImpl;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.FunctionObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Undefined;
import org.mozilla.javascript.annotations.JSConstructor;
import org.mozilla.javascript.annotations.JSFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.lang.reflect.InvocationTargetException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Random;
import java.util.ServiceLoader;
import static io.apigee.trireme.core.ArgUtils.*;
/**
* This class implements the internal "crypto" module. It is in turn used by crypto.js, which is the core
* Node.js crypto package. This module supports only the crypto mechanisms that are available in native
* Java 6 or 7. The "noderunner-crypto" module depends on Bouncy Castle. This module uses the ServiceLocator
* interface to load that service, so that it becomes available when added to the class path but
* fails gracefully otherwise.
*/
public class Crypto
implements InternalNodeModule
{
private static final Logger log = LoggerFactory.getLogger(Crypto.class);
public static final String MODULE_NAME = "crypto";
/** This is a maximum value for a byte buffer that seems to be part of V8. Used to make tests pass. */
public static final long MAX_BUFFER_LEN = 0x3fffffffL;
protected static CryptoService cryptoService;
protected static Provider cryptoProvider;
static {
loadCryptoService();
}
@Override
public String getModuleName()
{
return MODULE_NAME;
}
@Override
public Scriptable registerExports(Context cx, Scriptable scope, NodeRuntime runtime)
throws InvocationTargetException, IllegalAccessException, InstantiationException
{
ScriptableObject.defineClass(scope, CryptoImpl.class);
CryptoImpl export = (CryptoImpl) cx.newObject(scope, CryptoImpl.CLASS_NAME);
export.setRuntime(runtime);
// We have to lock the scope in which the randomBytes/pseudoRandomBytes methods are executed to the `export`
// CryptoImpl instance. In the JS module, the binding methods are exposed through exports, but this reassignment
// makes them lose the scope of the module. That is:
//
// var binding = process.binding('crypto');
//
// binding.randomBytes(...); // works fine
//
// var r = binding.randomBytes;
// r(); // fails; wrong scope
//
// These methods can't be static/independent of the module because we need access to the runtime.
//
// Interestingly enough, when using the "regular," non-varargs version of JSFunction, Rhino pulls a new instance
// of CryptoImpl out of a hat and uses it for the `this` scope in the Java code. This new instance is
// *not* `exports`, and hasn't been initialized here, so it doesn't have a reference to the runtime.
// With the varargs form, `thisObj` is the "wrong" scope (not a CryptoImpl), and func.getParentScope()
// is the new, uninitialized CryptoImpl instance.
ScriptableObject proto = (ScriptableObject) export.getPrototype();
FunctionObject randomBytes = (FunctionObject) proto.get("randomBytes", proto);
randomBytes.setParentScope(export);
FunctionObject pseudoRandomBytes = (FunctionObject) proto.get("pseudoRandomBytes", proto);
pseudoRandomBytes.setParentScope(export);
ScriptableObject.defineClass(export, HashImpl.class, false, true);
ScriptableObject.defineClass(export, MacImpl.class, false, true);
ScriptableObject.defineClass(export, CipherImpl.class);
ScriptableObject.defineClass(export, DecipherImpl.class);
ScriptableObject.defineClass(export, SignImpl.class);
ScriptableObject.defineClass(export, VerifyImpl.class);
ScriptableObject.defineClass(export, SecureContextImpl.class);
ScriptableObject.defineClass(export, DHImpl.class);
ScriptableObject.defineClass(export, DHGroupImpl.class);
ScriptableObject.defineClass(export, ConnectionImpl.class);
return export;
}
/**
* Load the crypto service using the service loader. This is in a separate jar and loaded using the service
* loader because it depends on Bouncy Castle and we don't want that required all the time. So, we load it
* lazily here. We also load the Bouncy Castle provider (if present) so that we can manually select it
* for certain algorithms if we wish.
*/
private static void loadCryptoService()
{
ServiceLoader<CryptoService> loc = ServiceLoader.load(CryptoService.class);
if (loc.iterator().hasNext()) {
if (log.isDebugEnabled()) {
log.debug("Using crypto service implementation {}", cryptoService);
}
cryptoService = loc.iterator().next();
cryptoProvider = cryptoService.getProvider();
} else if (log.isDebugEnabled()) {
log.debug("No crypto service available");
}
}
public static ByteBuffer convertString(Object o, String encoding, Context cx, Scriptable scope)
{
if (o instanceof String) {
Charset cs = Charsets.get().resolveCharset(encoding);
return Utils.stringToBuffer((String)o, cs);
} else if (o instanceof Buffer.BufferImpl) {
return ((Buffer.BufferImpl)o).getBuffer();
} else {
throw Utils.makeError(cx, scope, "argument must be a String or Buffer");
}
}
public static void ensureCryptoService(Context cx, Scriptable scope)
{
if (cryptoService == null) {
throw Utils.makeError(cx, scope, "Crypto service not available");
}
}
public static CryptoService getCryptoService() {
return cryptoService;
}
public static Provider getCryptoProvider() {
return (cryptoService == null ? null : cryptoService.getProvider());
}
public static class CryptoImpl
extends ScriptableObject
{
public static final String CLASS_NAME = "_cryptoClass";
private static final SecureRandom secureRandom = new SecureRandom();
private static final Random pseudoRandom = new Random();
private NodeRuntime runtime;
@Override
public String getClassName()
{
return CLASS_NAME;
}
@JSFunction
@SuppressWarnings("unused")
public static Object randomBytes(Context cx, Scriptable thisObj, Object[] args, Function func)
{
return randomBytesCommon(cx, thisObj, args, func, secureRandom);
}
@JSFunction
@SuppressWarnings("unused")
public static Object pseudoRandomBytes(Context cx, Scriptable thisObj, Object[] args, Function func)
{
return randomBytesCommon(cx, thisObj, args, func, pseudoRandom);
}
private static Object randomBytesCommon(Context cx, Scriptable thisObj, Object[] args, Function func, Random randomImpl) {
CryptoImpl thisClass = (CryptoImpl) func.getParentScope();
// the tests are picky about what can be passed in as size -- only a valid number
Number sizeNum = objArg(args, 0, Number.class, false);
// TypeErrors are thrown on call, not returned in callback
if (sizeNum == null) {
throw Utils.makeTypeError(cx, thisObj, "size must be a number");
} else {
if (sizeNum.longValue() < 0) {
throw Utils.makeTypeError(cx, thisObj, "size must be >= 0");
} else if (sizeNum.longValue() > MAX_BUFFER_LEN) {
throw Utils.makeTypeError(cx, thisObj, "size must be a valid integer");
}
}
Function callback = objArg(args, 1, Function.class, false);
byte[] randomBytes = new byte[sizeNum.intValue()];
randomImpl.nextBytes(randomBytes);
Buffer.BufferImpl randomBytesBuffer = Buffer.BufferImpl.newBuffer(cx, thisObj, randomBytes);
if (callback != null) {
// TODO: what exception can be returned here?
thisClass.runtime.enqueueCallback(callback, callback, thisObj, thisClass.runtime.getDomain(),
new Object[] { null, randomBytesBuffer });
return Undefined.instance;
} else {
return randomBytesBuffer;
}
}
@JSFunction
@SuppressWarnings("unused")
public static Scriptable getCiphers(Context cx, Scriptable thisObj, Object[] args, Function func)
{
return cx.newArray(thisObj, CryptoAlgorithms.get().getCiphers().toArray());
}
@JSFunction
@SuppressWarnings("unused")
public static Scriptable getHashes(Context cx, Scriptable thisObj, Object[] args, Function func)
{
return cx.newArray(thisObj, HashImpl.SUPPORTED_ALGORITHMS.toArray());
}
@JSFunction
@SuppressWarnings("unused")
public static Scriptable PBKDF2(Context cx, Scriptable thisObj, Object[] args, Function func)
{
String pw = stringArg(args, 0);
String saltStr = stringArg(args, 1);
int iterations = intArg(args, 2);
int keyLen = intArg(args, 3);
Function callback = functionArg(args, 4, false);
SecretKey key;
try {
SecretKeyFactory kf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
char[] passphrase = pw.toCharArray();
byte[] salt = saltStr.getBytes(Charsets.UTF8);
PBEKeySpec spec = new PBEKeySpec(passphrase, salt, iterations, keyLen * 8);
try {
key = kf.generateSecret(spec);
} finally {
Arrays.fill(passphrase, '\0');
}
} catch (GeneralSecurityException gse) {
if (callback == null) {
throw Utils.makeError(cx, thisObj, gse.toString());
} else {
callback.call(cx, thisObj, null,
new Object[] { Utils.makeErrorObject(cx, thisObj, gse.toString()) });
return null;
}
}
Buffer.BufferImpl keyBuf = Buffer.BufferImpl.newBuffer(cx, thisObj, key.getEncoded());
if (callback == null) {
return keyBuf;
}
callback.call(cx, thisObj, null,
new Object[] { Context.getUndefinedValue(), keyBuf });
return null;
}
private void setRuntime(NodeRuntime runtime) {
this.runtime = runtime;
}
}
}