Ensure that privates are private.

- Remove JS code injection functionality from UtilsNativeHandler.
- Ensure that utils.expose only exposes public properties.
- Prevent privates from getting poisoned via arbitrary constructor invocations.
- Prevent privates from leaking through prototypes.

BUG=603748

Review URL: https://codereview.chromium.org/1903303002

Cr-Commit-Position: refs/heads/master@{#389292}
(cherry picked from commit 77e0fbe12e32b16d5c1d7c0380b45fde363004b2)

Review URL: https://codereview.chromium.org/1938123002 .

Cr-Commit-Position: refs/branch-heads/2704@{#337}
Cr-Branched-From: 6e53600def8f60d8c632fadc70d7c1939ccea347-refs/heads/master@{#386251}
diff --git a/chrome/renderer/resources/extensions/automation/automation_event.js b/chrome/renderer/resources/extensions/automation/automation_event.js
index 7d04ecc..0ba408d 100644
--- a/chrome/renderer/resources/extensions/automation/automation_event.js
+++ b/chrome/renderer/resources/extensions/automation/automation_event.js
@@ -19,8 +19,18 @@
   }
 };
 
-exports.AutomationEvent = utils.expose(
-    'AutomationEvent',
-    AutomationEventImpl,
-    { functions: ['stopPropagation'],
-      readonly: ['type', 'target', 'eventPhase'] });
+function AutomationEvent() {
+  privates(AutomationEvent).constructPrivate(this, arguments);
+}
+utils.expose(AutomationEvent, AutomationEventImpl, {
+  functions: [
+    'stopPropagation',
+  ],
+  readonly: [
+    'type',
+    'target',
+    'eventPhase',
+  ],
+});
+
+exports.$set('AutomationEvent', AutomationEvent);
diff --git a/chrome/renderer/resources/extensions/automation/automation_node.js b/chrome/renderer/resources/extensions/automation/automation_node.js
index ade163d..ab5d26a 100644
--- a/chrome/renderer/resources/extensions/automation/automation_node.js
+++ b/chrome/renderer/resources/extensions/automation/automation_node.js
@@ -937,46 +937,57 @@
   },
 };
 
-var AutomationNode = utils.expose('AutomationNode',
-                                  AutomationNodeImpl,
-                                  { functions: ['doDefault',
-                                                'find',
-                                                'findAll',
-                                                'focus',
-                                                'makeVisible',
-                                                'matches',
-                                                'setSelection',
-                                                'showContextMenu',
-                                                'addEventListener',
-                                                'removeEventListener',
-                                                'domQuerySelector',
-                                                'toString',
-                                                'boundsForRange'],
-                                    readonly: publicAttributes.concat(
-                                              ['parent',
-                                               'firstChild',
-                                               'lastChild',
-                                               'children',
-                                               'previousSibling',
-                                               'nextSibling',
-                                               'isRootNode',
-                                               'role',
-                                               'state',
-                                               'location',
-                                               'indexInParent',
-                                               'root']) });
+function AutomationNode() {
+  privates(AutomationNode).constructPrivate(this, arguments);
+}
+utils.expose(AutomationNode, AutomationNodeImpl, {
+  functions: [
+    'doDefault',
+    'find',
+    'findAll',
+    'focus',
+    'makeVisible',
+    'matches',
+    'setSelection',
+    'showContextMenu',
+    'addEventListener',
+    'removeEventListener',
+    'domQuerySelector',
+    'toString',
+    'boundsForRange',
+  ],
+  readonly: $Array.concat(publicAttributes, [
+      'parent',
+      'firstChild',
+      'lastChild',
+      'children',
+      'previousSibling',
+      'nextSibling',
+      'isRootNode',
+      'role',
+      'state',
+      'location',
+      'indexInParent',
+      'root',
+  ]),
+});
 
