nervur 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GETTING_STARTED.md +138 -0
- package/NOTICE +1 -1
- package/README.md +108 -20
- package/dist/being/being.d.ts +24 -0
- package/dist/being/being.js +109 -0
- package/dist/being/digest.d.ts +3 -0
- package/dist/being/digest.js +37 -0
- package/dist/being/index.d.ts +5 -0
- package/dist/being/index.js +6 -0
- package/dist/being/silence.d.ts +12 -0
- package/dist/being/silence.js +41 -0
- package/dist/being/types.d.ts +89 -0
- package/dist/being/types.js +58 -0
- package/dist/conformance/assert.d.ts +11 -0
- package/dist/conformance/assert.js +106 -0
- package/dist/conformance/beings.d.ts +198 -0
- package/dist/conformance/beings.js +183 -0
- package/dist/conformance/estate.d.ts +5 -0
- package/dist/conformance/estate.js +388 -0
- package/dist/conformance/index.d.ts +81 -0
- package/dist/conformance/index.js +816 -0
- package/dist/conformance/reach.d.ts +10 -0
- package/dist/conformance/reach.js +72 -0
- package/dist/conformance/store.d.ts +5 -0
- package/dist/conformance/store.js +123 -0
- package/dist/harbor/core.d.ts +53 -0
- package/dist/harbor/core.js +621 -0
- package/dist/harbor/dial.d.ts +9 -0
- package/dist/harbor/dial.js +81 -0
- package/dist/harbor/index.d.ts +8 -0
- package/dist/harbor/index.js +16 -0
- package/dist/harbor/memory.d.ts +25 -0
- package/dist/harbor/memory.js +72 -0
- package/dist/harbor/reach.d.ts +36 -0
- package/dist/harbor/reach.js +191 -0
- package/dist/harbor/store.d.ts +40 -0
- package/dist/harbor/store.js +70 -0
- package/dist/vector/cases.d.ts +41 -0
- package/dist/vector/cases.js +195 -0
- package/dist/vector/index.d.ts +6 -0
- package/dist/vector/index.js +8 -0
- package/dist/vector/stand.d.ts +9 -0
- package/dist/vector/stand.js +76 -0
- package/dist/vector/world.d.ts +143 -0
- package/dist/vector/world.js +198 -0
- package/dist/ward/allowance.d.ts +10 -0
- package/dist/ward/allowance.js +71 -0
- package/dist/ward/arithmetic.d.ts +31 -0
- package/dist/ward/arithmetic.js +245 -0
- package/dist/ward/cells.d.ts +7 -0
- package/dist/ward/cells.js +186 -0
- package/dist/ward/door.d.ts +18 -0
- package/dist/ward/door.js +186 -0
- package/dist/ward/ground.d.ts +23 -0
- package/dist/ward/ground.js +38 -0
- package/dist/ward/heirs.d.ts +13 -0
- package/dist/ward/heirs.js +115 -0
- package/dist/ward/index.d.ts +9 -0
- package/dist/ward/index.js +13 -0
- package/dist/ward/owner.d.ts +13 -0
- package/dist/ward/owner.js +220 -0
- package/dist/ward/partition.d.ts +65 -0
- package/dist/ward/partition.js +295 -0
- package/dist/ward/seal.d.ts +52 -0
- package/dist/ward/seal.js +145 -0
- package/dist/ward/stance.d.ts +25 -0
- package/dist/ward/stance.js +413 -0
- package/dist/ward/ward.d.ts +12 -0
- package/dist/ward/ward.js +361 -0
- package/package.json +37 -15
- package/protocol/SPEC.md +1843 -0
- package/protocol/vectors/arithmetic.json +117 -0
- package/protocol/vectors/door.json +345 -0
- package/protocol/vectors/framing.json +92 -0
- package/protocol/vectors/wire.json +48 -0
- package/quo-kit.md +652 -0
- package/src/being/being.ts +123 -0
- package/src/being/digest.ts +46 -0
- package/src/being/index.ts +7 -0
- package/src/being/silence.ts +46 -0
- package/src/being/types.ts +170 -0
- package/src/conformance/assert.ts +100 -0
- package/src/conformance/beings.ts +183 -0
- package/src/conformance/estate.ts +412 -0
- package/src/conformance/index.ts +962 -0
- package/src/conformance/reach.ts +83 -0
- package/src/conformance/store.ts +136 -0
- package/src/harbor/core.ts +660 -0
- package/src/harbor/dial.ts +112 -0
- package/src/harbor/index.ts +17 -0
- package/src/harbor/memory.ts +83 -0
- package/src/harbor/reach.ts +215 -0
- package/src/harbor/store.ts +101 -0
- package/src/vector/cases.ts +229 -0
- package/src/vector/index.ts +11 -0
- package/src/vector/stand.ts +75 -0
- package/src/vector/world.ts +220 -0
- package/src/ward/allowance.ts +85 -0
- package/src/ward/arithmetic.ts +247 -0
- package/src/ward/cells.ts +190 -0
- package/src/ward/door.ts +186 -0
- package/src/ward/ground.ts +160 -0
- package/src/ward/heirs.ts +114 -0
- package/src/ward/index.ts +17 -0
- package/src/ward/owner.ts +214 -0
- package/src/ward/partition.ts +353 -0
- package/src/ward/seal.ts +174 -0
- package/src/ward/stance.ts +433 -0
- package/src/ward/ward.ts +378 -0
- package/index.js +0 -17
- package/nervur.js +0 -5
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The allowance. Every ask carries, in its signed body, the time it may still
|
|
3
|
+
// spend, in milliseconds. A being who says nothing gets her ward's default and
|
|
4
|
+
// never thinks about it; one who wants to say so passes a third argument, and
|
|
5
|
+
// is held to the ward's ceiling. The door reads it before anything is done
|
|
6
|
+
// under it and refuses a budget already gone, as one silence like every other
|
|
7
|
+
// refusal there. The sender bounds the whole of her ask to the same number,
|
|
8
|
+
// and a wait that ran out is the word late, never silence and never unreached.
|
|
9
|
+
//
|
|
10
|
+
// Each ask is bounded on its own. The time an arriving call has left does not
|
|
11
|
+
// bound the asks a being makes while answering it: attributing her onward ask
|
|
12
|
+
// to the arrival that caused it needs a context the ward cannot get without
|
|
13
|
+
// naming a runtime, and the ward names none. Every wait still ends.
|
|
14
|
+
// How wide the default is, and how wide she may ask for, are the ward's own —
|
|
15
|
+
// wider is more patient, and no peer can tell the difference except by being
|
|
16
|
+
// refused. A being who never learns the third parameter exists writes correct
|
|
17
|
+
// code forever under these. They are two numbers because the third argument
|
|
18
|
+
// goes both ways: less for the ask she wants back quickly, more for the one
|
|
19
|
+
// she knows is slow.
|
|
20
|
+
export const DEFAULT = { time: 30_000 };
|
|
21
|
+
export const CEILING = { time: 300_000 };
|
|
22
|
+
// What she asked for, held to what her ward allows. A being cannot mint budget
|
|
23
|
+
// nobody granted her: asking for a minute where the ward gives thirty seconds
|
|
24
|
+
// is thirty seconds, silently, because the ceiling is not hers to know.
|
|
25
|
+
// Asking for nothing at all is the default, which is the whole point.
|
|
26
|
+
export function allow(wanted, ceiling = CEILING, base = DEFAULT) {
|
|
27
|
+
// Floored first, then tested: a fraction of a millisecond is not a budget,
|
|
28
|
+
// and falls to the default like every other number that is not a positive
|
|
29
|
+
// whole one.
|
|
30
|
+
const whole = (n, fallback) => {
|
|
31
|
+
const w = typeof n === 'number' ? Math.floor(n) : NaN;
|
|
32
|
+
return Number.isFinite(w) && w > 0 ? w : fallback;
|
|
33
|
+
};
|
|
34
|
+
return { time: Math.min(whole(wanted?.time, base.time), ceiling.time) };
|
|
35
|
+
}
|
|
36
|
+
// Whether a budget has anything left to spend, and the one reader of it: the
|
|
37
|
+
// door calls this on arrival and nothing else asks the question anywhere. A
|
|
38
|
+
// time that is not a whole number above zero is not a budget that ran out, it
|
|
39
|
+
// is no budget at all, and the door refuses both the same way, since a peer
|
|
40
|
+
// learns nothing from the difference. `allow` never makes one, so only bytes
|
|
41
|
+
// off the road ever carry one.
|
|
42
|
+
export const spent = (a) => !(Number.isSafeInteger(a.time) && a.time > 0);
|
|
43
|
+
// What a wait that ran out comes back as. Its own value, held by nobody
|
|
44
|
+
// outside the ward, so no answer from any door can be mistaken for it.
|
|
45
|
+
export const LATE = Symbol('late');
|
|
46
|
+
// A wait with an end. This is the one thing the ward times, and it covers the
|
|
47
|
+
// whole of an ask, not only the wire: a relation that comes back round is
|
|
48
|
+
// stopped at its own lane, before a single byte is sealed, and a bound that
|
|
49
|
+
// only watched the wire would never see it. A timer is the only way to stop
|
|
50
|
+
// waiting and every language that runs Quo has one; no runtime is named here.
|
|
51
|
+
//
|
|
52
|
+
// `rang` is called the moment the bell rings, before anyone waiting on the
|
|
53
|
+
// wait is told: whoever holds work that has not started yet reads it and does
|
|
54
|
+
// not start it. What comes back late is not read, and what has not left yet
|
|
55
|
+
// does not leave.
|
|
56
|
+
export async function within(ms, work, rang) {
|
|
57
|
+
let timer;
|
|
58
|
+
const bell = new Promise((ring) => {
|
|
59
|
+
timer = setTimeout(() => {
|
|
60
|
+
rang?.();
|
|
61
|
+
ring(LATE);
|
|
62
|
+
}, ms);
|
|
63
|
+
});
|
|
64
|
+
try {
|
|
65
|
+
return await Promise.race([work, bell]);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
void work.then(() => { }, () => { }); // whatever it was, it is nobody's answer now
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const KEY = 32;
|
|
2
|
+
export declare const SIGNATURE = 64;
|
|
3
|
+
export declare const NONCE = 12;
|
|
4
|
+
export declare const TAG = 16;
|
|
5
|
+
export declare function hex(bytes: Uint8Array | ArrayBuffer): string;
|
|
6
|
+
export declare function unhex(text: string): Uint8Array;
|
|
7
|
+
export declare function concat(parts: Uint8Array[]): Uint8Array;
|
|
8
|
+
export declare function sameBytes(a: Uint8Array, b: Uint8Array): boolean;
|
|
9
|
+
export declare const smallOrder: (pk: Uint8Array) => boolean;
|
|
10
|
+
export declare const heldKeys: () => {
|
|
11
|
+
held: number;
|
|
12
|
+
bound: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function sha256(...parts: Uint8Array[]): Promise<Uint8Array>;
|
|
15
|
+
export type Pair = {
|
|
16
|
+
secret: Uint8Array;
|
|
17
|
+
pk: Uint8Array;
|
|
18
|
+
};
|
|
19
|
+
export declare function signingPair(seed: Uint8Array): Promise<Pair>;
|
|
20
|
+
export declare function sealingPair(seed: Uint8Array): Promise<Pair>;
|
|
21
|
+
export declare function sign(message: Uint8Array, secret: Uint8Array): Promise<Uint8Array>;
|
|
22
|
+
export declare function verify(message: Uint8Array, signature: Uint8Array, pk: Uint8Array): Promise<boolean>;
|
|
23
|
+
export declare function agree(secret: Uint8Array, peerPk: Uint8Array): Promise<Uint8Array>;
|
|
24
|
+
export declare function derive(secret: Uint8Array, label: Uint8Array, bytes: number): Promise<Uint8Array>;
|
|
25
|
+
export declare function encrypt(shared: Uint8Array, plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
|
|
26
|
+
export declare function decrypt(shared: Uint8Array, ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array>;
|
|
27
|
+
export declare function box(inside: Uint8Array, padlock: Uint8Array, seed: Uint8Array): Promise<{
|
|
28
|
+
bytes: Uint8Array;
|
|
29
|
+
ephemeral: Pair;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function unbox(bytes: Uint8Array, padlockSecret: Uint8Array): Promise<Uint8Array>;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Four algorithms, named once and never negotiated: Ed25519 signs, X25519
|
|
3
|
+
// agrees, SHA-256 hashes, AES-256-GCM encrypts with the key derived through
|
|
4
|
+
// HKDF-SHA-256 under a fixed label. All four live in `crypto.subtle`, so this
|
|
5
|
+
// takes no package and runs the same in a browser tab as on a server -- on
|
|
6
|
+
// any terrain that carries them. SHA-256, AES-GCM and HKDF are everywhere;
|
|
7
|
+
// the two curves are recent, and a terrain without them is a terrain no ward
|
|
8
|
+
// runs on. `test/floor.test.ts` names the floor and probes for it. Subtle is
|
|
9
|
+
// asynchronous, so everything here is.
|
|
10
|
+
//
|
|
11
|
+
// `crypto.subtle` is read at every use and never captured at load. A browser
|
|
12
|
+
// on a plain http:// origin has `crypto` without `subtle`, and a terrain may
|
|
13
|
+
// install one after this module is first imported; a reference taken here
|
|
14
|
+
// would turn either into an unreadable failure deep inside a key import.
|
|
15
|
+
const subtle = () => {
|
|
16
|
+
const s = globalThis.crypto?.subtle;
|
|
17
|
+
if (!s)
|
|
18
|
+
throw new Error('this terrain has no crypto.subtle: a ward needs a secure context');
|
|
19
|
+
return s;
|
|
20
|
+
};
|
|
21
|
+
export const KEY = 32;
|
|
22
|
+
export const SIGNATURE = 64;
|
|
23
|
+
export const NONCE = 12;
|
|
24
|
+
export const TAG = 16;
|
|
25
|
+
const SEAL_INFO = new TextEncoder().encode('quo-seal');
|
|
26
|
+
const SALT = new Uint8Array(0);
|
|
27
|
+
// A 32-byte secret plus a fixed prefix is the whole PKCS#8 wrapping for both curves.
|
|
28
|
+
const ED_SECRET = unhex('302e020100300506032b657004220420');
|
|
29
|
+
const X_SECRET = unhex('302e020100300506032b656e04220420');
|
|
30
|
+
const ED = { name: 'Ed25519' };
|
|
31
|
+
const X = { name: 'X25519' };
|
|
32
|
+
const HEX = Array.from({ length: 256 }, (_, at) => at.toString(16).padStart(2, '0'));
|
|
33
|
+
export function hex(bytes) {
|
|
34
|
+
let out = '';
|
|
35
|
+
for (const byte of new Uint8Array(bytes))
|
|
36
|
+
out += HEX[byte];
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
export function unhex(text) {
|
|
40
|
+
const out = new Uint8Array(text.length / 2);
|
|
41
|
+
for (let at = 0; at < out.length; at += 1)
|
|
42
|
+
out[at] = parseInt(text.slice(at * 2, at * 2 + 2), 16);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
export function concat(parts) {
|
|
46
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
47
|
+
let at = 0;
|
|
48
|
+
for (const part of parts) {
|
|
49
|
+
out.set(part, at);
|
|
50
|
+
at += part.length;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export function sameBytes(a, b) {
|
|
55
|
+
if (a.length !== b.length)
|
|
56
|
+
return false;
|
|
57
|
+
let diff = 0;
|
|
58
|
+
for (let at = 0; at < a.length; at += 1)
|
|
59
|
+
diff |= a[at] ^ b[at];
|
|
60
|
+
return diff === 0;
|
|
61
|
+
}
|
|
62
|
+
// The eight small-order points of Ed25519, in their canonical encoding. A
|
|
63
|
+
// public key among them verifies nothing. An encoding whose y coordinate is
|
|
64
|
+
// not reduced, y at or above the field's prime p, names one of the same
|
|
65
|
+
// points under another spelling, and a terrain's verify may accept a zero
|
|
66
|
+
// signature under it; the field has room for the spelling, so it is refused
|
|
67
|
+
// before the list is read.
|
|
68
|
+
const SMALL_ORDER = [
|
|
69
|
+
'0100000000000000000000000000000000000000000000000000000000000000',
|
|
70
|
+
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
71
|
+
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
72
|
+
'0000000000000000000000000000000000000000000000000000000000000080',
|
|
73
|
+
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05',
|
|
74
|
+
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
|
|
75
|
+
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85',
|
|
76
|
+
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa',
|
|
77
|
+
].map(unhex);
|
|
78
|
+
// y >= p, read little-endian with the sign bit masked off: the top byte is
|
|
79
|
+
// 0x7f under the mask, every middle byte is 0xff, and the low byte is at
|
|
80
|
+
// least 0xed, which is p's.
|
|
81
|
+
const unreduced = (pk) => (pk[31] & 0x7f) === 0x7f && pk[0] >= 0xed && pk.subarray(1, 31).every((b) => b === 0xff);
|
|
82
|
+
export const smallOrder = (pk) => unreduced(pk) || SMALL_ORDER.some((p) => sameBytes(p, pk));
|
|
83
|
+
const key32 = (value, what) => {
|
|
84
|
+
if (!(value instanceof Uint8Array) || value.length !== KEY)
|
|
85
|
+
throw new Error(`${what} is not a 32-byte key`);
|
|
86
|
+
return value;
|
|
87
|
+
};
|
|
88
|
+
const pkcs8 = (prefix, value, what) => concat([prefix, key32(value, what)]);
|
|
89
|
+
// Importing a key is the most expensive thing on the path of an ask, and most
|
|
90
|
+
// of the imports are the same key again: a ward signs every reply with the one
|
|
91
|
+
// key, opens every ask with the one padlock, and verifies a relation under the
|
|
92
|
+
// key it verified it under last time. Measured over a round trip, fifteen of
|
|
93
|
+
// the twenty-five imports were bytes already imported once.
|
|
94
|
+
//
|
|
95
|
+
// So an imported key is kept, by the bytes it was imported from. A CryptoKey
|
|
96
|
+
// cannot be changed once it exists, so handing the same one out twice is
|
|
97
|
+
// handing out what a second import would have built. Nothing here is a
|
|
98
|
+
// decision a peer can see: two wards that cache differently, or not at all,
|
|
99
|
+
// speak the same bytes.
|
|
100
|
+
//
|
|
101
|
+
// It is bounded, and that is not a detail. A relation mints a fresh key on
|
|
102
|
+
// every ask, so a ward that talked all day would otherwise hold a key for
|
|
103
|
+
// every ask it ever made. Past the bound the least recently used goes, which
|
|
104
|
+
// is the key of a relation that has fallen quiet, and importing it again
|
|
105
|
+
// costs what it cost the first time.
|
|
106
|
+
//
|
|
107
|
+
// The secret keys in here are the ones the partition already holds in this
|
|
108
|
+
// process, as seeds. The cache is another shape of what the ward is already
|
|
109
|
+
// standing on, and never a second place a secret comes from.
|
|
110
|
+
const KEYS = 512;
|
|
111
|
+
const imported = new Map();
|
|
112
|
+
const keep = (id, make) => {
|
|
113
|
+
const had = imported.get(id);
|
|
114
|
+
if (had !== undefined) {
|
|
115
|
+
imported.delete(id); // and set again below: the most recently used goes last
|
|
116
|
+
imported.set(id, had);
|
|
117
|
+
return had;
|
|
118
|
+
}
|
|
119
|
+
const made = make();
|
|
120
|
+
// A key that would not import is not kept: the next call asks subtle again
|
|
121
|
+
// and hears the same refusal, rather than reading one this cache remembered.
|
|
122
|
+
// Node takes any thirty-two bytes as a public key and finds out at verify,
|
|
123
|
+
// so nothing here reaches this line; a terrain that checks the point at the
|
|
124
|
+
// import does, and a refusal it remembered would be a relation killed for
|
|
125
|
+
// good by one bad arrival.
|
|
126
|
+
made.catch(() => imported.delete(id));
|
|
127
|
+
imported.set(id, made);
|
|
128
|
+
// One in, at most one out: a map keeps what was put in the order it was put,
|
|
129
|
+
// so the first key it names is the one used longest ago.
|
|
130
|
+
if (imported.size > KEYS)
|
|
131
|
+
imported.delete(imported.keys().next().value);
|
|
132
|
+
return made;
|
|
133
|
+
};
|
|
134
|
+
// How many imported keys are held, and the bound they are held under. Nothing
|
|
135
|
+
// in the ward reads either: they are here to be looked at, and for the suite
|
|
136
|
+
// that holds the bound to what it says.
|
|
137
|
+
export const heldKeys = () => ({ held: imported.size, bound: KEYS });
|
|
138
|
+
const secretKey = (alg, prefix, value, what, uses) => {
|
|
139
|
+
const bytes = pkcs8(prefix, value, what);
|
|
140
|
+
return keep(`${alg.name}|${uses.join('+')}|${hex(bytes)}`, () => subtle().importKey('pkcs8', bytes, alg, true, uses));
|
|
141
|
+
};
|
|
142
|
+
const publicKey = (alg, value, what, uses) => {
|
|
143
|
+
const bytes = key32(value, what);
|
|
144
|
+
return keep(`${alg.name}|${uses.join('+')}|pk|${hex(bytes)}`, () => subtle().importKey('raw', bytes, alg, true, uses));
|
|
145
|
+
};
|
|
146
|
+
// Subtle exports the public half of a private key only through a JWK, where
|
|
147
|
+
// `x` is the 32 raw bytes in base64url. The answer is a fact about the key and
|
|
148
|
+
// never changes, so it is kept beside the key it was read from and goes when
|
|
149
|
+
// the key does.
|
|
150
|
+
const publics = new WeakMap();
|
|
151
|
+
function rawPublic(secret) {
|
|
152
|
+
const had = publics.get(secret);
|
|
153
|
+
if (had !== undefined)
|
|
154
|
+
return had;
|
|
155
|
+
const read = (async () => {
|
|
156
|
+
const jwk = await subtle().exportKey('jwk', secret);
|
|
157
|
+
const binary = atob(jwk.x.replaceAll('-', '+').replaceAll('_', '/'));
|
|
158
|
+
const out = new Uint8Array(binary.length);
|
|
159
|
+
for (let at = 0; at < binary.length; at += 1)
|
|
160
|
+
out[at] = binary.charCodeAt(at);
|
|
161
|
+
return out;
|
|
162
|
+
})();
|
|
163
|
+
publics.set(secret, read);
|
|
164
|
+
return read;
|
|
165
|
+
}
|
|
166
|
+
export async function sha256(...parts) {
|
|
167
|
+
return new Uint8Array(await subtle().digest('SHA-256', concat(parts)));
|
|
168
|
+
}
|
|
169
|
+
// Both halves are copies. The seed and the public key are kept behind the two
|
|
170
|
+
// caches above, and a pair is handed to whoever asked for it: what she does
|
|
171
|
+
// with the bytes in her hand is hers, and must not reach what the next caller
|
|
172
|
+
// is given.
|
|
173
|
+
export async function signingPair(seed) {
|
|
174
|
+
const secret = await secretKey(ED, ED_SECRET, seed, 'seed', ['sign']);
|
|
175
|
+
return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
|
|
176
|
+
}
|
|
177
|
+
export async function sealingPair(seed) {
|
|
178
|
+
const secret = await secretKey(X, X_SECRET, seed, 'seed', ['deriveBits']);
|
|
179
|
+
return { secret: Uint8Array.from(seed), pk: Uint8Array.from(await rawPublic(secret)) };
|
|
180
|
+
}
|
|
181
|
+
export async function sign(message, secret) {
|
|
182
|
+
const key = await secretKey(ED, ED_SECRET, secret, 'secret', ['sign']);
|
|
183
|
+
return new Uint8Array(await subtle().sign(ED, key, message));
|
|
184
|
+
}
|
|
185
|
+
export async function verify(message, signature, pk) {
|
|
186
|
+
if (!(signature instanceof Uint8Array) || signature.length !== SIGNATURE)
|
|
187
|
+
return false;
|
|
188
|
+
if (!(pk instanceof Uint8Array) || pk.length !== KEY || smallOrder(pk))
|
|
189
|
+
return false;
|
|
190
|
+
try {
|
|
191
|
+
return await subtle().verify(ED, await publicKey(ED, pk, 'pk', ['verify']), signature, message);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export async function agree(secret, peerPk) {
|
|
198
|
+
const key = await secretKey(X, X_SECRET, secret, 'secret', ['deriveBits']);
|
|
199
|
+
const peer = await publicKey(X, peerPk, 'padlock', []);
|
|
200
|
+
const shared = new Uint8Array(await subtle().deriveBits({ name: X.name, public: peer }, key, KEY * 8));
|
|
201
|
+
if (shared.every((b) => b === 0))
|
|
202
|
+
throw new Error('dead agreement'); // a padlock that was not a real key
|
|
203
|
+
return shared;
|
|
204
|
+
}
|
|
205
|
+
// HKDF-SHA-256: bytes from a secret, under a label that says what they are
|
|
206
|
+
// for. No salt, because the secret handed in is already a secret of full
|
|
207
|
+
// strength and the label is what keeps one use apart from another. Every
|
|
208
|
+
// label in this kit is spelled where it is used, never here: this is
|
|
209
|
+
// arithmetic and a label is a decision.
|
|
210
|
+
export async function derive(secret, label, bytes) {
|
|
211
|
+
const material = await subtle().importKey('raw', secret, 'HKDF', false, ['deriveBits']);
|
|
212
|
+
return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: label }, material, bytes * 8));
|
|
213
|
+
}
|
|
214
|
+
// One HKDF-SHA-256 yields the AES key and the nonce together. The nonce needs
|
|
215
|
+
// no randomness of its own: the key it pairs with is fresh on every message.
|
|
216
|
+
async function cipherKey(shared, use) {
|
|
217
|
+
const out = await derive(shared, SEAL_INFO, KEY + NONCE);
|
|
218
|
+
return { key: await subtle().importKey('raw', out.subarray(0, KEY), 'AES-GCM', false, [use]), nonce: out.subarray(KEY) };
|
|
219
|
+
}
|
|
220
|
+
// The additional authenticated data is the ephemeral public key: the one thing outside the seal, bound to it.
|
|
221
|
+
export async function encrypt(shared, plaintext, aad) {
|
|
222
|
+
const { key, nonce } = await cipherKey(shared, 'encrypt');
|
|
223
|
+
return new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad'), tagLength: TAG * 8 }, key, plaintext));
|
|
224
|
+
}
|
|
225
|
+
export async function decrypt(shared, ciphertext, aad) {
|
|
226
|
+
if (ciphertext.length < TAG)
|
|
227
|
+
throw new Error('short input');
|
|
228
|
+
const { key, nonce } = await cipherKey(shared, 'decrypt');
|
|
229
|
+
return new Uint8Array(await subtle().decrypt({ name: 'AES-GCM', iv: nonce, additionalData: key32(aad, 'aad'), tagLength: TAG * 8 }, key, ciphertext));
|
|
230
|
+
}
|
|
231
|
+
// A box: an ephemeral X25519 pk outside, one ciphertext sealed to the
|
|
232
|
+
// padlock. The answer to a box is a box sealed to that ephemeral pk, so the
|
|
233
|
+
// sender keeps the ephemeral secret until the answer comes.
|
|
234
|
+
export async function box(inside, padlock, seed) {
|
|
235
|
+
const ephemeral = await sealingPair(seed);
|
|
236
|
+
const shared = await agree(ephemeral.secret, padlock);
|
|
237
|
+
return { bytes: concat([ephemeral.pk, await encrypt(shared, inside, ephemeral.pk)]), ephemeral };
|
|
238
|
+
}
|
|
239
|
+
export async function unbox(bytes, padlockSecret) {
|
|
240
|
+
if (bytes.length <= KEY)
|
|
241
|
+
throw new Error('short input');
|
|
242
|
+
const ephemeralPk = bytes.subarray(0, KEY);
|
|
243
|
+
const shared = await agree(padlockSecret, ephemeralPk);
|
|
244
|
+
return decrypt(shared, bytes.subarray(KEY), ephemeralPk);
|
|
245
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Cells } from '../being/types.ts';
|
|
2
|
+
export declare const DEPTH = 64;
|
|
3
|
+
export declare const BREADTH: number;
|
|
4
|
+
export declare const cellFault: (v: unknown, path: string) => string | null;
|
|
5
|
+
export declare const unkeepable: (k: string) => boolean;
|
|
6
|
+
export declare const guardCells: (cells: Cells, wrote?: () => void) => Cells;
|
|
7
|
+
export declare const unguarded: (cells: Cells) => Cells;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// How deep a value may nest. Every walk over a value in the kit recurses,
|
|
2
|
+
// and a peer chooses the depth of what she answers; a bound keeps a hostile
|
|
3
|
+
// reply from ending a walk in a stack overflow instead of a refusal.
|
|
4
|
+
export const DEPTH = 64;
|
|
5
|
+
// How many values one written out may come to. Depth alone does not bound
|
|
6
|
+
// what a value costs to keep: one subvalue may sit under two keys, and
|
|
7
|
+
// nesting that forty levels deep is inside the depth bound and is a trillion
|
|
8
|
+
// values once written down. What is walked here is the object graph, which is
|
|
9
|
+
// small; what a harbor keeps is the tree it expands to, which is not. So the
|
|
10
|
+
// walk counts the tree while it proves the graph, and refuses a value nobody
|
|
11
|
+
// could write down. This is a bound on what a being holds, so it is this
|
|
12
|
+
// kit's number and no word of the spec.
|
|
13
|
+
export const BREADTH = 1 << 20;
|
|
14
|
+
// Text a harbor can write down: every surrogate in a pair. A lone one is a
|
|
15
|
+
// code unit with no character, which JSON escapes and UTF-8 cannot encode at
|
|
16
|
+
// all, so a harbor that keeps strings as bytes loses it and one that keeps
|
|
17
|
+
// the object does not. I-JSON says a string is text, and this is that rule
|
|
18
|
+
// where the value is written rather than where it is read.
|
|
19
|
+
const WELL_FORMED = /^(?:[^\uD800-\uDFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF])*$/;
|
|
20
|
+
// I-JSON, all the way down. A number that JSON cannot write is not a number
|
|
21
|
+
// a harbor can keep, a key on the prototype is not a key she wrote, and a
|
|
22
|
+
// hole in a list, or a key named `__proto__`, is a thing JSON writes one way
|
|
23
|
+
// and a runtime reads another.
|
|
24
|
+
//
|
|
25
|
+
// A fault is a sentence; anything else is the count of values under this one,
|
|
26
|
+
// itself included. A value already counted is a value already proven, so a
|
|
27
|
+
// graph that shares is walked once per node rather than once per path.
|
|
28
|
+
function walk(v, path, seen, sized, depth) {
|
|
29
|
+
if (v === null || typeof v === 'boolean')
|
|
30
|
+
return 1;
|
|
31
|
+
if (typeof v === 'string')
|
|
32
|
+
return WELL_FORMED.test(v) ? 1 : `${path} carries a lone surrogate, which is no text a harbor can write down`;
|
|
33
|
+
if (typeof v === 'number') {
|
|
34
|
+
if (!Number.isFinite(v))
|
|
35
|
+
return `${path} is ${String(v)}, which no harbor can write down`;
|
|
36
|
+
// Minus zero is a number JSON writes as `0` and reads back as `0`. A
|
|
37
|
+
// harbor that keeps values would hand her the sign she wrote and one that
|
|
38
|
+
// writes JSON would not, and she is never told which she is standing in.
|
|
39
|
+
return Object.is(v, -0) ? `${path} is minus zero, which comes back as zero from every harbor that writes JSON` : 1;
|
|
40
|
+
}
|
|
41
|
+
if (typeof v !== 'object')
|
|
42
|
+
return `${path} is a ${typeof v}, which is not a value`;
|
|
43
|
+
if (seen.has(v))
|
|
44
|
+
return `${path} refers back to itself`;
|
|
45
|
+
const counted = sized.get(v);
|
|
46
|
+
if (counted !== undefined)
|
|
47
|
+
return counted;
|
|
48
|
+
if (depth >= DEPTH)
|
|
49
|
+
return `${path} is nested past ${DEPTH} levels, which no harbor can keep`;
|
|
50
|
+
seen.add(v);
|
|
51
|
+
try {
|
|
52
|
+
let size = 1;
|
|
53
|
+
const under = (where, x) => {
|
|
54
|
+
const r = walk(x, where, seen, sized, depth + 1);
|
|
55
|
+
if (typeof r === 'string')
|
|
56
|
+
return r;
|
|
57
|
+
size += r;
|
|
58
|
+
return size > BREADTH ? `${path} writes out to more than ${BREADTH} values, which no harbor can keep` : null;
|
|
59
|
+
};
|
|
60
|
+
if (Array.isArray(v)) {
|
|
61
|
+
for (let i = 0; i < v.length; i += 1) {
|
|
62
|
+
if (!(i in v))
|
|
63
|
+
return `${path}[${i}] is a hole, which no harbor can write down`;
|
|
64
|
+
const f = under(`${path}[${i}]`, v[i]);
|
|
65
|
+
if (f)
|
|
66
|
+
return f;
|
|
67
|
+
}
|
|
68
|
+
sized.set(v, size);
|
|
69
|
+
return size;
|
|
70
|
+
}
|
|
71
|
+
const proto = Object.getPrototypeOf(v);
|
|
72
|
+
if (proto !== Object.prototype && proto !== null)
|
|
73
|
+
return `${path} is a ${(v).constructor?.name ?? 'object'}, which is not a value`;
|
|
74
|
+
for (const k of Object.keys(v)) {
|
|
75
|
+
if (k === '__proto__')
|
|
76
|
+
return `${path}.__proto__ is a key no harbor can keep`;
|
|
77
|
+
const f = under(`${path}.${k}`, v[k]);
|
|
78
|
+
if (f)
|
|
79
|
+
return f;
|
|
80
|
+
}
|
|
81
|
+
sized.set(v, size);
|
|
82
|
+
return size;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
seen.delete(v);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export const cellFault = (v, path) => {
|
|
89
|
+
const out = walk(v, path, new Set(), new Map(), 0);
|
|
90
|
+
return typeof out === 'string' ? out : null;
|
|
91
|
+
};
|
|
92
|
+
// The three keys at the root of her cells that are the ward's: it writes
|
|
93
|
+
// them, she reads them, and a write of hers there is refused like a non-value.
|
|
94
|
+
const WARDS = new Set(['standings', 'occupants', 'class']);
|
|
95
|
+
// The guard is one proxy at the root and one for every container read through
|
|
96
|
+
// it, so a write nested three deep is refused the same way a write at the top
|
|
97
|
+
// is. Wrappers are remembered, so reading the same array twice is the same
|
|
98
|
+
// object twice and a being may still compare what she holds.
|
|
99
|
+
const wrapped = new WeakMap();
|
|
100
|
+
const targets = new WeakMap();
|
|
101
|
+
const guards = new WeakSet();
|
|
102
|
+
// A name no partition can hold, wherever it appears. It is here beside the
|
|
103
|
+
// guard that refuses it, because the ward refuses it too: a relation named
|
|
104
|
+
// this one would be written into the bind table and then throw on her cells,
|
|
105
|
+
// which is half a relation and a throw where a null is promised. One rule,
|
|
106
|
+
// one place, read by the guard and by the stance.
|
|
107
|
+
export const unkeepable = (k) => k === '__proto__';
|
|
108
|
+
// A key a write may land on: a string that is not `__proto__`, and not one
|
|
109
|
+
// of the ward's at the root. A symbol key is a thing JSON never writes.
|
|
110
|
+
const refuse = (why) => {
|
|
111
|
+
throw new TypeError(`cells hold values: ${why}`);
|
|
112
|
+
};
|
|
113
|
+
function keyFault(t, k, v, path, root) {
|
|
114
|
+
if (typeof k !== 'string')
|
|
115
|
+
refuse(`${path} takes no symbol key`);
|
|
116
|
+
if (unkeepable(k))
|
|
117
|
+
refuse(`${path}.__proto__ is a key no harbor can keep`);
|
|
118
|
+
if (root && WARDS.has(k))
|
|
119
|
+
refuse(`${path}.${k} is the ward's to write`);
|
|
120
|
+
// A list grows by one at its end, or it has holes JSON cannot write. A
|
|
121
|
+
// push sets the slot at its length and then the length: both pass. A
|
|
122
|
+
// length set past what she wrote, or a slot beyond it, would leave holes.
|
|
123
|
+
if (Array.isArray(t)) {
|
|
124
|
+
if (k === 'length') {
|
|
125
|
+
if (typeof v !== 'number' || v > t.length)
|
|
126
|
+
refuse(`${path}.length set past what she wrote would leave holes`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const i = Number(k);
|
|
130
|
+
if (Number.isInteger(i) && i > t.length)
|
|
131
|
+
refuse(`${path}[${i}] would leave a hole`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function guard(target, path, wrote, root = false) {
|
|
135
|
+
if (guards.has(target))
|
|
136
|
+
return target;
|
|
137
|
+
const had = wrapped.get(target);
|
|
138
|
+
if (had)
|
|
139
|
+
return had;
|
|
140
|
+
const p = new Proxy(target, {
|
|
141
|
+
get(t, k, r) {
|
|
142
|
+
const v = Reflect.get(t, k, r);
|
|
143
|
+
// A container reached through her cells is part of her cells.
|
|
144
|
+
return v !== null && typeof v === 'object' && !ArrayBuffer.isView(v) ? guard(v, `${path}.${String(k)}`, wrote) : v;
|
|
145
|
+
},
|
|
146
|
+
set(t, k, v, r) {
|
|
147
|
+
keyFault(t, k, v, path, root);
|
|
148
|
+
const f = cellFault(v, `${path}.${String(k)}`);
|
|
149
|
+
if (f)
|
|
150
|
+
refuse(f);
|
|
151
|
+
const ok = Reflect.set(t, k, v, r);
|
|
152
|
+
wrote();
|
|
153
|
+
return ok;
|
|
154
|
+
},
|
|
155
|
+
defineProperty(t, k, d) {
|
|
156
|
+
if (!('value' in d))
|
|
157
|
+
refuse(`${path}.${String(k)} is an accessor, which no harbor can keep`);
|
|
158
|
+
keyFault(t, k, d.value, path, root);
|
|
159
|
+
const f = cellFault(d.value, `${path}.${String(k)}`);
|
|
160
|
+
if (f)
|
|
161
|
+
refuse(f);
|
|
162
|
+
const ok = Reflect.defineProperty(t, k, d);
|
|
163
|
+
wrote();
|
|
164
|
+
return ok;
|
|
165
|
+
},
|
|
166
|
+
deleteProperty(t, k) {
|
|
167
|
+
if (root && typeof k === 'string' && WARDS.has(k))
|
|
168
|
+
refuse(`${path}.${k} is the ward's to keep`);
|
|
169
|
+
const ok = Reflect.deleteProperty(t, k);
|
|
170
|
+
wrote();
|
|
171
|
+
return ok;
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
wrapped.set(target, p);
|
|
175
|
+
targets.set(p, target);
|
|
176
|
+
guards.add(p);
|
|
177
|
+
return p;
|
|
178
|
+
}
|
|
179
|
+
// Her cells, guarded. Called once per being at boot; the guarded object is
|
|
180
|
+
// what goes into the partition and what the stance hands her, so there is no
|
|
181
|
+
// second door onto the same cells. Every write that passes says so to the
|
|
182
|
+
// ward, which says so to its harbor.
|
|
183
|
+
export const guardCells = (cells, wrote = () => { }) => guard(cells, 'cells', wrote, true);
|
|
184
|
+
// The cells behind the guard, for the ward alone: the one writer of the
|
|
185
|
+
// three keys that are its own.
|
|
186
|
+
export const unguarded = (cells) => (targets.get(cells) ?? cells);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Asker, BeingLike, JsonObject, OccupantRecord } from '../being/types.ts';
|
|
2
|
+
import type { Heirs } from './heirs.ts';
|
|
3
|
+
import { type ReplyPayload, type WardKey } from './seal.ts';
|
|
4
|
+
export type Door = {
|
|
5
|
+
key: string;
|
|
6
|
+
being: BeingLike;
|
|
7
|
+
cells: {
|
|
8
|
+
occupants: Record<string, OccupantRecord>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export type Judged = {
|
|
12
|
+
bytes: Uint8Array;
|
|
13
|
+
heard: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function answered(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
|
|
16
|
+
export declare function arrive(door: Door, asker: Asker, method: string | undefined, args: JsonObject, bound: boolean): Promise<ReplyPayload>;
|
|
17
|
+
export declare function seen(door: Door, asker: Asker): Promise<string | null>;
|
|
18
|
+
export declare function makeDoor(key: WardKey, heirs: Heirs, doors: Map<string, Door>, publicKey: () => string | null, random: (n: number) => Uint8Array, keep: () => Promise<boolean>): (bytes: Uint8Array) => Promise<Judged>;
|