@membox-cloud/membox 0.1.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/README.md +169 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +159 -0
- package/dist/src/api/account.d.ts +3 -0
- package/dist/src/api/account.js +3 -0
- package/dist/src/api/client.d.ts +21 -0
- package/dist/src/api/client.js +107 -0
- package/dist/src/api/device-flow.d.ts +9 -0
- package/dist/src/api/device-flow.js +55 -0
- package/dist/src/api/devices.d.ts +10 -0
- package/dist/src/api/devices.js +24 -0
- package/dist/src/api/recovery.d.ts +5 -0
- package/dist/src/api/recovery.js +9 -0
- package/dist/src/api/sync.d.ts +9 -0
- package/dist/src/api/sync.js +22 -0
- package/dist/src/cli/bootstrap.d.ts +37 -0
- package/dist/src/cli/bootstrap.js +326 -0
- package/dist/src/cli/grants.d.ts +8 -0
- package/dist/src/cli/grants.js +76 -0
- package/dist/src/cli/helpers.d.ts +6 -0
- package/dist/src/cli/helpers.js +13 -0
- package/dist/src/cli/passphrase-input.d.ts +11 -0
- package/dist/src/cli/passphrase-input.js +94 -0
- package/dist/src/cli/pause.d.ts +2 -0
- package/dist/src/cli/pause.js +29 -0
- package/dist/src/cli/provisioning.d.ts +3 -0
- package/dist/src/cli/provisioning.js +30 -0
- package/dist/src/cli/pull.d.ts +31 -0
- package/dist/src/cli/pull.js +142 -0
- package/dist/src/cli/restore.d.ts +12 -0
- package/dist/src/cli/restore.js +90 -0
- package/dist/src/cli/setup.d.ts +17 -0
- package/dist/src/cli/setup.js +209 -0
- package/dist/src/cli/status.d.ts +1 -0
- package/dist/src/cli/status.js +31 -0
- package/dist/src/cli/sync.d.ts +4 -0
- package/dist/src/cli/sync.js +124 -0
- package/dist/src/cli/unlock.d.ts +19 -0
- package/dist/src/cli/unlock.js +153 -0
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +12 -0
- package/dist/src/constants.d.ts +23 -0
- package/dist/src/constants.js +27 -0
- package/dist/src/contract-types.d.ts +301 -0
- package/dist/src/contract-types.js +52 -0
- package/dist/src/crypto/aes-gcm.d.ts +29 -0
- package/dist/src/crypto/aes-gcm.js +44 -0
- package/dist/src/crypto/device-keys.d.ts +18 -0
- package/dist/src/crypto/device-keys.js +25 -0
- package/dist/src/crypto/grant.d.ts +29 -0
- package/dist/src/crypto/grant.js +87 -0
- package/dist/src/crypto/kdf.d.ts +16 -0
- package/dist/src/crypto/kdf.js +24 -0
- package/dist/src/crypto/keys.d.ts +14 -0
- package/dist/src/crypto/keys.js +35 -0
- package/dist/src/crypto/manifest.d.ts +25 -0
- package/dist/src/crypto/manifest.js +41 -0
- package/dist/src/crypto/recovery.d.ts +16 -0
- package/dist/src/crypto/recovery.js +94 -0
- package/dist/src/crypto/types.d.ts +34 -0
- package/dist/src/crypto/types.js +1 -0
- package/dist/src/debug-logger.d.ts +32 -0
- package/dist/src/debug-logger.js +108 -0
- package/dist/src/hooks/gateway-lifecycle.d.ts +6 -0
- package/dist/src/hooks/gateway-lifecycle.js +40 -0
- package/dist/src/hooks/prompt-inject.d.ts +7 -0
- package/dist/src/hooks/prompt-inject.js +18 -0
- package/dist/src/store/keychain.d.ts +26 -0
- package/dist/src/store/keychain.js +151 -0
- package/dist/src/store/local-state.d.ts +27 -0
- package/dist/src/store/local-state.js +47 -0
- package/dist/src/store/managed-unlock.d.ts +8 -0
- package/dist/src/store/managed-unlock.js +46 -0
- package/dist/src/store/pending-setup.d.ts +23 -0
- package/dist/src/store/pending-setup.js +32 -0
- package/dist/src/store/session.d.ts +13 -0
- package/dist/src/store/session.js +28 -0
- package/dist/src/sync/auto-sync.d.ts +12 -0
- package/dist/src/sync/auto-sync.js +82 -0
- package/dist/src/sync/conflict.d.ts +24 -0
- package/dist/src/sync/conflict.js +92 -0
- package/dist/src/sync/diff.d.ts +25 -0
- package/dist/src/sync/diff.js +75 -0
- package/dist/src/sync/downloader.d.ts +16 -0
- package/dist/src/sync/downloader.js +73 -0
- package/dist/src/sync/scanner.d.ts +12 -0
- package/dist/src/sync/scanner.js +52 -0
- package/dist/src/sync/state.d.ts +4 -0
- package/dist/src/sync/state.js +22 -0
- package/dist/src/sync/uploader.d.ts +20 -0
- package/dist/src/sync/uploader.js +86 -0
- package/dist/src/tools/grants-approve-pending.d.ts +17 -0
- package/dist/src/tools/grants-approve-pending.js +50 -0
- package/dist/src/tools/pull.d.ts +31 -0
- package/dist/src/tools/pull.js +71 -0
- package/dist/src/tools/restore.d.ts +31 -0
- package/dist/src/tools/restore.js +96 -0
- package/dist/src/tools/result.d.ts +7 -0
- package/dist/src/tools/result.js +6 -0
- package/dist/src/tools/secret-file.d.ts +10 -0
- package/dist/src/tools/secret-file.js +37 -0
- package/dist/src/tools/setup-finish.d.ts +36 -0
- package/dist/src/tools/setup-finish.js +108 -0
- package/dist/src/tools/setup-poll.d.ts +27 -0
- package/dist/src/tools/setup-poll.js +83 -0
- package/dist/src/tools/setup-start.d.ts +18 -0
- package/dist/src/tools/setup-start.js +49 -0
- package/dist/src/tools/status.d.ts +17 -0
- package/dist/src/tools/status.js +40 -0
- package/dist/src/tools/sync.d.ts +17 -0
- package/dist/src/tools/sync.js +49 -0
- package/dist/src/tools/unlock-secret.d.ts +42 -0
- package/dist/src/tools/unlock-secret.js +87 -0
- package/dist/src/tools/unlock.d.ts +25 -0
- package/dist/src/tools/unlock.js +72 -0
- package/openclaw.plugin.json +16 -0
- package/package.json +35 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ed25519, x25519 } from "@noble/curves/ed25519";
|
|
2
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
3
|
+
/** Generate a fresh device key pair (Ed25519 for signing, X25519 for KEX). */
|
|
4
|
+
export function generateDeviceKeyPair() {
|
|
5
|
+
const edPriv = randomBytes(32);
|
|
6
|
+
const edPub = ed25519.getPublicKey(edPriv);
|
|
7
|
+
const xPriv = randomBytes(32);
|
|
8
|
+
const xPub = x25519.getPublicKey(xPriv);
|
|
9
|
+
return {
|
|
10
|
+
ed25519: { publicKey: edPub, privateKey: edPriv },
|
|
11
|
+
x25519: { publicKey: xPub, privateKey: xPriv },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Sign a message with Ed25519. */
|
|
15
|
+
export function sign(privateKey, message) {
|
|
16
|
+
return ed25519.sign(message, privateKey);
|
|
17
|
+
}
|
|
18
|
+
/** Verify an Ed25519 signature. */
|
|
19
|
+
export function verify(publicKey, message, signature) {
|
|
20
|
+
return ed25519.verify(signature, message, publicKey);
|
|
21
|
+
}
|
|
22
|
+
/** Compute X25519 shared secret for key exchange. */
|
|
23
|
+
export function computeSharedSecret(privateKey, publicKey) {
|
|
24
|
+
return x25519.getSharedSecret(privateKey, publicKey);
|
|
25
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DeviceGrantPayload } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Create a device grant: encrypt AMK for the target device using X25519 shared secret,
|
|
4
|
+
* and sign the payload with the source device's Ed25519 key.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDeviceGrant(params: {
|
|
7
|
+
sourceEdPrivateKey: Uint8Array;
|
|
8
|
+
sourceXPrivateKey: Uint8Array;
|
|
9
|
+
sourceDeviceId: string;
|
|
10
|
+
targetDeviceId: string;
|
|
11
|
+
targetKexPublicKey: Uint8Array;
|
|
12
|
+
amk: Uint8Array;
|
|
13
|
+
}): {
|
|
14
|
+
payload: DeviceGrantPayload;
|
|
15
|
+
payloadBytes: Uint8Array;
|
|
16
|
+
signature: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Receive and decrypt a device grant from a source device.
|
|
20
|
+
* Verifies signature, validates grant fields, then decrypts the AMK.
|
|
21
|
+
*/
|
|
22
|
+
export declare function receiveDeviceGrant(params: {
|
|
23
|
+
targetXPrivateKey: Uint8Array;
|
|
24
|
+
targetDeviceId: string;
|
|
25
|
+
sourceKexPublicKey: Uint8Array;
|
|
26
|
+
sourceSigningPublicKey: Uint8Array;
|
|
27
|
+
payloadBytes: Uint8Array;
|
|
28
|
+
signature: Uint8Array;
|
|
29
|
+
}): Uint8Array;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
2
|
+
import { computeSharedSecret, sign, verify } from "./device-keys.js";
|
|
3
|
+
import { encrypt, decrypt } from "./aes-gcm.js";
|
|
4
|
+
import { CRYPTO_VERSION } from "../constants.js";
|
|
5
|
+
function toB64(d) {
|
|
6
|
+
return Buffer.from(d).toString("base64");
|
|
7
|
+
}
|
|
8
|
+
function fromB64(s) {
|
|
9
|
+
return new Uint8Array(Buffer.from(s, "base64"));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create a device grant: encrypt AMK for the target device using X25519 shared secret,
|
|
13
|
+
* and sign the payload with the source device's Ed25519 key.
|
|
14
|
+
*/
|
|
15
|
+
export function createDeviceGrant(params) {
|
|
16
|
+
// Derive encryption key from X25519 shared secret
|
|
17
|
+
const shared = computeSharedSecret(params.sourceXPrivateKey, params.targetKexPublicKey);
|
|
18
|
+
// Guard against low-order X25519 points (all-zero shared secret)
|
|
19
|
+
if (shared.every((b) => b === 0)) {
|
|
20
|
+
throw new Error("X25519 shared secret is all-zero (invalid public key)");
|
|
21
|
+
}
|
|
22
|
+
const derivedKey = sha256(shared);
|
|
23
|
+
// Encrypt AMK with derived key
|
|
24
|
+
const { ciphertext, iv, tag } = encrypt(derivedKey, params.amk);
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const expires = new Date(now.getTime() + 20 * 60 * 1000); // 20 min TTL
|
|
27
|
+
const payload = {
|
|
28
|
+
version: 1,
|
|
29
|
+
grant_type: "amk_transfer",
|
|
30
|
+
crypto_version: CRYPTO_VERSION,
|
|
31
|
+
source_device_id: params.sourceDeviceId,
|
|
32
|
+
target_device_id: params.targetDeviceId,
|
|
33
|
+
target_device_kex_public_key_b64: toB64(params.targetKexPublicKey),
|
|
34
|
+
encrypted_amk_b64: toB64(new Uint8Array([...ciphertext, ...tag])),
|
|
35
|
+
shared_secret_salt_b64: toB64(iv),
|
|
36
|
+
created_at: now.toISOString(),
|
|
37
|
+
expires_at: expires.toISOString(),
|
|
38
|
+
};
|
|
39
|
+
const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
|
|
40
|
+
const signature = sign(params.sourceEdPrivateKey, payloadBytes);
|
|
41
|
+
// Zero shared secret
|
|
42
|
+
derivedKey.fill(0);
|
|
43
|
+
shared.fill(0);
|
|
44
|
+
return { payload, payloadBytes, signature };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Receive and decrypt a device grant from a source device.
|
|
48
|
+
* Verifies signature, validates grant fields, then decrypts the AMK.
|
|
49
|
+
*/
|
|
50
|
+
export function receiveDeviceGrant(params) {
|
|
51
|
+
// Verify signature first
|
|
52
|
+
if (!verify(params.sourceSigningPublicKey, params.payloadBytes, params.signature)) {
|
|
53
|
+
throw new Error("Device grant signature verification failed");
|
|
54
|
+
}
|
|
55
|
+
const payload = JSON.parse(new TextDecoder().decode(params.payloadBytes));
|
|
56
|
+
// Validate grant fields to prevent replay/misdirection
|
|
57
|
+
if (payload.target_device_id !== params.targetDeviceId) {
|
|
58
|
+
throw new Error("Device grant target_device_id mismatch");
|
|
59
|
+
}
|
|
60
|
+
if (payload.crypto_version !== CRYPTO_VERSION) {
|
|
61
|
+
throw new Error(`Unsupported grant crypto_version: ${payload.crypto_version}`);
|
|
62
|
+
}
|
|
63
|
+
if (new Date(payload.expires_at).getTime() < Date.now()) {
|
|
64
|
+
throw new Error("Device grant has expired");
|
|
65
|
+
}
|
|
66
|
+
// Derive same shared secret using target's private key + source's public key
|
|
67
|
+
const shared = computeSharedSecret(params.targetXPrivateKey, params.sourceKexPublicKey);
|
|
68
|
+
// Guard against low-order X25519 points (all-zero shared secret)
|
|
69
|
+
if (shared.every((b) => b === 0)) {
|
|
70
|
+
throw new Error("X25519 shared secret is all-zero (invalid public key)");
|
|
71
|
+
}
|
|
72
|
+
const derivedKey = sha256(shared);
|
|
73
|
+
// Decrypt AMK
|
|
74
|
+
const encryptedData = fromB64(payload.encrypted_amk_b64);
|
|
75
|
+
const iv = fromB64(payload.shared_secret_salt_b64);
|
|
76
|
+
// The encrypted AMK includes ciphertext + tag appended
|
|
77
|
+
const tagLength = 16;
|
|
78
|
+
const ciphertext = encryptedData.subarray(0, encryptedData.length - tagLength);
|
|
79
|
+
const tag = encryptedData.subarray(encryptedData.length - tagLength);
|
|
80
|
+
try {
|
|
81
|
+
return decrypt(derivedKey, ciphertext, iv, tag);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
derivedKey.fill(0);
|
|
85
|
+
shared.fill(0);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface KdfResult {
|
|
2
|
+
urk: Uint8Array;
|
|
3
|
+
salt: Uint8Array;
|
|
4
|
+
}
|
|
5
|
+
export interface KdfParams {
|
|
6
|
+
memoryKib: number;
|
|
7
|
+
iterations: number;
|
|
8
|
+
parallelism: number;
|
|
9
|
+
saltLength?: number;
|
|
10
|
+
outputLength?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Derive URK (Unwrap Root Key) from a passphrase using Argon2id.
|
|
14
|
+
* If no salt is provided, a fresh random salt is generated.
|
|
15
|
+
*/
|
|
16
|
+
export declare function deriveURK(passphrase: string, existingSalt?: Uint8Array, params?: Partial<KdfParams>): Promise<KdfResult>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { argon2id } from "hash-wasm";
|
|
2
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
3
|
+
import { ARGON2_PARAMS } from "../constants.js";
|
|
4
|
+
/**
|
|
5
|
+
* Derive URK (Unwrap Root Key) from a passphrase using Argon2id.
|
|
6
|
+
* If no salt is provided, a fresh random salt is generated.
|
|
7
|
+
*/
|
|
8
|
+
export async function deriveURK(passphrase, existingSalt, params) {
|
|
9
|
+
const effective = {
|
|
10
|
+
...ARGON2_PARAMS,
|
|
11
|
+
...params,
|
|
12
|
+
};
|
|
13
|
+
const salt = existingSalt ?? randomBytes(effective.saltLength);
|
|
14
|
+
const hash = await argon2id({
|
|
15
|
+
password: passphrase,
|
|
16
|
+
salt,
|
|
17
|
+
parallelism: effective.parallelism,
|
|
18
|
+
iterations: effective.iterations,
|
|
19
|
+
memorySize: effective.memoryKib,
|
|
20
|
+
hashLength: effective.outputLength,
|
|
21
|
+
outputType: "binary",
|
|
22
|
+
});
|
|
23
|
+
return { urk: new Uint8Array(hash), salt };
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type KeyEnvelope } from "./aes-gcm.js";
|
|
2
|
+
import type { WrappedAMKBundle } from "./types.js";
|
|
3
|
+
/** Generate a new Account Master Key (32 random bytes). */
|
|
4
|
+
export declare function generateAMK(): Uint8Array;
|
|
5
|
+
/** Generate a new Data Encryption Key (32 random bytes). */
|
|
6
|
+
export declare function generateDEK(): Uint8Array;
|
|
7
|
+
/** Wrap AMK with URK using AES-256-GCM. */
|
|
8
|
+
export declare function wrapAMK(urk: Uint8Array, amk: Uint8Array): WrappedAMKBundle;
|
|
9
|
+
/** Unwrap AMK from URK-protected bundle. Throws on wrong passphrase. */
|
|
10
|
+
export declare function unwrapAMK(urk: Uint8Array, bundle: WrappedAMKBundle): Uint8Array;
|
|
11
|
+
/** Wrap DEK with AMK using AES-256-GCM + AAD. */
|
|
12
|
+
export declare function wrapDEK(amk: Uint8Array, dek: Uint8Array, aad: Uint8Array): KeyEnvelope;
|
|
13
|
+
/** Unwrap DEK from AMK-protected envelope. Throws on wrong key. */
|
|
14
|
+
export declare function unwrapDEK(amk: Uint8Array, envelope: KeyEnvelope, aad: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomBytes } from "@noble/hashes/utils";
|
|
2
|
+
import { wrapKey, unwrapKey } from "./aes-gcm.js";
|
|
3
|
+
/** Generate a new Account Master Key (32 random bytes). */
|
|
4
|
+
export function generateAMK() {
|
|
5
|
+
return randomBytes(32);
|
|
6
|
+
}
|
|
7
|
+
/** Generate a new Data Encryption Key (32 random bytes). */
|
|
8
|
+
export function generateDEK() {
|
|
9
|
+
return randomBytes(32);
|
|
10
|
+
}
|
|
11
|
+
/** Wrap AMK with URK using AES-256-GCM. */
|
|
12
|
+
export function wrapAMK(urk, amk) {
|
|
13
|
+
const envelope = wrapKey(urk, amk);
|
|
14
|
+
return {
|
|
15
|
+
encrypted_amk: envelope.encrypted_key,
|
|
16
|
+
iv: envelope.iv,
|
|
17
|
+
tag: envelope.tag,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Unwrap AMK from URK-protected bundle. Throws on wrong passphrase. */
|
|
21
|
+
export function unwrapAMK(urk, bundle) {
|
|
22
|
+
return unwrapKey(urk, {
|
|
23
|
+
encrypted_key: bundle.encrypted_amk,
|
|
24
|
+
iv: bundle.iv,
|
|
25
|
+
tag: bundle.tag,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Wrap DEK with AMK using AES-256-GCM + AAD. */
|
|
29
|
+
export function wrapDEK(amk, dek, aad) {
|
|
30
|
+
return wrapKey(amk, dek, aad);
|
|
31
|
+
}
|
|
32
|
+
/** Unwrap DEK from AMK-protected envelope. Throws on wrong key. */
|
|
33
|
+
export function unwrapDEK(amk, envelope, aad) {
|
|
34
|
+
return unwrapKey(amk, envelope, aad);
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EncryptedObjectManifest, AadDescriptor, BlobRef } from "../contract-types.js";
|
|
2
|
+
export interface ManifestInput {
|
|
3
|
+
objectId: string;
|
|
4
|
+
objectVersion: number;
|
|
5
|
+
contentBlob: BlobRef;
|
|
6
|
+
metadataBlob: BlobRef;
|
|
7
|
+
dekEnvelope: {
|
|
8
|
+
encrypted_dek: Uint8Array;
|
|
9
|
+
iv: Uint8Array;
|
|
10
|
+
tag: Uint8Array;
|
|
11
|
+
};
|
|
12
|
+
aad: AadDescriptor;
|
|
13
|
+
ciphertextSize: number;
|
|
14
|
+
contentSha256: string;
|
|
15
|
+
metadataSha256: string;
|
|
16
|
+
}
|
|
17
|
+
/** Build an EncryptedObjectManifest matching the shared contract schema. */
|
|
18
|
+
export declare function buildManifest(input: ManifestInput): EncryptedObjectManifest;
|
|
19
|
+
/** Compute SHA-256 hex digest of binary data. */
|
|
20
|
+
export declare function computeSha256Hex(data: Uint8Array): string;
|
|
21
|
+
/**
|
|
22
|
+
* Serialize AAD descriptor to bytes for use as AES-GCM additional data.
|
|
23
|
+
* Keys are sorted alphabetically for cross-implementation canonical encoding.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serializeAad(aad: AadDescriptor): Uint8Array;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
2
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
3
|
+
import { MANIFEST_VERSION, CIPHER_ALGORITHM, WRAP_ALGORITHM, CRYPTO_VERSION, } from "../constants.js";
|
|
4
|
+
function toB64(data) {
|
|
5
|
+
return Buffer.from(data).toString("base64");
|
|
6
|
+
}
|
|
7
|
+
/** Build an EncryptedObjectManifest matching the shared contract schema. */
|
|
8
|
+
export function buildManifest(input) {
|
|
9
|
+
return {
|
|
10
|
+
manifest_version: MANIFEST_VERSION,
|
|
11
|
+
cipher_algorithm: CIPHER_ALGORITHM,
|
|
12
|
+
wrap_algorithm: WRAP_ALGORITHM,
|
|
13
|
+
object_id: input.objectId,
|
|
14
|
+
object_version: input.objectVersion,
|
|
15
|
+
ciphertext_size: input.ciphertextSize,
|
|
16
|
+
content_sha256: input.contentSha256,
|
|
17
|
+
metadata_sha256: input.metadataSha256,
|
|
18
|
+
blob: input.contentBlob,
|
|
19
|
+
metadata: input.metadataBlob,
|
|
20
|
+
dek_envelope: {
|
|
21
|
+
version: CRYPTO_VERSION,
|
|
22
|
+
algorithm: CIPHER_ALGORITHM,
|
|
23
|
+
wrapped_for: "amk",
|
|
24
|
+
encrypted_dek_b64: toB64(input.dekEnvelope.encrypted_dek),
|
|
25
|
+
iv_b64: toB64(input.dekEnvelope.iv),
|
|
26
|
+
tag_b64: toB64(input.dekEnvelope.tag),
|
|
27
|
+
},
|
|
28
|
+
aad: input.aad,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Compute SHA-256 hex digest of binary data. */
|
|
32
|
+
export function computeSha256Hex(data) {
|
|
33
|
+
return bytesToHex(sha256(data));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Serialize AAD descriptor to bytes for use as AES-GCM additional data.
|
|
37
|
+
* Keys are sorted alphabetically for cross-implementation canonical encoding.
|
|
38
|
+
*/
|
|
39
|
+
export function serializeAad(aad) {
|
|
40
|
+
return new TextEncoder().encode(JSON.stringify(aad, Object.keys(aad).sort()));
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RecoveryBundle } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a high-entropy 24-character recovery code.
|
|
4
|
+
* Avoids ambiguous characters (0, O, 1, l, I).
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateRecoveryCode(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Create a recovery bundle that wraps the AMK with a key derived from the recovery code.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createRecoveryBundle(recoveryCode: string, amk: Uint8Array): Promise<RecoveryBundle>;
|
|
11
|
+
/**
|
|
12
|
+
* Restore AMK from a recovery bundle using the recovery code.
|
|
13
|
+
* Validates checksum before attempting decryption.
|
|
14
|
+
* Throws on wrong code (AES-GCM auth failure) or tampered bundle.
|
|
15
|
+
*/
|
|
16
|
+
export declare function restoreFromRecoveryBundle(recoveryCode: string, bundle: RecoveryBundle): Promise<Uint8Array>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { randomBytes, bytesToHex } from "@noble/hashes/utils";
|
|
2
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
3
|
+
import { deriveURK } from "./kdf.js";
|
|
4
|
+
import { encrypt, decrypt } from "./aes-gcm.js";
|
|
5
|
+
import { ARGON2_PARAMS } from "../constants.js";
|
|
6
|
+
function toB64(d) {
|
|
7
|
+
return Buffer.from(d).toString("base64");
|
|
8
|
+
}
|
|
9
|
+
function fromB64(s) {
|
|
10
|
+
return new Uint8Array(Buffer.from(s, "base64"));
|
|
11
|
+
}
|
|
12
|
+
function readBundleKdfParams(bundle) {
|
|
13
|
+
if (bundle.bundle_type !== "amk_recovery") {
|
|
14
|
+
throw new Error(`Unsupported recovery bundle type: ${bundle.bundle_type}`);
|
|
15
|
+
}
|
|
16
|
+
if (bundle.wrap_algorithm !== "aes-256-gcm") {
|
|
17
|
+
throw new Error(`Unsupported recovery wrap algorithm: ${bundle.wrap_algorithm}`);
|
|
18
|
+
}
|
|
19
|
+
if (bundle.kdf_algorithm !== "argon2id") {
|
|
20
|
+
throw new Error(`Unsupported recovery KDF: ${bundle.kdf_algorithm}`);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
memoryKib: bundle.kdf_params.memory_kib,
|
|
24
|
+
iterations: bundle.kdf_params.iterations,
|
|
25
|
+
parallelism: bundle.kdf_params.parallelism,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Generate a high-entropy 24-character recovery code.
|
|
30
|
+
* Avoids ambiguous characters (0, O, 1, l, I).
|
|
31
|
+
*/
|
|
32
|
+
export function generateRecoveryCode() {
|
|
33
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
|
34
|
+
const bytes = randomBytes(24);
|
|
35
|
+
return Array.from(bytes)
|
|
36
|
+
.map((b) => chars[b % chars.length])
|
|
37
|
+
.join("");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a recovery bundle that wraps the AMK with a key derived from the recovery code.
|
|
41
|
+
*/
|
|
42
|
+
export async function createRecoveryBundle(recoveryCode, amk) {
|
|
43
|
+
const { urk: recoveryKey, salt } = await deriveURK(recoveryCode);
|
|
44
|
+
const { ciphertext, iv, tag } = encrypt(recoveryKey, amk);
|
|
45
|
+
// Checksum covers the encrypted payload for integrity
|
|
46
|
+
const payload = new Uint8Array([
|
|
47
|
+
...ciphertext,
|
|
48
|
+
...iv,
|
|
49
|
+
...tag,
|
|
50
|
+
]);
|
|
51
|
+
// Zero recovery key after use
|
|
52
|
+
recoveryKey.fill(0);
|
|
53
|
+
return {
|
|
54
|
+
version: 1,
|
|
55
|
+
bundle_type: "amk_recovery",
|
|
56
|
+
wrap_algorithm: "aes-256-gcm",
|
|
57
|
+
kdf_algorithm: "argon2id",
|
|
58
|
+
kdf_params: {
|
|
59
|
+
memory_kib: ARGON2_PARAMS.memoryKib,
|
|
60
|
+
iterations: ARGON2_PARAMS.iterations,
|
|
61
|
+
parallelism: ARGON2_PARAMS.parallelism,
|
|
62
|
+
salt_b64: toB64(salt),
|
|
63
|
+
},
|
|
64
|
+
encrypted_amk_b64: toB64(ciphertext),
|
|
65
|
+
iv_b64: toB64(iv),
|
|
66
|
+
tag_b64: toB64(tag),
|
|
67
|
+
checksum_sha256: bytesToHex(sha256(payload)),
|
|
68
|
+
created_at: new Date().toISOString(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Restore AMK from a recovery bundle using the recovery code.
|
|
73
|
+
* Validates checksum before attempting decryption.
|
|
74
|
+
* Throws on wrong code (AES-GCM auth failure) or tampered bundle.
|
|
75
|
+
*/
|
|
76
|
+
export async function restoreFromRecoveryBundle(recoveryCode, bundle) {
|
|
77
|
+
// Validate checksum integrity before spending time on KDF
|
|
78
|
+
const ciphertext = fromB64(bundle.encrypted_amk_b64);
|
|
79
|
+
const iv = fromB64(bundle.iv_b64);
|
|
80
|
+
const tag = fromB64(bundle.tag_b64);
|
|
81
|
+
const payload = new Uint8Array([...ciphertext, ...iv, ...tag]);
|
|
82
|
+
const actualChecksum = bytesToHex(sha256(payload));
|
|
83
|
+
if (actualChecksum !== bundle.checksum_sha256) {
|
|
84
|
+
throw new Error("Recovery bundle checksum mismatch — data may be corrupted");
|
|
85
|
+
}
|
|
86
|
+
const salt = fromB64(bundle.kdf_params.salt_b64);
|
|
87
|
+
const { urk: recoveryKey } = await deriveURK(recoveryCode, salt, readBundleKdfParams(bundle));
|
|
88
|
+
try {
|
|
89
|
+
return decrypt(recoveryKey, ciphertext, iv, tag);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
recoveryKey.fill(0);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface WrappedAMKBundle {
|
|
2
|
+
encrypted_amk: Uint8Array;
|
|
3
|
+
iv: Uint8Array;
|
|
4
|
+
tag: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export interface RecoveryBundle {
|
|
7
|
+
version: number;
|
|
8
|
+
bundle_type: "amk_recovery";
|
|
9
|
+
wrap_algorithm: "aes-256-gcm";
|
|
10
|
+
kdf_algorithm: "argon2id";
|
|
11
|
+
kdf_params: {
|
|
12
|
+
memory_kib: number;
|
|
13
|
+
iterations: number;
|
|
14
|
+
parallelism: number;
|
|
15
|
+
salt_b64: string;
|
|
16
|
+
};
|
|
17
|
+
encrypted_amk_b64: string;
|
|
18
|
+
iv_b64: string;
|
|
19
|
+
tag_b64: string;
|
|
20
|
+
checksum_sha256: string;
|
|
21
|
+
created_at: string;
|
|
22
|
+
}
|
|
23
|
+
export interface DeviceGrantPayload {
|
|
24
|
+
version: number;
|
|
25
|
+
grant_type: "amk_transfer";
|
|
26
|
+
crypto_version: number;
|
|
27
|
+
source_device_id: string;
|
|
28
|
+
target_device_id: string;
|
|
29
|
+
target_device_kex_public_key_b64: string;
|
|
30
|
+
encrypted_amk_b64: string;
|
|
31
|
+
shared_secret_salt_b64: string;
|
|
32
|
+
created_at: string;
|
|
33
|
+
expires_at: string;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export declare const debugLog: {
|
|
2
|
+
info(component: string, msg: string): void;
|
|
3
|
+
warn(component: string, msg: string): void;
|
|
4
|
+
error(component: string, msg: string): void;
|
|
5
|
+
/** Log an API request (never logs tokens or ciphertext). */
|
|
6
|
+
apiRequest(method: string, url: string): void;
|
|
7
|
+
/** Log an API response (status + timing). */
|
|
8
|
+
apiResponse(method: string, url: string, status: number, ms: number): void;
|
|
9
|
+
/** Log an API error. */
|
|
10
|
+
apiError(method: string, url: string, error: unknown): void;
|
|
11
|
+
/** Log a crypto operation (never log key material). */
|
|
12
|
+
crypto(op: string, detail?: string): void;
|
|
13
|
+
/** Log a sync event. */
|
|
14
|
+
sync(msg: string): void;
|
|
15
|
+
/** Log sync upload start/end. */
|
|
16
|
+
syncUploadStart(logicalPath: string, objectId: string): void;
|
|
17
|
+
syncUploadEnd(logicalPath: string, objectId: string, version: number): void;
|
|
18
|
+
/** Log sync download start/end. */
|
|
19
|
+
syncDownloadStart(objectId: string): void;
|
|
20
|
+
syncDownloadEnd(objectId: string, logicalPath: string, version: number): void;
|
|
21
|
+
/** Log sync diff results. */
|
|
22
|
+
syncDiff(toUpload: number, toDownload: number, conflicts: number): void;
|
|
23
|
+
/** Log KDF started (never log passphrase or key material). */
|
|
24
|
+
cryptoKdfStart(): void;
|
|
25
|
+
cryptoKdfEnd(ms: number): void;
|
|
26
|
+
/** Log encrypt/decrypt completed (never log key material). */
|
|
27
|
+
cryptoEncryptDone(objectId: string): void;
|
|
28
|
+
cryptoDecryptDone(objectId: string): void;
|
|
29
|
+
/** Log CLI command execution. */
|
|
30
|
+
cliCommand(command: string): void;
|
|
31
|
+
cliCommandEnd(command: string, success: boolean, ms: number): void;
|
|
32
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
const LOG_FILE = process.env.MEMBOX_LOG_FILE ?? "";
|
|
4
|
+
let initialized = false;
|
|
5
|
+
function ensureDir() {
|
|
6
|
+
if (initialized || !LOG_FILE)
|
|
7
|
+
return;
|
|
8
|
+
try {
|
|
9
|
+
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
/* dir may already exist */
|
|
13
|
+
}
|
|
14
|
+
initialized = true;
|
|
15
|
+
}
|
|
16
|
+
function ts() {
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
}
|
|
19
|
+
// Synchronous write is acceptable for debug/联调 use — keeps log ordering
|
|
20
|
+
// deterministic and avoids async complexity. Not intended for production.
|
|
21
|
+
function write(level, component, msg) {
|
|
22
|
+
if (!LOG_FILE)
|
|
23
|
+
return;
|
|
24
|
+
ensureDir();
|
|
25
|
+
const line = `[${ts()}] [${level}] [${component}] ${msg}\n`;
|
|
26
|
+
try {
|
|
27
|
+
appendFileSync(LOG_FILE, line);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* best-effort file logging */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export const debugLog = {
|
|
34
|
+
info(component, msg) {
|
|
35
|
+
write("INFO", component, msg);
|
|
36
|
+
},
|
|
37
|
+
warn(component, msg) {
|
|
38
|
+
write("WARN", component, msg);
|
|
39
|
+
},
|
|
40
|
+
error(component, msg) {
|
|
41
|
+
write("ERROR", component, msg);
|
|
42
|
+
},
|
|
43
|
+
/** Log an API request (never logs tokens or ciphertext). */
|
|
44
|
+
apiRequest(method, url) {
|
|
45
|
+
// Strip auth tokens from URL if any query params
|
|
46
|
+
const safeUrl = url.replace(/access_token=[^&]+/g, "access_token=***");
|
|
47
|
+
write("INFO", "api-client", `→ ${method} ${safeUrl}`);
|
|
48
|
+
},
|
|
49
|
+
/** Log an API response (status + timing). */
|
|
50
|
+
apiResponse(method, url, status, ms) {
|
|
51
|
+
const safeUrl = url.replace(/access_token=[^&]+/g, "access_token=***");
|
|
52
|
+
write("INFO", "api-client", `← ${status} ${method} ${safeUrl} (${ms}ms)`);
|
|
53
|
+
},
|
|
54
|
+
/** Log an API error. */
|
|
55
|
+
apiError(method, url, error) {
|
|
56
|
+
const safeUrl = url.replace(/access_token=[^&]+/g, "access_token=***");
|
|
57
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
58
|
+
write("ERROR", "api-client", `✗ ${method} ${safeUrl} — ${msg}`);
|
|
59
|
+
},
|
|
60
|
+
/** Log a crypto operation (never log key material). */
|
|
61
|
+
crypto(op, detail) {
|
|
62
|
+
write("INFO", "crypto", detail ? `${op}: ${detail}` : op);
|
|
63
|
+
},
|
|
64
|
+
/** Log a sync event. */
|
|
65
|
+
sync(msg) {
|
|
66
|
+
write("INFO", "sync", msg);
|
|
67
|
+
},
|
|
68
|
+
/** Log sync upload start/end. */
|
|
69
|
+
syncUploadStart(logicalPath, objectId) {
|
|
70
|
+
write("INFO", "sync", `upload-start: ${logicalPath} (${objectId})`);
|
|
71
|
+
},
|
|
72
|
+
syncUploadEnd(logicalPath, objectId, version) {
|
|
73
|
+
write("INFO", "sync", `upload-end: ${logicalPath} (${objectId}) v${version}`);
|
|
74
|
+
},
|
|
75
|
+
/** Log sync download start/end. */
|
|
76
|
+
syncDownloadStart(objectId) {
|
|
77
|
+
write("INFO", "sync", `download-start: ${objectId}`);
|
|
78
|
+
},
|
|
79
|
+
syncDownloadEnd(objectId, logicalPath, version) {
|
|
80
|
+
write("INFO", "sync", `download-end: ${objectId} → ${logicalPath} v${version}`);
|
|
81
|
+
},
|
|
82
|
+
/** Log sync diff results. */
|
|
83
|
+
syncDiff(toUpload, toDownload, conflicts) {
|
|
84
|
+
write("INFO", "sync", `diff: upload=${toUpload} download=${toDownload} conflicts=${conflicts}`);
|
|
85
|
+
},
|
|
86
|
+
/** Log KDF started (never log passphrase or key material). */
|
|
87
|
+
cryptoKdfStart() {
|
|
88
|
+
write("INFO", "crypto", "KDF started (Argon2id)");
|
|
89
|
+
},
|
|
90
|
+
cryptoKdfEnd(ms) {
|
|
91
|
+
write("INFO", "crypto", `KDF completed (${ms}ms)`);
|
|
92
|
+
},
|
|
93
|
+
/** Log encrypt/decrypt completed (never log key material). */
|
|
94
|
+
cryptoEncryptDone(objectId) {
|
|
95
|
+
write("INFO", "crypto", `encrypt completed: ${objectId}`);
|
|
96
|
+
},
|
|
97
|
+
cryptoDecryptDone(objectId) {
|
|
98
|
+
write("INFO", "crypto", `decrypt completed: ${objectId}`);
|
|
99
|
+
},
|
|
100
|
+
/** Log CLI command execution. */
|
|
101
|
+
cliCommand(command) {
|
|
102
|
+
write("INFO", "cli", `command-start: ${command}`);
|
|
103
|
+
},
|
|
104
|
+
cliCommandEnd(command, success, ms) {
|
|
105
|
+
const status = success ? "ok" : "failed";
|
|
106
|
+
write("INFO", "cli", `command-end: ${command} [${status}] (${ms}ms)`);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AutoSyncService } from "../sync/auto-sync.js";
|
|
2
|
+
export declare function getAutoSync(): AutoSyncService | undefined;
|
|
3
|
+
/** Create and register a new AutoSyncService (used by resume when none exists). */
|
|
4
|
+
export declare function startAutoSync(): Promise<AutoSyncService>;
|
|
5
|
+
export declare function onGatewayStart(): Promise<void>;
|
|
6
|
+
export declare function onGatewayStop(): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readState } from "../store/local-state.js";
|
|
2
|
+
import { vaultSession } from "../store/session.js";
|
|
3
|
+
import { getSyncStatus } from "../api/sync.js";
|
|
4
|
+
import { createAuthenticatedClient } from "../cli/helpers.js";
|
|
5
|
+
let autoSync;
|
|
6
|
+
export function getAutoSync() {
|
|
7
|
+
return autoSync;
|
|
8
|
+
}
|
|
9
|
+
/** Create and register a new AutoSyncService (used by resume when none exists). */
|
|
10
|
+
export async function startAutoSync() {
|
|
11
|
+
const { AutoSyncService } = await import("../sync/auto-sync.js");
|
|
12
|
+
autoSync = new AutoSyncService(process.cwd());
|
|
13
|
+
return autoSync;
|
|
14
|
+
}
|
|
15
|
+
export async function onGatewayStart() {
|
|
16
|
+
try {
|
|
17
|
+
const state = await readState();
|
|
18
|
+
if (!state?.setup_complete || !vaultSession.isUnlocked())
|
|
19
|
+
return;
|
|
20
|
+
// Remote status check: compare cursors
|
|
21
|
+
const client = await createAuthenticatedClient(state);
|
|
22
|
+
const remote = await getSyncStatus(client);
|
|
23
|
+
if (remote.cursor > state.sync_cursor) {
|
|
24
|
+
console.log(`[membox] Remote has updates (remote cursor ${remote.cursor} > local ${state.sync_cursor}). Run 'pull' to sync.`);
|
|
25
|
+
}
|
|
26
|
+
// Start auto-sync if not paused
|
|
27
|
+
if (!state.sync_paused) {
|
|
28
|
+
const { AutoSyncService } = await import("../sync/auto-sync.js");
|
|
29
|
+
autoSync = new AutoSyncService(process.cwd());
|
|
30
|
+
autoSync.start();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Network unavailable — don't block startup
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function onGatewayStop() {
|
|
38
|
+
autoSync?.stop();
|
|
39
|
+
autoSync = undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readState } from "../store/local-state.js";
|
|
2
|
+
import { vaultSession } from "../store/session.js";
|
|
3
|
+
/**
|
|
4
|
+
* Inject a brief vault status line into agent context.
|
|
5
|
+
* Called on before_prompt_build hook.
|
|
6
|
+
*/
|
|
7
|
+
export async function injectVaultStatus() {
|
|
8
|
+
const state = await readState();
|
|
9
|
+
if (!state?.setup_complete) {
|
|
10
|
+
return { appendSystemContext: "Membox Vault: not configured." };
|
|
11
|
+
}
|
|
12
|
+
const files = Object.keys(state.file_versions).length;
|
|
13
|
+
const locked = !vaultSession.isUnlocked();
|
|
14
|
+
const paused = state.sync_paused ? ", paused" : "";
|
|
15
|
+
return {
|
|
16
|
+
appendSystemContext: `Membox Vault: ${locked ? "locked" : "unlocked"}${paused}, ${files} synced file(s), cursor ${state.sync_cursor}.`,
|
|
17
|
+
};
|
|
18
|
+
}
|