-var AutomationRootNode = utils.expose('AutomationRootNode',
-                                      AutomationRootNodeImpl,
-                                      { superclass: AutomationNode,
-                                        readonly: ['docTitle',
-                                                   'docUrl',
-                                                   'docLoaded',
-                                                   'docLoadingProgress',
-                                                   'anchorObject',
-                                                   'anchorOffset',
-                                                   'focusObject',
-                                                   'focusOffset'] });
+function AutomationRootNode() {
+  privates(AutomationRootNode).constructPrivate(this, arguments);
+}
+utils.expose(AutomationRootNode, AutomationRootNodeImpl, {
+  superclass: AutomationNode,
+  readonly: [
+    'docTitle',
+    'docUrl',
+    'docLoaded',
+    'docLoadingProgress',
+    'anchorObject',
+    'anchorOffset',
+    'focusObject',
+    'focusOffset',
+  ],
+});
 
 AutomationRootNode.get = function(treeID) {
   return AutomationRootNodeImpl.get(treeID);
diff --git a/chrome/renderer/resources/extensions/enterprise_platform_keys/key_pair.js b/chrome/renderer/resources/extensions/enterprise_platform_keys/key_pair.js
index dc06f16..1cab18f 100644
--- a/chrome/renderer/resources/extensions/enterprise_platform_keys/key_pair.js
+++ b/chrome/renderer/resources/extensions/enterprise_platform_keys/key_pair.js
@@ -30,6 +30,14 @@
                             false /* not extractable */);
 };
 
-exports.KeyPair = utils.expose('KeyPair',
-                               KeyPairImpl,
-                               {readonly:['publicKey', 'privateKey']});
+function KeyPair() {
+  privates(KeyPair).constructPrivate(this, arguments);
+}
+utils.expose(KeyPair, KeyPairImpl, {
+  readonly: [
+    'publicKey',
+    'privateKey',
+  ],
+});
+
+exports.$set('KeyPair', KeyPair);
diff --git a/chrome/renderer/resources/extensions/enterprise_platform_keys/subtle_crypto.js b/chrome/renderer/resources/extensions/enterprise_platform_keys/subtle_crypto.js
index ece24305..c53bec3 100644
--- a/chrome/renderer/resources/extensions/enterprise_platform_keys/subtle_crypto.js
+++ b/chrome/renderer/resources/extensions/enterprise_platform_keys/subtle_crypto.js
@@ -6,7 +6,6 @@
 var internalAPI = require('enterprise.platformKeys.internalAPI');
 var intersect = require('platformKeys.utils').intersect;
 var subtleCryptoModule = require('platformKeys.SubtleCrypto');
-var SubtleCrypto = subtleCryptoModule.SubtleCrypto;
 var SubtleCryptoImpl = subtleCryptoModule.SubtleCryptoImpl;
 var KeyPair = require('enterprise.platformKeys.KeyPair').KeyPair;
 var KeyUsage = require('platformKeys.Key').KeyUsage;
@@ -133,11 +132,15 @@
   });
 };
 
-exports.SubtleCrypto =
-    utils.expose('SubtleCrypto',
-                 EnterpriseSubtleCryptoImpl,
-                 {
-                   superclass: SubtleCrypto,
-                   functions: ['generateKey']
-                   // ['sign', 'exportKey'] are exposed by the base class
-                 });
+function SubtleCrypto() {
+  privates(SubtleCrypto).constructPrivate(this, arguments);
+}
+utils.expose(SubtleCrypto, EnterpriseSubtleCryptoImpl, {
+  superclass: subtleCryptoModule.SubtleCrypto,
+  functions: [
+    'generateKey',
+    // 'sign', 'exportKey' are exposed by the base class
+  ],
+});
+
+exports.$set('SubtleCrypto', SubtleCrypto);
diff --git a/chrome/renderer/resources/extensions/enterprise_platform_keys/token.js b/chrome/renderer/resources/extensions/enterprise_platform_keys/token.js
index dee7952..266db59 100644
--- a/chrome/renderer/resources/extensions/enterprise_platform_keys/token.js
+++ b/chrome/renderer/resources/extensions/enterprise_platform_keys/token.js
@@ -15,5 +15,14 @@
   this.subtleCrypto = new SubtleCrypto(id);
 };
 
