@agentshouse/envelope 0.1.0-alpha.1
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/README.md +32 -0
- package/index.d.ts +17 -0
- package/index.js +75 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# @agentshouse/envelope
|
|
2
|
+
|
|
3
|
+
The AES-256-GCM purpose-bound envelope agents.house seals recoverable secrets
|
|
4
|
+
with. Every value gets a fresh data key and nonce, an authentication tag, and an
|
|
5
|
+
authenticated context that binds it to its immutable owning object and one
|
|
6
|
+
purpose the caller supplies. Key management stays behind a port the caller
|
|
7
|
+
implements, so the package names no purpose and no key-management provider.
|
|
8
|
+
|
|
9
|
+
## Use
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { open, seal } = require('@agentshouse/envelope');
|
|
13
|
+
|
|
14
|
+
const envelope = await seal(keys, 'provider-token', objectId, plaintext);
|
|
15
|
+
const token = await open(keys, 'provider-token', objectId, envelope, (plaintext) =>
|
|
16
|
+
plaintext.toString('utf8'),
|
|
17
|
+
);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`keys` implements `Keys`: `wrap(dataKey, authenticatedData)` and
|
|
21
|
+
`unwrap(wrappedDataKey, authenticatedData)`. The authenticated context is the
|
|
22
|
+
canonical JSON `{"object":…,"purpose":…,"version":1}` and is passed to both.
|
|
23
|
+
`objectId` is a UUID.
|
|
24
|
+
|
|
25
|
+
The `Envelope` is five components the caller stores as they are: `version`,
|
|
26
|
+
`wrappedDataKey`, `nonce`, `tag` and `ciphertext`. `open` refuses a malformed or
|
|
27
|
+
forged envelope, and one opened under another object or purpose, with
|
|
28
|
+
`IntegrityError`. `valid(envelope)` is the check of the stored form `open`
|
|
29
|
+
makes before it asks the port, so a caller can refuse a malformed record when it
|
|
30
|
+
reads one. An error the port throws passes through unchanged, so the caller maps
|
|
31
|
+
its own refusals and unavailability. The plaintext exists only inside the
|
|
32
|
+
callback and is wiped when it returns.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface Keys {
|
|
2
|
+
wrap(dataKey: Uint8Array, authenticatedData: Uint8Array): Promise<Uint8Array>;
|
|
3
|
+
unwrap(wrappedDataKey: Uint8Array, authenticatedData: Uint8Array): Promise<Uint8Array>;
|
|
4
|
+
}
|
|
5
|
+
export interface Envelope {
|
|
6
|
+
readonly version: number;
|
|
7
|
+
readonly wrappedDataKey: Buffer;
|
|
8
|
+
readonly nonce: Buffer;
|
|
9
|
+
readonly tag: Buffer;
|
|
10
|
+
readonly ciphertext: Buffer;
|
|
11
|
+
}
|
|
12
|
+
export declare class IntegrityError extends Error {
|
|
13
|
+
constructor();
|
|
14
|
+
}
|
|
15
|
+
export declare function valid(envelope: Envelope): boolean;
|
|
16
|
+
export declare function seal(keys: Keys, purpose: string, objectId: string, plaintext: Uint8Array): Promise<Envelope>;
|
|
17
|
+
export declare function open<T>(keys: Keys, purpose: string, objectId: string, envelope: Envelope, work: (plaintext: Buffer) => Promise<T> | T): Promise<T>;
|
package/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IntegrityError = void 0;
|
|
4
|
+
exports.valid = valid;
|
|
5
|
+
exports.seal = seal;
|
|
6
|
+
exports.open = open;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const VERSION = 1;
|
|
9
|
+
const DATA_KEY_BYTES = 32;
|
|
10
|
+
const NONCE_BYTES = 12;
|
|
11
|
+
const TAG_BYTES = 16;
|
|
12
|
+
const OBJECT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13
|
+
class IntegrityError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super('envelope_integrity');
|
|
16
|
+
this.name = 'IntegrityError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.IntegrityError = IntegrityError;
|
|
20
|
+
function authenticatedContext(purpose, objectId) {
|
|
21
|
+
if (!OBJECT_ID.test(objectId))
|
|
22
|
+
throw new IntegrityError();
|
|
23
|
+
return Buffer.from(JSON.stringify({ object: objectId, purpose, version: VERSION }), 'utf8');
|
|
24
|
+
}
|
|
25
|
+
function length(component) {
|
|
26
|
+
return component instanceof Uint8Array ? component.length : -1;
|
|
27
|
+
}
|
|
28
|
+
function valid(envelope) {
|
|
29
|
+
return (envelope.version === VERSION &&
|
|
30
|
+
length(envelope.wrappedDataKey) > 0 &&
|
|
31
|
+
length(envelope.nonce) === NONCE_BYTES &&
|
|
32
|
+
length(envelope.tag) === TAG_BYTES &&
|
|
33
|
+
length(envelope.ciphertext) > 0);
|
|
34
|
+
}
|
|
35
|
+
async function seal(keys, purpose, objectId, plaintext) {
|
|
36
|
+
if (plaintext.length < 1)
|
|
37
|
+
throw new IntegrityError();
|
|
38
|
+
const aad = authenticatedContext(purpose, objectId);
|
|
39
|
+
const dataKey = (0, node_crypto_1.randomBytes)(DATA_KEY_BYTES);
|
|
40
|
+
try {
|
|
41
|
+
const nonce = (0, node_crypto_1.randomBytes)(NONCE_BYTES);
|
|
42
|
+
const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', dataKey, nonce);
|
|
43
|
+
cipher.setAAD(aad);
|
|
44
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
45
|
+
const tag = cipher.getAuthTag();
|
|
46
|
+
const wrappedDataKey = Buffer.from(await keys.wrap(dataKey, aad));
|
|
47
|
+
return { version: VERSION, wrappedDataKey, nonce, tag, ciphertext };
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
dataKey.fill(0);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function open(keys, purpose, objectId, envelope, work) {
|
|
54
|
+
if (!valid(envelope))
|
|
55
|
+
throw new IntegrityError();
|
|
56
|
+
const aad = authenticatedContext(purpose, objectId);
|
|
57
|
+
const dataKey = Buffer.from(await keys.unwrap(envelope.wrappedDataKey, aad));
|
|
58
|
+
let opened;
|
|
59
|
+
try {
|
|
60
|
+
try {
|
|
61
|
+
const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', dataKey, envelope.nonce);
|
|
62
|
+
decipher.setAAD(aad);
|
|
63
|
+
decipher.setAuthTag(envelope.tag);
|
|
64
|
+
opened = Buffer.concat([decipher.update(envelope.ciphertext), decipher.final()]);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new IntegrityError();
|
|
68
|
+
}
|
|
69
|
+
return await work(opened);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
dataKey.fill(0);
|
|
73
|
+
opened?.fill(0);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentshouse/envelope",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "AES-256-GCM purpose-bound envelope over a key-management port: a fresh data key and nonce per value, bound to its owning object and one caller purpose.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=24"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"default": "./index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"*.js",
|
|
19
|
+
"*.d.ts",
|
|
20
|
+
"README.md"
|
|
21
|
+
]
|
|
22
|
+
}
|