UNPKG

priv

Version:

Can be used to tie "private" properties to objects in JavaScript.

581 lines (485 loc) 17.1 kB
(function(global, Object, String, Boolean, Error, TypeError) { 'use strict'; if (Object(global) !== global) throw new Error('Expected global object'); var DEFAULT_STORAGE_TYPE = 'WeakKeyedStore', // We capture the built-in functions and methods as they are now and store them as references so that we can // maintain some integrity. This is done to prevent scripts which run later from mischievously trying to get // details about or alter the secrets stored on an object. lazyBind = Function.prototype.bind.bind(Function.prototype.call), create = Object.create, getPrototypeOf = Object.getPrototypeOf, keys = Object.keys, getOwnPropertyNames = Object.getOwnPropertyNames, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor, isExtensible = Object.isExtensible, freeze = Object.freeze, hasOwn = lazyBind(Object.prototype.hasOwnProperty), push = lazyBind(Array.prototype.push), forEach = lazyBind(Array.prototype.forEach), map = lazyBind(Array.prototype.map), join = lazyBind(Array.prototype.join), splice = lazyBind(Array.prototype.splice), ArrayIndexOf = lazyBind(Array.prototype.indexOf), fromCharCode = String.fromCharCode, apply = lazyBind(Function.prototype.apply), bind = lazyBind(Function.prototype.bind), // We only want to define with own properties of the descriptor. define = (function(defineProperty) { return function define(obj, name, desc) { if ('value' in desc && !hasOwn(desc, 'value') || 'get' in desc && !hasOwn(desc, 'get') || 'set' in desc && !hasOwn(desc, 'set') || 'enumerable' in desc && !hasOwn(desc, 'enumerable') || 'writable' in desc && !hasOwn(desc, 'writable') || 'configurable' in desc && !hasOwn(desc, 'configurable')) desc = createSafeDescriptor(desc); return defineProperty(obj, name, desc); }; function createSafeDescriptor(obj) { if (obj == null) { locked = true; throw new TypeError('Argument cannot be null or undefined.'); } obj = Object(obj); var O = create(null), k = keys(obj); for (var i = 0, key = k[i]; key = k[i], i < k.length; i++) O[key] = obj[key]; return O; } })(Object.defineProperty), configure = function configure(config) { if (config === undefined) { config = Object.create(null); } else if (Object(config) !== config) { throw new TypeError('Object expected'); } var storageType = getConfig(config, 'storageType', String, DEFAULT_STORAGE_TYPE), inherit = getConfig(config, 'inherit', Boolean, true), storageGenerators = Object.create(null); storageGenerators.WeakKeyedStore = WeakKeyedStoreFactory.create if (WeakMapStoreFactory) storageGenerators.WeakMap = WeakMapStoreFactory.create; configureStorage(storageType); configureInheritance(inherit); function configureStorage(storageType) { var storageGenerator = storageGenerators[storageType]; if (!storageGenerator) throw new TypeError('Storage configuration not found for storage type "' + storageType + '".'); CouplerFactory.configureStorage(storageGenerator); } function configureInheritance(tf) { CouplerFactory.configureInheritance(tf); } function create() { var coupler = CouplerFactory.create(); coupler.toString = function() { return 'function SecretCoupler(object) { return secret; }'; }; return coupler; } var secrets = { }; define(secrets, 'create', { value: create, writable: true, enumerable: false, configurable: true }); define(secrets, 'configure', { value: configure, writable: true, enumerable: false, configurable: true }); return secrets; }; var CouplerFactory = (function() { var inheritDefault = true, storeGenerator = function() { throw new Error('No store has been configured.'); }, mixin = function mixin(target, source) { forEach(getOwnPropertyNames(source), function(name) { define(target, name, getOwnPropertyDescriptor(source, name)); }); return target; }, protoIsMutable = (function() { // TODO: Keep up-to-date with whether ES6 goes with __proto__ or Reflect.setPrototypeOf. var A = { }, A2 = { }, B = create(A); B.__proto__ = A2; return getPrototypeOf(B) === A2; })(), setPrototypeOf = Object.setPrototypeOf || (function() { if (!protoIsMutable) return; // It is now believed ES6 will allow the setter on `__proto__` to work on any object form any realm. var protoDesc = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__'), _setProto = protoDesc && protoDesc.set; if (typeof _setProto == 'function') return lazyBind(_setProto); // If the implementation supports mutable proto but doesn't have a __proto__ setter, see if // mutable proto is possible on objects which don't inherit from Object.prototype. // This behavior has been observed on Chrome 25-27 but is believed to be fixed on a later V8. // https://mail.mozilla.org/pipermail/es-discuss/2013-March/029244.html // However, the version of V8 mentioned in the above post does not support __proto__ setter. // It is believed a later version of V8 will support a __proto__ setter, but for interim // implementations it may be impossible to mutate [[Prototype]] on an object which doesn't // inherit from `Object.prototype`. var A = create(null), B = create(null); A.test = 5; try { B.__proto__ = A; } catch(x) { } if (B.test === 5) return function(obj, proto) { obj.__proto__ = proto; }; return function() { locked = true; throw new Error( 'Mutable prototype is supported by this implementation, but it does not support mutating the prototype ' + 'of an object which doesn\'t inherit from Object.prototype.' ); }; })(), assertFunction = function assertFunction(f) { if (typeof f != 'function') throw new TypeError('Function expected'); return f; }, configureStorage = function configureStorage(storeGen) { return storeGenerator = assertFunction(storeGen); }, configureInheritance = function configureInheritance(tf) { inheritDefault = tf; }, createCoupler = function createCoupler(config) { var createStore = getConfig(config, 'storeGenerator', assertFunction, storeGenerator), inherit = getConfig(config, 'inherit', Boolean, inheritDefault), store = createStore(); return function coupler(obj) { var S = store.get(obj), proto, protoS, protoSTest; if (!S) { proto = inherit ? getPrototypeOf(obj) : null; store.set(obj, S = create(proto ? coupler(proto) : null)); } else if (inherit && protoIsMutable) { proto = getPrototypeOf(obj); protoS = getPrototypeOf(S); protoSTest = proto == null ? null : coupler(proto); // If the prototype on the object has changed, then change the secret's // prototype to reflect this. if (protoSTest !== protoS) try { setPrototypeOf(S, protoSTest); } catch(x) { // This could occur under some circumstances. For ES6 compliant implementations, it should not // occur (assuming the __proto__ setter is allowed to work cross-realm, as is expected). // Likewise, in ES5 implementations without the __proto__ extension, there's no problem because // mutable proto is not supported. // However, many of the ES5 browsers which have the __proto__ extension have implemented it in // ways that may not provide a __proto__ setter or may not support cross-realm setting or setting // on objects which don't inherit from Object.prototype. // The fallback is to generate a new object which has the same properties as `S` and // then return that. This is a little hacky because we don't actually end up preserving // object identity across the same secrets, and it could cause certain situations to // break, such as using secrets as keys in a WeakMap, or some other place where object // identity of secrets matters. However, it is so unlikely that the two conditions // necessary to cause problems here will occur together (since both are by themselves // edge-case scenarios), we are happy enough with this solution, since it is the best // we can do given the direction ES6 is going. store.set(S = mixin(create(protoSTest), S)); } } return S; }; }; return { configureStorage: configureStorage, configureInheritance: configureInheritance, create: createCoupler }; })(); function getConfig(config, prop, coerce, defaultValue) { var value; if (config != null && hasOwn(config, prop)) { value = config.prop; } if (value === undefined) { value = defaultValue; } return coerce(value); } var WeakKeyedStoreFactory = (function() { var locked = true, valid = false, // ES6? TODO: Keep up with this. getPropertyNames = Object.getPropertyNames, getPropertyDescriptors = Object.getPropertyDescriptors, getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors, _Proxy = typeof Proxy == 'undefined' ? undefined : Proxy, MIN_PRECISION = -Math.pow(2, 53), MAX_PRECISION = -MIN_PRECISION, // idNum will ensure identifiers are unique. idNum = (function() { // Use an Array-like rather than a true array to protect against setters defined on Array.prototype indices. var idNum = create(null); idNum[0] = MIN_PRECISION; idNum.length = 1; return idNum; })(), random = getRandomGenerator(), PARALLEL_KEY = '||:' + getIdentifier(); function StoreGet(obj, parallelId) { if (Object(obj) !== obj) throw new TypeError('Object expected for `obj`. Received "' + obj + '"'); var P = getParallels(obj); return P[parallelId]; } function StoreSet(obj, parallelId, value) { if (Object(obj) !== obj) throw new TypeError('Object expected for `obj`. Received "' + obj + '"'); var P = getParallels(obj); return P[parallelId] = value; } function getParallels(obj) { var D = getOwnPropertyDescriptor(obj, PARALLEL_KEY), F, P; if (!D) return setupParallels(obj); else if (!hasOwn(D, 'value') || typeof D.value != 'function') throw new Error('Parallel object support has been compromised.'); F = D.value; locked = false; try { P = F(); locked = true; } catch(x) { locked = true; valid = false; throw new Error('Parallel object support has been compromised'); } if (!valid) throw new Error('Parallel object support has been compromised'); valid = false; return P; } function setupParallels(obj) { var P = create(null); define(obj, PARALLEL_KEY, { value: function() { if (locked) { valid = false; throw new Error('Parallel object locked.'); } valid = true; locked = true; return P; }, enumerable: false, writable: false, configurable: false }); return P; } function getRandStrs(count, length) { var r = create(null); r.length = 0; for(var i = 0; i < count; i++) { push(r, getRandStr(length)); } return r; } function getRandStr(length) { var s = ''; for (var i = 0; i < length; i++) { s += encodeStr(random() * (125 - 65 + 1)); } return s; } function encodeStr(num) { return fromCharCode(num + 65); } function nextUniqueId() { idNum[0]++; for(var i = 0; idNum[i] >= MAX_PRECISION && i < idNum.length; i++) { idNum[i] = MIN_PRECISION; if (i < idNum.length) idNum[i + 1]++; else { // Reset and add a digit. idNum = map(idNum, function() { return MIN_PRECISION; }); push(idNum, MIN_PRECISION); break; } } return '{' + join(idNum, ',') + '}'; } function createStore() { var store = create(null), id = nextUniqueId(); store.get = function(key) { return StoreGet(key, id); }; store.set = function(key, value) { return StoreSet(key, id, value); }; return freeze(store); } function getIdentifier() { return getRandStr(7) + '1:' + join(getRandStrs(8, 11), '/') + ':' + nextUniqueId(); } function getRandomGenerator() { var getRandomValues = typeof crypto != 'undefined' && crypto != null ? (function() { var f = crypto.random || crypto.getRandomValues; if (f) return f.bind(crypto); return undefined; })() : undefined; if (getRandomValues) { // Firefox (15 & 16) seems to be throwing a weird "not implemented" error on getRandomValues. // Not sure why? try { getRandomValues(new Uint8Array(4)); } catch(x) { getRandomValues = undefined } } if (typeof getRandomValues == 'function' && typeof Uint8Array == 'function') { return (function() { var values = new Uint8Array(16), index = values.length; return function random() { if (index >= values.length) { getRandomValues(values); index = 0; } return values[index++] / 256; }; })(); } else return Math.random; } (function() { // Override get(Own)PropertyNames and get(Own)PropertyDescriptors to hide PARALLEL_KEY. var overrides = create(null); overrides.getOwnPropertyNames = getOwnPropertyNames if (getPropertyNames) overrides.getPropertyNames = getPropertyNames; keys(overrides).forEach(function(u) { var original = overrides[u]; define(Object, u, { value: function(obj) { var names = apply(original, this, arguments); if (u === 'getOwnPropertyNames' && !hasOwn(obj, PARALLEL_KEY)) return names; var index = ArrayIndexOf(names, PARALLEL_KEY); if (~index) splice(names, index, 1); return names; }, enumerable: false, writable: true, configurable: true }); }); overrides = create(null); if (getPropertyDescriptors) overrides.getPropertyDescriptors = getPropertyDescriptors; if (getOwnPropertyDescriptors) overrides.getOwnPropertyDescriptors = getOwnPropertyDescriptors; keys(overrides).forEach(function(u) { var original = overrides[u]; define(Object, u, { value: function(obj) { var desc = apply(original, this, arguments); delete desc[PARALLEL_KEY]; return desc; }, enumerable: false, writable: true, configurable: true }); }); })(); // Override functions which prevent extensions on objects to go ahead and add a parallel map first. [ 'preventExtensions', 'seal', 'freeze' ].forEach(function(u) { var original = Object[u]; define(Object, u, { value: function(obj) { setupParallels(obj); return apply(original, this, arguments); } }); }); if (typeof _Proxy == 'function') Proxy = (function() { /* TODO: This works for "direct_proxies", the current ES6 draft; however, some browsers have * support for an old draft (such as FF 17 and below) which uses Proxy.create(). Should this * version be overridden to protect against discovery of PARALLEL_KEY on these browsers also? */ var trapBypasses = create(null); trapBypasses.defineProperty = Object.defineProperty; trapBypasses.hasOwn = hasOwn; trapBypasses.get = function(target, name) { return target[name]; }; return function Proxy(target, traps) { if (!(this instanceof Proxy)) { // TODO: The new keyword wasn't used. What should be done? return new Proxy(target, traps); } var _traps = create(traps); forEach(keys(trapBypasses), function(trapName) { var bypass = trapBypasses[trapName]; if (typeof traps[trapName] == 'function') { // Override traps which could discover PARALLEL_KEY. _traps[trapName] = function(target, name) { if (name === PARALLEL_KEY) { // Bypass any user defined trap when name === PARALLEL_KEY. return apply(bypass, null, arguments); } return apply(traps[trapName], this, arguments); }; } }); return new _Proxy(target, _traps); }; })(); else if (_Proxy && _Proxy.create) { // Proxy.create = (function() { // // return function create(traps, proto) { // // TODO // }; // // })(); } return { create: createStore }; })(); var WeakMapStoreFactory = (function(WeakMap) { if (WeakMap === undefined) return; var WeakMapGet = lazyBind(WeakMap.prototype.get), WeakMapSet = lazyBind(WeakMap.prototype.set); return { create: function createStore() { var wm = new WeakMap(), store = create(null); store.get = function(key) { return WeakMapGet(wm, key); }; store.set = function(key, value) { return WeakMapSet(wm, key, value); }; return freeze(store); } }; })(typeof WeakMap == 'function' ? WeakMap : undefined); var secrets = configure(); // Export for Node. if (typeof module == 'object' && typeof module.exports == 'object') module.exports = secrets; // Export for AMD else if (typeof global.define == 'function' && global.define.amd) global.define(function() { return secrets; }); // Export as a global else global.secrets = secrets; })(typeof global != 'undefined' && Object(global) === global ? global : typeof window != 'undefined' ? window : this, Object, String, Boolean, Error, TypeError);