-exports.Token =
-    utils.expose('Token', TokenImpl, {readonly:['id', 'subtleCrypto']});
+function Token() {
+  privates(Token).constructPrivate(this, arguments);
+}
+utils.expose(Token, TokenImpl, {
+  readonly: [
+    'id',
+    'subtleCrypto',
+  ],
+});
+
+exports.$set('Token', Token);
diff --git a/chrome/renderer/resources/extensions/platform_keys/key.js b/chrome/renderer/resources/extensions/platform_keys/key.js
index ee29bf0..bea1fc40 100644
--- a/chrome/renderer/resources/extensions/platform_keys/key.js
+++ b/chrome/renderer/resources/extensions/platform_keys/key.js
@@ -49,10 +49,17 @@
   }
 });
 
-var Key = utils.expose(
-    'Key',
-    KeyImpl,
-    {superclass: KeyBase, readonly: ['extractable', 'type', 'usages']});
+function Key() {
+  privates(Key).constructPrivate(this, arguments);
+}
+utils.expose(Key, KeyImpl, {
+  superclass: KeyBase,
+  readonly: [
+    'extractable',
+    'type',
+    'usages',
+  ],
+});
 
 /**
  * Returns |key|'s Subject Public Key Info. Throws an exception if |key| is not
diff --git a/chrome/renderer/resources/extensions/platform_keys/subtle_crypto.js b/chrome/renderer/resources/extensions/platform_keys/subtle_crypto.js
index 14a5388..3904003 100644
--- a/chrome/renderer/resources/extensions/platform_keys/subtle_crypto.js
+++ b/chrome/renderer/resources/extensions/platform_keys/subtle_crypto.js
@@ -110,10 +110,16 @@
   });
 };
 
-// Required for subclassing.
-exports.SubtleCryptoImpl = SubtleCryptoImpl
+function SubtleCrypto() {
+  privates(SubtleCrypto).constructPrivate(this, arguments);
+}
+utils.expose(SubtleCrypto, SubtleCryptoImpl, {
+  functions: [
+    'sign',
+    'exportKey',
+  ],
+});
 
-exports.SubtleCrypto =
-    utils.expose('SubtleCrypto',
-                 SubtleCryptoImpl,
-                 {functions:['sign', 'exportKey']});
+// Required for subclassing.
+exports.$set('SubtleCryptoImpl', SubtleCryptoImpl);
+exports.$set('SubtleCrypto', SubtleCrypto);
diff --git a/chrome/renderer/resources/extensions/web_view/chrome_web_view.js b/chrome/renderer/resources/extensions/web_view/chrome_web_view.js
index 2d8c445..abad162 100644
--- a/chrome/renderer/resources/extensions/web_view/chrome_web_view.js
+++ b/chrome/renderer/resources/extensions/web_view/chrome_web_view.js
@@ -12,7 +12,7 @@
 var EventBindings = require('event_bindings');
 var GuestViewInternalNatives = requireNative('guest_view_internal');
 var idGeneratorNatives = requireNative('id_generator');
-var Utils = require('utils');
+var utils = require('utils');
 var WebViewImpl = require('webView').WebViewImpl;
 
 // This is the only "webViewInternal.onClicked" named event for this renderer.
@@ -122,9 +122,17 @@
   return $Function.apply(ChromeWebView.contextMenusUpdate, null, args);
 };
 
-var WebViewContextMenus = Utils.expose(
-    'WebViewContextMenus', WebViewContextMenusImpl,
-    { functions: ['create', 'remove', 'removeAll', 'update'] });
+function WebViewContextMenus() {
+  privates(WebViewContextMenus).constructPrivate(this, arguments);
+}
+utils.expose(WebViewContextMenus, WebViewContextMenusImpl, {
+  functions: [
+    'create',
+    'remove',
+    'removeAll',
+    'update',
+  ],
+});
 
 // -----------------------------------------------------------------------------
 
@@ -132,7 +140,7 @@
   if (!this.contextMenusOnContextMenuEvent_) {
     var eventName = 'chromeWebViewInternal.onContextMenuShow';
     var eventSchema =
-        Utils.lookup(ChromeWebViewSchema.events, 'name', 'onShow');
+        utils.lookup(ChromeWebViewSchema.events, 'name', 'onShow');
     var eventOptions = {supportsListeners: true};
     this.contextMenusOnContextMenuEvent_ = new ContextMenusOnContextMenuEvent(
         this.viewInstanceId, eventName, eventSchema, eventOptions);
@@ -152,7 +160,7 @@
           if (!this.contextMenusOnClickedEvent_) {
             var eventName = 'chromeWebViewInternal.onClicked';
             var eventSchema =
-                Utils.lookup(ChromeWebViewSchema.events, 'name', 'onClicked');
+                utils.lookup(ChromeWebViewSchema.events, 'name', 'onClicked');
             var eventOptions = {supportsListeners: true};
             var onClickedEvent = new ContextMenusOnClickedEvent(
                 this.viewInstanceId, eventName, eventSchema, eventOptions);
diff --git a/extensions/renderer/module_system.cc b/extensions/renderer/module_system.cc
index 692939e..4ff9b5c 100644
--- a/extensions/renderer/module_system.cc
+++ b/extensions/renderer/module_system.cc
@@ -615,6 +615,10 @@
           ToV8StringUnsafe(GetIsolate(), "Failed to create privates"));
       return;
     }
+    v8::Maybe<bool> maybe =
+        privates.As<v8::Object>()->SetPrototype(context()->v8_context(),
+                                                v8::Null(args.GetIsolate()));
+    CHECK(maybe.IsJust() && maybe.FromJust());
     SetPrivate(obj, "privates", privates);
   }
   args.GetReturnValue().Set(privates);
diff --git a/extensions/renderer/module_system_unittest.cc b/extensions/renderer/module_system_unittest.cc
index f49b3b9..5803ad0 100644
--- a/extensions/renderer/module_system_unittest.cc
+++ b/extensions/renderer/module_system_unittest.cc
@@ -505,4 +505,14 @@
   RunResolvedPromises();
 }
 
+TEST_F(ModuleSystemTest, TestPrivatesIsPrivate) {
+  ModuleSystem::NativesEnabledScope natives_enabled_scope(
+      env()->module_system());
+  env()->RegisterModule(
+      "test",
+      "var v = privates({});"
+      "requireNative('assert').AssertFalse(v instanceof Object);");
+  env()->module_system()->Require("test");
+}
+
 }  // namespace extensions
diff --git a/extensions/renderer/resources/event.js b/extensions/renderer/resources/event.js
index a6d64cf..105f7114 100644
--- a/extensions/renderer/resources/event.js
+++ b/extensions/renderer/resources/event.js
@@ -490,17 +490,22 @@
       ruleFunctionSchemas.getRules.parameters);
   }
 
-  var Event = utils.expose('Event', EventImpl, { functions: [
-    'addListener',
-    'removeListener',
-    'hasListener',
-    'hasListeners',
-    'dispatchToListener',
-    'dispatch',
-    'addRules',
-    'removeRules',
-    'getRules'
-  ] });
+  function Event() {
+    privates(Event).constructPrivate(this, arguments);
+  }
+  utils.expose(Event, EventImpl, {
+    functions: [
+      'addListener',
+      'removeListener',
+      'hasListener',
+      'hasListeners',
+      'dispatchToListener',
+      'dispatch',
+      'addRules',
+      'removeRules',
+      'getRules',
+    ],
+  });
 
   // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc.
   exports.$set('Event', Event);
diff --git a/extensions/renderer/resources/messaging.js b/extensions/renderer/resources/messaging.js
index 3b98432..afd7244 100644
--- a/extensions/renderer/resources/messaging.js
+++ b/extensions/renderer/resources/messaging.js
@@ -400,15 +400,20 @@
     return alignedArgs;
   }
 
-var Port = utils.expose('Port', PortImpl, { functions: [
-    'disconnect',
-    'postMessage'
-  ],
-  properties: [
-    'name',
-    'onDisconnect',
-    'onMessage'
-  ] });
+  function Port() {
+    privates(Port).constructPrivate(this, arguments);
+  }
+  utils.expose(Port, PortImpl, {
+    functions: [
+      'disconnect',
+      'postMessage',
+    ],
+    properties: [
+      'name',
+      'onDisconnect',
+      'onMessage',
+    ],
+  });
 
 exports.$set('kRequestChannel', kRequestChannel);
 exports.$set('kMessageChannel', kMessageChannel);
diff --git a/extensions/renderer/resources/utils.js b/extensions/renderer/resources/utils.js
index eee1356..fd70c60 100644
--- a/extensions/renderer/resources/utils.js
+++ b/extensions/renderer/resources/utils.js
@@ -2,7 +2,6 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-var createClassWrapper = requireNative('utils').createClassWrapper;
 var nativeDeepCopy = requireNative('utils').deepCopy;
 var schemaRegistry = requireNative('schema_registry');
 var CHECK = requireNative('logging').CHECK;
@@ -65,15 +64,21 @@
 }
 
 /**
- * Takes a private class implementation |cls| and exposes a subset of its
- * methods |functions| and properties |properties| and |readonly| in a public
- * wrapper class that it returns. Within bindings code, you can access the
- * implementation from an instance of the wrapper class using
+ * Takes a private class implementation |privateClass| and exposes a subset of
+ * its methods |functions| and properties |properties| and |readonly| to a
+ * public wrapper class that should be passed in. Within bindings code, you can
+ * access the implementation from an instance of the wrapper class using
  * privates(instance).impl, and from the implementation class you can access
  * the wrapper using this.wrapper (or implInstance.wrapper if you have another
  * instance of the implementation class).
- * @param {string} name The name of the exposed wrapper class.
- * @param {Object} cls The class implementation.
+ *
+ * |publicClass| should be a constructor that calls constructPrivate() like so:
+ *
+ *     privates(publicClass).constructPrivate(this, arguments);
+ *
+ * @param {function} publicClass The publicly exposed wrapper class. This must
+ *     be a named function, and the name appears in stack traces.
+ * @param {Object} privateClass The class implementation.
  * @param {{superclass: ?Function,
  *          functions: ?Array<string>,
  *          properties: ?Array<string>,
@@ -84,13 +89,49 @@
  *     delegated to the implementation; |properties| are gettable/settable
  *     properties and |readonly| are read-only properties.
  */
