-
Notifications
You must be signed in to change notification settings - Fork 5
/
frenemies.js
177 lines (161 loc) · 5.5 KB
/
frenemies.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
'use strict';
/**
* Allow modules to establish secure channels to other modules.
* This is a general purpose building block that should eventually enable:
* - product teams to grant more privilege to dependencies they
* know are compatible with their security needs than to the
* bulk of dependencies.
* - allow conveying sensitive data (secrets, passwords, PII) from
* one part of a system to another that are opaque to cross-cutting
* concerns like logging.
* - allow modules that provide APIs that are unsafe when not used
* very carefully to interact closely with carefully vetted modules
* but failsafe when used by general purpose code.
*/
// Capture some globals so we can rely on them later
const { create, defineProperties, freeze } = Object;
/**
* Maps opaque boxes to box data records.
*/
const boxes = defineProperties(
new WeakMap(),
{
get: { value: WeakMap.prototype.get },
set: { value: WeakMap.prototype.set }
});
/**
* A set of all public keys.
*/
const publicKeys = defineProperties(
new WeakSet(),
{
has: { value: WeakSet.prototype.has },
add: { value: WeakSet.prototype.add }
});
/**
* True iff the given function is in fact a public key.
*
* Public keys are represented as functions that return
* the first of two arguments if called during the execution
* of their matching private key, or otherwise return their
* second argument.
*
* Their arguments default to (true, false).
*/
const isPublicKey = publicKeys.has.bind(publicKeys);
/** An opaque token used to represent a boxed value in transit. */
export class Box {
toString() { return '[Box]'; }
}
/**
* Space for collaboration between the private and public
* halves of a public/private key pair.
*/
let hidden = undefined;
/**
* Creates a bundle that should be available as
* a local variable to module code.
*/
export function makeFrenemies(moduleIdentifier) {
// Allocate a public/private key pair.
function publicKey() {
return !!(hidden === privateKey);
}
function privateKey(f) {
const previous = hidden;
hidden = privateKey;
try {
return f();
} finally {
hidden = previous;
}
}
publicKeys.add(publicKey);
// We attach a module identifier to the public key to enable
// whitelisting based on strings in a configuration without having
// to load modules before storing their public key in a set.
// TODO: This may be less robust than private/public key pair checking.
// TODO: Find a better way. Maybe imports that only import the public
// TODO: key are a special case that don't cause loading.
defineProperties(
publicKey,
{
moduleIdentifier: { value: '' + moduleIdentifier, enumerable: true },
call: { value: Function.prototype.call, enumerable: true },
apply: { value: Function.prototype.apply, enumerable: true }
});
/**
* Wraps a value in a box so that only an approved
* opener may unbox it.
*
* @param value the value that will be given to
* an approved unboxer.
* @param mayOpen receives the public key of the opener.
* Should return `true` to allow.
* This will be called in the context of the opener's
* private key, so the public key should also return true
* called with no arguments.
* @return a box that is opaque to any receivers that cannot
* unbox it.
*/
function box(value, mayOpen) {
if (typeof mayOpen !== 'function') {
throw new Error(`Expected function not ${mayOpen}`);
}
const box = new Box(); // An opaque token
boxes.set(
box,
freeze({ boxerPriv: privateKey, boxerPub: publicKey, value, mayOpen }));
return box;
}
// TODO should this be async so it can await the box?
/**
* Tries to open a box.
*
* @param box the box to unbox.
* @param ifFrom if the box may be opened by this unboxer's owner,
* then ifFrom receives the publicKey of the box creator.
* It should return true to allow unboxing to proceed.
* TODO: Is this the object identity equivalent of an
* out-of-order verify/decrypt fault?
* http://world.std.com/~dtd/sign_encrypt/sign_encrypt7.html
* @param fallback a value to substitute if unboxing failed.
* Defaults to undefined.
* @return the value if unboxing is allowed or fallback otherwise.
*/
function unbox(box, ifFrom, fallback) {
if (typeof ifFrom !== 'function') {
throw new Error(`Expected function not ${ifFrom}`);
}
const data = boxes.get(box);
if (!data) { return fallback; }
const { boxerPriv, boxerPub, value, mayOpen } = data;
// Require mutual consent
return (true === privateKey(() => mayOpen(publicKey)) &&
true === boxerPriv(() => ifFrom(boxerPub))) ?
value :
fallback;
}
const neverBoxed = {};
/** Like unbox but raises an exception if unboxing fails. */
function unboxStrict(box, ifFrom) {
const result = unbox(box, ifFrom, neverBoxed);
if (result === neverBoxed) {
throw new Error('Could not unbox');
}
return result;
}
return defineProperties(
create(null),
{
// These close over private keys, so do not leak them.
box: { value: box, enumerable: true },
unbox: { value: unbox, enumerable: true },
unboxStrict: { value: unboxStrict, enumerable: true },
privateKey: { value: privateKey, enumerable: true },
isPublicKey: { value: isPublicKey, enumerable: true },
// Modules may allow access to this, perhaps via
// module object.
publicKey: { value: publicKey, enumerable: true }
});
}