Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(java-shell): use proper crypto.randomBytes() polyfill MONGOSH-988 #1674

Merged
merged 3 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 20 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 7 additions & 10 deletions packages/java-shell/src/main/js/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,18 @@ process.cwd = process.cwd || (() => '/');
globalThis.btoa = globalThis.btoa || (data => Buffer.from(data, 'latin1').toString('base64'));
globalThis.atob = globalThis.atob || (data => Buffer.from(data, 'base64').toString('latin1'));

// The BSON package tries to use crypto.randomBytes(), but that throws
// in the current browserify replacement for that package.
// We provide a low-quality polyfill for now.
// https://jira.mongodb.org/browse/MONGOSH-988
// crypto.randomBytes() is required by BSON package
// but that throws error in the current browserify replacement for that package.
// Here we create crypto object that will be linked to BSON package
// we provide proper implementation of randomBytes in Kotlin in MongoShellEvaluator
const crypto = require('crypto');
try {
crypto.randomBytes(1);
} catch (err) {
crypto.randomBytes = function(size) {
const uint8Array = new Uint8Array(size);
for (var i = 0; i < uint8Array.length; i++) {
uint8Array[i] = Math.random() * 256;
}
return size;
throw new Error('randomBytes is not implemented');
};
};
}

require('../../../../service-provider-core'); // Ensure TextEncoder polyfill is loaded early enough
const ShellApi = require('../../../../shell-api/');
Expand All @@ -34,4 +30,5 @@ _global = {
ShellEvaluator: ShellEvaluator,
toShellResult: ShellApi.toShellResult,
getShellApiType: ShellApi.getShellApiType,
crypto: crypto
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.mongodb.mongosh.service.Either
import com.mongodb.mongosh.service.Left
import com.mongodb.mongosh.service.Right
import org.bson.*
import org.bson.internal.UuidHelper
import org.bson.json.JsonReader
import org.bson.types.*
import org.graalvm.polyglot.Value
Expand Down Expand Up @@ -93,6 +94,14 @@ internal class MongoShellConverter(private val context: MongoShellContext, priva
return array
}

fun toBuffer(array: ByteArray): Value {
val jsArray = context.eval("new Buffer(${array.size})")
array.forEachIndexed { index, v ->
jsArray.setArrayElement(index.toLong(), v)
}
return jsArray
}

fun <T> toJsPromise(promise: Either<T>): Value {
return when (promise) {
is Right -> context.eval("(v) => new Promise(((resolve) => resolve(v)))", "resolved_promise_script").execute(toJs(promise.value))
Expand Down Expand Up @@ -189,7 +198,15 @@ internal class MongoShellConverter(private val context: MongoShellContext, priva
v.instanceOf(context, bsonTypes.numberLong) -> LongResult(JsonReader(v.invokeMember("toExtendedJSON").toString()).readInt64())
v.instanceOf(context, bsonTypes.binData) -> {
val binary = JsonReader(v.invokeMember("toExtendedJSON").toString()).readBinaryData()
BinaryResult(Binary(binary.type, binary.data))
val uuidRepresentation = when (binary.type) {
BsonBinarySubType.UUID_STANDARD.value -> UuidRepresentation.STANDARD
BsonBinarySubType.UUID_LEGACY.value -> UuidRepresentation.JAVA_LEGACY
else -> null
}
if (uuidRepresentation != null) {
UUIDResult(UuidHelper.decodeBinaryToUuid(binary.data, binary.type, uuidRepresentation))
}
else BinaryResult(Binary(binary.type, binary.data))
}
v.instanceOf(context, bsonTypes.hexData) -> {
val binary = JsonReader(v.invokeMember("toExtendedJSON").toString()).readBinaryData()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.graalvm.polyglot.Source
import org.graalvm.polyglot.Value
import org.graalvm.polyglot.proxy.ProxyExecutable
import org.intellij.lang.annotations.Language
import java.security.SecureRandom
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
Expand Down Expand Up @@ -37,7 +38,7 @@ internal class MongoShellEvaluator(client: MongoClient?, private val context: Mo
toShellResultFn = global.getMember("toShellResult")
getShellApiTypeFn = global.getMember("getShellApiType")
shellInstanceState.invokeMember("setCtx", context.bindings)
initContext(context.bindings)
initContext(context.bindings, global)
}

private fun resultHandler() = context.jsFun { args ->
Expand All @@ -51,13 +52,24 @@ internal class MongoShellEvaluator(client: MongoClient?, private val context: Mo
}
}

private fun initContext(bindings: Value) {
private fun initContext(bindings: Value, global: Value) {
val date = context.eval("(dateHelper) => function inner() { return dateHelper(new.target !== undefined, ...arguments) }", "dateHelper_script")
.execute(ProxyExecutable { args -> dateHelper(args[0].asBoolean(), args.drop(1)) })
.execute(ProxyExecutable { args -> dateHelper(args[0].asBoolean(), args.drop(1)) })
date["now"] = ProxyExecutable { System.currentTimeMillis() }
bindings["Date"] = date
bindings["ISODate"] = context.jsFun { args -> dateHelper(true, args.toList()) }
bindings["UUID"] = context.jsFun { args -> if (args.isEmpty()) UUID.randomUUID() else UUID.fromString(args[0].asString()) }
val secureRandom = SecureRandom()
global["crypto"]!!["randomBytes"] = context.jsFun { args ->
// randomBytes method is used to generate UUID bson object
// here we use SecureRandom to generate cryptographically strong random numbers
// java.util.UUID also uses SecureRandom
if (args.size != 1) {
throw IllegalArgumentException("Expected one argument. Got ${args.size} ${args.contentToString()}")
}
val randomBytes = ByteArray(args[0].asInt())
secureRandom.nextBytes(randomBytes)
return@jsFun converter.toBuffer(randomBytes) // Buffer must be used because .toString('hex') will be invoked on the object
}
}

private fun shellResult(printable: Value, type: String): Value {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ true
[ { "_id": <ObjectID>, "a": 1, "objectId": <ObjectID>, "maxKey": {"$maxKey": 1}, "minKey": {"$minKey": 1}, "binData": {"$binary": {"base64": "MTIzNA==", "subType": "10"}}, "date": {"$date": {"$numberLong": "1355875200000"}}, "isoDate": {"$date": {"$numberLong": "1355875200000"}}, "numberInt": 24, "timestamp": Timestamp{value=429496729600, seconds=100, inc=0}, "undefined": null, "null": null, "uuid": <UUID> } ]
{ "acknowledged": true, "insertedId": null }
{ "acknowledged": true, "insertedId": {"$date": {"$numberLong": "1355875200000"}} }
{ "acknowledged": true, "insertedId": {"$binary": {"base64": "ASNFZ4mrze8BI0VniavN7w==", "subType": "04"}} }
{ "acknowledged": true, "insertedId": <UUID> }
{ "acknowledged": true, "insertedId": {"$maxKey": 1} }
{ "acknowledged": true, "insertedId": 24 }
{ "acknowledged": true, "insertedId": true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
BinaryResult: {"$binary": {"base64": "MTIzNA==", "subType": "10"}}
BinaryResult: {"$binary": {"base64": "MTIzNA==", "subType": "10"}}
BinaryResult: {"$binary": {"base64": "MTIzNA==", "subType": "04"}}
UUIDResult: 5220b418-8f7d-4cd9-bd27-35b6f8d990c5
BinaryResult: {"$binary": {"base64": "MTIzNA==", "subType": "10"}}
4 changes: 2 additions & 2 deletions packages/java-shell/src/test/resources/literal/BinData.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ db.coll.insertOne({"_id": 1, v: new BinData(16, 'MTIzNA==')})
new BinData(16, 'MTIzNA==')
// command checkResultClass
BinData(16, 'MTIzNA==')
// command checkResultClass
new BinData(4, 'MTIzNA==')
// command checkResultClass dontReplaceId
new BinData(4, 'UiC0GI99TNm9JzW2+NmQxQ==')
// command checkResultClass
db.coll.find().toArray()[0].v
// clear
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
UUIDResult: 5220b418-8f7d-4cd9-bd27-35b6f8d990c5
UUIDResult: 5220b418-8f7d-4cd9-bd27-35b6f8d990c5
UUIDResult: <UUID>
UUIDResult: <UUID>
UUIDResult: <UUID>
UUIDResult: <UUID>
UUIDResult: 5220b418-8f7d-4cd9-bd27-35b6f8d990c5
6 changes: 3 additions & 3 deletions packages/java-shell/src/test/resources/literal/UUID.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// before
db.coll.insertOne({"_id": 1, v: UUID('5220b418-8f7d-4cd9-bd27-35b6f8d990c5')});
// command checkResultClass
// command checkResultClass dontReplaceId
new UUID('5220b418-8f7d-4cd9-bd27-35b6f8d990c5')
// command checkResultClass
// command checkResultClass dontReplaceId
UUID('5220b418-8f7d-4cd9-bd27-35b6f8d990c5')
// command checkResultClass
new UUID()
// command checkResultClass
// command checkResultClass dontReplaceId
db.coll.find().toArray()[0].v;
// clear
db.coll.drop();