-function expose(name, cls, exposed) {
-  var publicClass = createClassWrapper(name, cls, exposed.superclass);
+function expose(publicClass, privateClass, exposed) {
+  // TODO(robwu): Fix callers and uncomment this assertion.
+  // DCHECK(!(privateClass instanceof $Object.self));
+
+  $Object.setPrototypeOf(exposed, null);
+
+  // This should be called by publicClass.
+  privates(publicClass).constructPrivate = function(self, args) {
+    if (!(self instanceof publicClass)) {
+      throw new Error('Please use "new ' + publicClass.name + '"');
+    }
+    // The "instanceof publicClass" check can easily be spoofed, so we check
+    // whether the private impl is already set before continuing.
+    var privateSelf = privates(self);
+    if ('impl' in privateSelf) {
+      throw new Error('Object ' + publicClass.name + ' is already constructed');
+    }
+    var privateObj = $Object.create(privateClass.prototype);
+    $Function.apply(privateClass, privateObj, args);
+    privateObj.wrapper = self;
+    privateSelf.impl = privateObj;
+  };
+
+  function getPrivateImpl(self) {
+    var impl = privates(self).impl;
+    if (!(impl instanceof privateClass)) {
+      // Either the object is not constructed, or the property descriptor is
+      // used on a target that is not an instance of publicClass.
+      throw new Error('impl is not an instance of ' + privateClass.name);
+    }
+    return impl;
+  }
+
+  var publicClassPrototype = {
+    // The final prototype will be assigned at the end of this method.
+    __proto__: null,
+    constructor: publicClass,
+  };
 
   if ('functions' in exposed) {
     $Array.forEach(exposed.functions, function(func) {
-      publicClass.prototype[func] = function() {
-        var impl = privates(this).impl;
+      publicClassPrototype[func] = function() {
+        var impl = getPrivateImpl(this);
         return $Function.apply(impl[func], impl, arguments);
       };
     });
@@ -98,13 +139,14 @@
 
   if ('properties' in exposed) {
     $Array.forEach(exposed.properties, function(prop) {
-      $Object.defineProperty(publicClass.prototype, prop, {
+      $Object.defineProperty(publicClassPrototype, prop, {
+        __proto__: null,
         enumerable: true,
         get: function() {
-          return privates(this).impl[prop];
+          return getPrivateImpl(this)[prop];
         },
         set: function(value) {
-          var impl = privates(this).impl;
+          var impl = getPrivateImpl(this);
           delete impl[prop];
           impl[prop] = value;
         }
@@ -114,15 +156,22 @@
 
   if ('readonly' in exposed) {
     $Array.forEach(exposed.readonly, function(readonly) {
-      $Object.defineProperty(publicClass.prototype, readonly, {
+      $Object.defineProperty(publicClassPrototype, readonly, {
+        __proto__: null,
         enumerable: true,
         get: function() {
-          return privates(this).impl[readonly];
+          return getPrivateImpl(this)[readonly];
         },
       });
     });
   }
 
+  // The prototype properties have been installed. Now we can safely assign an
+  // unsafe prototype and export the class to the public.
+  var superclass = exposed.superclass || $Object.self;
+  $Object.setPrototypeOf(publicClassPrototype, superclass.prototype);
+  publicClass.prototype = publicClassPrototype;
+
   return publicClass;
 }
 
diff --git a/extensions/renderer/resources/web_request_internal_custom_bindings.js b/extensions/renderer/resources/web_request_internal_custom_bindings.js
index 7c9e279a..df10729 100644
--- a/extensions/renderer/resources/web_request_internal_custom_bindings.js
+++ b/extensions/renderer/resources/web_request_internal_custom_bindings.js
@@ -175,17 +175,20 @@
   });
 });
 
-var WebRequestEvent = utils.expose('WebRequestEvent',
-                                   WebRequestEventImpl,
-                                   { functions: [
-  'hasListener',
-  'hasListeners',
-  'addListener',
-  'removeListener',
-  'addRules',
-  'removeRules',
-  'getRules'
-] });
+function WebRequestEvent() {
+  privates(WebRequestEvent).constructPrivate(this, arguments);
+}
+utils.expose(WebRequestEvent, WebRequestEventImpl, {
+  functions: [
+    'hasListener',
+    'hasListeners',
+    'addListener',
+    'removeListener',
+    'addRules',
+    'removeRules',
+    'getRules',
+  ],
+});
 
 webRequestInternal = binding.generate();
 exports.$set('binding', webRequestInternal);
diff --git a/extensions/renderer/safe_builtins.cc b/extensions/renderer/safe_builtins.cc
index 2933e4d..7669a21 100644
--- a/extensions/renderer/safe_builtins.cc
+++ b/extensions/renderer/safe_builtins.cc
@@ -71,7 +71,7 @@
     "            ['hasOwnProperty'],\n"
     "            ['create', 'defineProperty', 'freeze',\n"
     "             'getOwnPropertyDescriptor', 'getPrototypeOf', 'keys',\n"
-    "             'assign']);\n"
+    "             'assign', 'setPrototypeOf']);\n"
     "saveBuiltin(Function,\n"
     "            ['apply', 'bind', 'call']);\n"
     "saveBuiltin(Array,\n"
diff --git a/extensions/renderer/utils_native_handler.cc b/extensions/renderer/utils_native_handler.cc
index 1ded40c6..741a111 100644
--- a/extensions/renderer/utils_native_handler.cc
+++ b/extensions/renderer/utils_native_handler.cc
@@ -5,7 +5,6 @@
 #include "extensions/renderer/utils_native_handler.h"
 
 #include "base/macros.h"
-#include "base/strings/stringprintf.h"
 #include "extensions/renderer/script_context.h"
 #include "third_party/WebKit/public/web/WebSerializedScriptValue.h"
 
@@ -13,9 +12,6 @@
 
 UtilsNativeHandler::UtilsNativeHandler(ScriptContext* context)
     : ObjectBackedNativeHandler(context) {
-  RouteFunction("createClassWrapper",
-                base::Bind(&UtilsNativeHandler::CreateClassWrapper,
-                           base::Unretained(this)));
   RouteFunction(
       "deepCopy",
       base::Bind(&UtilsNativeHandler::DeepCopy, base::Unretained(this)));
@@ -23,67 +19,6 @@
 
 UtilsNativeHandler::~UtilsNativeHandler() {}
 
-void UtilsNativeHandler::CreateClassWrapper(
-    const v8::FunctionCallbackInfo<v8::Value>& args) {
-  CHECK_EQ(3, args.Length());
-  CHECK(args[0]->IsString());
-  std::string name = *v8::String::Utf8Value(args[0]);
-  CHECK(args[1]->IsObject());
-  v8::Local<v8::Object> cls = args[1].As<v8::Object>();
-  CHECK(args[2]->IsObject() || args[2]->IsUndefined());
-  v8::Local<v8::Value> superclass = args[2];
-
-  v8::HandleScope handle_scope(GetIsolate());
-  // TODO(fsamuel): Consider moving the source wrapping to ModuleSystem.
-  v8::Local<v8::String> source = v8::String::NewFromUtf8(
-      GetIsolate(),
-      base::StringPrintf(
-          "(function($Object, $Function, privates, cls, superclass) {"
-          "'use strict';\n"
-          "  function %s() {\n"
-          "    var privateObj = $Object.create(cls.prototype);\n"
-          "    $Function.apply(cls, privateObj, arguments);\n"
-          "    privateObj.wrapper = this;\n"
-          "    privates(this).impl = privateObj;\n"
-          "  };\n"
-          "  if (superclass) {\n"
-          "    %s.prototype = Object.create(superclass.prototype);\n"
-          "  }\n"
-          "  return %s;\n"
-          "})",
-          name.c_str(), name.c_str(), name.c_str()).c_str());
-  v8::Local<v8::Value> func_as_value = context()->module_system()->RunString(
-      source, v8::String::NewFromUtf8(GetIsolate(), name.c_str()));
-  if (func_as_value.IsEmpty() || func_as_value->IsUndefined()) {
-    args.GetReturnValue().SetUndefined();
-    return;
-  }
-
-  // TODO(fsamuel): Move privates from ModuleSystem to a shared location.
-  v8::Local<v8::Object> natives(context()->module_system()->NewInstance());
-  CHECK(!natives.IsEmpty());  // this can happen if v8 has issues
-
-  v8::Local<v8::Function> func = func_as_value.As<v8::Function>();
-  v8::Local<v8::Value> func_args[] = {
-      context()->safe_builtins()->GetObjekt(),
-      context()->safe_builtins()->GetFunction(),
-      natives->Get(v8::String::NewFromUtf8(GetIsolate(), "privates",
-                                           v8::String::kInternalizedString)),
-      cls,
-      superclass};
-  v8::Local<v8::Value> result;
-  {
-    v8::TryCatch try_catch(GetIsolate());
-    try_catch.SetCaptureMessage(true);
-    result = context()->CallFunction(func, arraysize(func_args), func_args);
-    if (try_catch.HasCaught()) {
-      args.GetReturnValue().SetUndefined();
-      return;
-    }
-  }
-  args.GetReturnValue().Set(result);
-}
-
 void UtilsNativeHandler::DeepCopy(
     const v8::FunctionCallbackInfo<v8::Value>& args) {
   CHECK_EQ(1, args.Length());
diff --git a/extensions/renderer/utils_native_handler.h b/extensions/renderer/utils_native_handler.h
index c69860f..6a2ae4da 100644
--- a/extensions/renderer/utils_native_handler.h
+++ b/extensions/renderer/utils_native_handler.h
@@ -17,11 +17,6 @@
   ~UtilsNativeHandler() override;
 
  private:
-  // |args| consists of two arguments: a public class name, and a reference
-  // to the implementation class. CreateClassWrapper returns a new class
-  // that wraps the implementation, while hiding its members.
-  void CreateClassWrapper(const v8::FunctionCallbackInfo<v8::Value>& args);
-
   // |args| consists of one argument: an arbitrary value. Returns a deep copy of
   // that value. The copy will have no references to nested values of the
   // argument.
diff --git a/extensions/test/data/utils_unittest.js b/extensions/test/data/utils_unittest.js
index 1ecc421..64adec4 100644
--- a/extensions/test/data/utils_unittest.js
+++ b/extensions/test/data/utils_unittest.js
@@ -29,16 +29,22 @@
     subFunc: function() { return 'subFunc'; }
   };
 
-  var SuperClass = utils.expose('SuperClass',
-                                SuperClassImpl,
-                                { functions: ['func', 'superFunc'],
-                                  properties: ['attrA', 'attrB'] });
+  function SuperClass() {
+    privates(SuperClass).constructPrivate(this, arguments);
+  }
+  utils.expose(SuperClass, SuperClassImpl, {
+    functions: ['func', 'superFunc'],
+    properties: ['attrA', 'attrB'],
+  });
 
-  var SubClass = utils.expose('SubClass',
-                              SubClassImpl,
-                              { superclass: SuperClass,
-                                functions: ['subFunc'],
-                                properties: ['attrC'] });
+  function SubClass() {
+    privates(SubClass).constructPrivate(this, arguments);
+  }
+  utils.expose(SubClass, SubClassImpl, {
+    superclass: SuperClass,
+    functions: ['subFunc'],
+    properties: ['attrC'],
+  });
 
   var supe = new SuperClass();
   AssertTrue(supe.attrA == 'aSuper');
@@ -63,12 +69,16 @@
 
   function SubSubClassImpl() {}
   SubSubClassImpl.prototype = Object.create(SubClassImpl.prototype);
-  SubSubClassImpl.prototype.subSubFunc = function() { return 'subsub'; }
+  SubSubClassImpl.prototype.subSubFunc = function() { return 'subsub'; };
 
-  var SubSubClass = utils.expose('SubSubClass',
-                                 SubSubClassImpl,
-                                 { superclass: SubClass,
-                                   functions: ['subSubFunc'] });
+  function SubSubClass() {
+    privates(SubSubClass).constructPrivate(this, arguments);
+  }
+  utils.expose(SubSubClass, SubSubClassImpl, {
+    superclass: SubClass,
+    functions: ['subSubFunc'],
+  });
+
   var subsub = new SubSubClass();
   AssertTrue(subsub.attrA == 'aSub');
   AssertTrue(subsub.attrB == 'bSuper');