@msdis/shield 0.2.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/LICENSE +140 -0
- package/README.md +106 -0
- package/dist/aead/index.d.ts +37 -0
- package/dist/aead/index.js +7 -0
- package/dist/aead/index.js.map +1 -0
- package/dist/asymmetric/index.d.ts +32 -0
- package/dist/asymmetric/index.js +6 -0
- package/dist/asymmetric/index.js.map +1 -0
- package/dist/chunk-3DQPQCAR.js +114 -0
- package/dist/chunk-3DQPQCAR.js.map +1 -0
- package/dist/chunk-3HCT6A2P.js +55 -0
- package/dist/chunk-3HCT6A2P.js.map +1 -0
- package/dist/chunk-AB2WZ7Y2.js +57 -0
- package/dist/chunk-AB2WZ7Y2.js.map +1 -0
- package/dist/chunk-BUFRR5PB.js +9 -0
- package/dist/chunk-BUFRR5PB.js.map +1 -0
- package/dist/chunk-CYIGDF63.js +30 -0
- package/dist/chunk-CYIGDF63.js.map +1 -0
- package/dist/chunk-EOXWR7DS.js +153 -0
- package/dist/chunk-EOXWR7DS.js.map +1 -0
- package/dist/chunk-FUDDBD2G.js +43 -0
- package/dist/chunk-FUDDBD2G.js.map +1 -0
- package/dist/chunk-JSKIWIEC.js +56 -0
- package/dist/chunk-JSKIWIEC.js.map +1 -0
- package/dist/chunk-JVFP2GAO.js +66 -0
- package/dist/chunk-JVFP2GAO.js.map +1 -0
- package/dist/chunk-KNCZMIZA.js +55 -0
- package/dist/chunk-KNCZMIZA.js.map +1 -0
- package/dist/chunk-MJO7IJZC.js +44 -0
- package/dist/chunk-MJO7IJZC.js.map +1 -0
- package/dist/chunk-MPWYZXW7.js +66 -0
- package/dist/chunk-MPWYZXW7.js.map +1 -0
- package/dist/chunk-OA5ARYJM.js +73 -0
- package/dist/chunk-OA5ARYJM.js.map +1 -0
- package/dist/chunk-OPHN2B3N.js +147 -0
- package/dist/chunk-OPHN2B3N.js.map +1 -0
- package/dist/chunk-RTAJJZKO.js +116 -0
- package/dist/chunk-RTAJJZKO.js.map +1 -0
- package/dist/chunk-SCHZI6YY.js +35 -0
- package/dist/chunk-SCHZI6YY.js.map +1 -0
- package/dist/chunk-T3IV7SHD.js +388 -0
- package/dist/chunk-T3IV7SHD.js.map +1 -0
- package/dist/chunk-U65A4HIY.js +133 -0
- package/dist/chunk-U65A4HIY.js.map +1 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/encoding-B-cb7Duu.d.ts +37 -0
- package/dist/errors-C79jA9vX.d.ts +65 -0
- package/dist/file-encryption/index.d.ts +135 -0
- package/dist/file-encryption/index.js +11 -0
- package/dist/file-encryption/index.js.map +1 -0
- package/dist/format-versioning/index.d.ts +37 -0
- package/dist/format-versioning/index.js +4 -0
- package/dist/format-versioning/index.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/integrity/index.d.ts +46 -0
- package/dist/integrity/index.js +6 -0
- package/dist/integrity/index.js.map +1 -0
- package/dist/kdf/index.d.ts +104 -0
- package/dist/kdf/index.js +7 -0
- package/dist/kdf/index.js.map +1 -0
- package/dist/key-management/index.d.ts +69 -0
- package/dist/key-management/index.js +10 -0
- package/dist/key-management/index.js.map +1 -0
- package/dist/migrations/index.d.ts +41 -0
- package/dist/migrations/index.js +4 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/post-quantum/index.d.ts +153 -0
- package/dist/post-quantum/index.js +3 -0
- package/dist/post-quantum/index.js.map +1 -0
- package/dist/random/index.d.ts +28 -0
- package/dist/random/index.js +5 -0
- package/dist/random/index.js.map +1 -0
- package/dist/secure-memory/index.d.ts +40 -0
- package/dist/secure-memory/index.js +5 -0
- package/dist/secure-memory/index.js.map +1 -0
- package/dist/signing/index.d.ts +41 -0
- package/dist/signing/index.js +5 -0
- package/dist/signing/index.js.map +1 -0
- package/dist/totp/index.d.ts +69 -0
- package/dist/totp/index.js +4 -0
- package/dist/totp/index.js.map +1 -0
- package/dist/vault-crypto/index.d.ts +225 -0
- package/dist/vault-crypto/index.js +465 -0
- package/dist/vault-crypto/index.js.map +1 -0
- package/dist/vault-encryption/index.d.ts +40 -0
- package/dist/vault-encryption/index.js +9 -0
- package/dist/vault-encryption/index.js.map +1 -0
- package/package.json +137 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DisError } from './chunk-MJO7IJZC.js';
|
|
2
|
+
|
|
3
|
+
// src/core/provider.ts
|
|
4
|
+
var activeProvider = null;
|
|
5
|
+
function resolvePlatformProvider() {
|
|
6
|
+
const candidate = globalThis.crypto;
|
|
7
|
+
if (!candidate || typeof candidate.getRandomValues !== "function" || !candidate.subtle) {
|
|
8
|
+
throw new DisError(
|
|
9
|
+
"PROVIDER_UNAVAILABLE",
|
|
10
|
+
"No WebCrypto provider available. Provide one via setCryptoProvider()."
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return candidate;
|
|
14
|
+
}
|
|
15
|
+
function getCryptoProvider() {
|
|
16
|
+
if (!activeProvider) {
|
|
17
|
+
activeProvider = resolvePlatformProvider();
|
|
18
|
+
}
|
|
19
|
+
return activeProvider;
|
|
20
|
+
}
|
|
21
|
+
function setCryptoProvider(provider) {
|
|
22
|
+
activeProvider = provider;
|
|
23
|
+
}
|
|
24
|
+
function subtle() {
|
|
25
|
+
return getCryptoProvider().subtle;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { getCryptoProvider, setCryptoProvider, subtle };
|
|
29
|
+
//# sourceMappingURL=chunk-CYIGDF63.js.map
|
|
30
|
+
//# sourceMappingURL=chunk-CYIGDF63.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/provider.ts"],"names":[],"mappings":";;;AAoBA,IAAI,cAAA,GAAwC,IAAA;AAE5C,SAAS,uBAAA,GAA0C;AAC/C,EAAA,MAAM,YAAa,UAAA,CAAmC,MAAA;AACtD,EAAA,IAAI,CAAC,aAAa,OAAO,SAAA,CAAU,oBAAoB,UAAA,IAAc,CAAC,UAAU,MAAA,EAAQ;AACpF,IAAA,MAAM,IAAI,QAAA;AAAA,MACN,sBAAA;AAAA,MACA;AAAA,KACJ;AAAA,EACJ;AACA,EAAA,OAAO,SAAA;AACX;AAGO,SAAS,iBAAA,GAAoC;AAChD,EAAA,IAAI,CAAC,cAAA,EAAgB;AACjB,IAAA,cAAA,GAAiB,uBAAA,EAAwB;AAAA,EAC7C;AACA,EAAA,OAAO,cAAA;AACX;AAOO,SAAS,kBAAkB,QAAA,EAAuC;AACrE,EAAA,cAAA,GAAiB,QAAA;AACrB;AAGO,SAAS,MAAA,GAAuB;AACnC,EAAA,OAAO,mBAAkB,CAAE,MAAA;AAC/B","file":"chunk-CYIGDF63.js","sourcesContent":["/**\r\n * Pluggable crypto provider abstraction.\r\n *\r\n * DIS does not invent cryptography. It binds to an audited primitive provider.\r\n * The default provider is the platform WebCrypto implementation\r\n * (`globalThis.crypto`), which is available in modern browsers and in Node >= 20.\r\n *\r\n * Exposing this as an interface keeps the door open for substituting a\r\n * hardware-backed or test provider without touching call sites, and keeps the\r\n * rest of DIS free of direct global access.\r\n */\r\n\r\nimport { DisError } from './errors.js';\r\n\r\n/** Minimal subset of the WebCrypto API that DIS relies on. */\r\nexport interface CryptoProvider {\r\n getRandomValues<T extends ArrayBufferView>(array: T): T;\r\n readonly subtle: SubtleCrypto;\r\n}\r\n\r\nlet activeProvider: CryptoProvider | null = null;\r\n\r\nfunction resolvePlatformProvider(): CryptoProvider {\r\n const candidate = (globalThis as { crypto?: Crypto }).crypto;\r\n if (!candidate || typeof candidate.getRandomValues !== 'function' || !candidate.subtle) {\r\n throw new DisError(\r\n 'PROVIDER_UNAVAILABLE',\r\n 'No WebCrypto provider available. Provide one via setCryptoProvider().',\r\n );\r\n }\r\n return candidate;\r\n}\r\n\r\n/** Returns the active crypto provider, falling back to platform WebCrypto. */\r\nexport function getCryptoProvider(): CryptoProvider {\r\n if (!activeProvider) {\r\n activeProvider = resolvePlatformProvider();\r\n }\r\n return activeProvider;\r\n}\r\n\r\n/**\r\n * Overrides the active crypto provider. Intended for tests and for\r\n * environments that supply a non-global WebCrypto implementation.\r\n * Pass `null` to reset to platform auto-detection.\r\n */\r\nexport function setCryptoProvider(provider: CryptoProvider | null): void {\r\n activeProvider = provider;\r\n}\r\n\r\n/** Convenience accessor for `SubtleCrypto`. */\r\nexport function subtle(): SubtleCrypto {\r\n return getCryptoProvider().subtle;\r\n}\r\n"]}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { sha256JsonBase64, sha256Base64 } from './chunk-MPWYZXW7.js';
|
|
2
|
+
import { randomBytes } from './chunk-3HCT6A2P.js';
|
|
3
|
+
import { importAesGcmKey } from './chunk-OPHN2B3N.js';
|
|
4
|
+
import { encryptBytes, decryptBytes } from './chunk-U65A4HIY.js';
|
|
5
|
+
import { AES_KEY_LENGTH } from './chunk-BUFRR5PB.js';
|
|
6
|
+
import { DisInvalidArgumentError } from './chunk-MJO7IJZC.js';
|
|
7
|
+
|
|
8
|
+
// src/file-encryption/index.ts
|
|
9
|
+
var DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024;
|
|
10
|
+
var FILE_MANIFEST_V1_PREFIX = "sv-file-manifest-v1:";
|
|
11
|
+
function manifestAad(ctx) {
|
|
12
|
+
return `sv-file-manifest-v1:${ctx.ownerId}:${ctx.vaultItemId}:${ctx.fileId}`;
|
|
13
|
+
}
|
|
14
|
+
function fileKeyAad(ctx) {
|
|
15
|
+
return `sv-file-key-v1:${ctx.ownerId}:${ctx.vaultItemId}:${ctx.fileId}`;
|
|
16
|
+
}
|
|
17
|
+
function chunkAad(ctx, fileRevision, manifestRoot, chunkIndex, chunkCount) {
|
|
18
|
+
return `sv-file-chunk-v1:${ctx.ownerId}:${ctx.vaultItemId}:${ctx.fileId}:${fileRevision}:${manifestRoot}:${chunkIndex}:${chunkCount}`;
|
|
19
|
+
}
|
|
20
|
+
function generateFileKeyBytes() {
|
|
21
|
+
return randomBytes(AES_KEY_LENGTH);
|
|
22
|
+
}
|
|
23
|
+
function importFileKey(rawKey) {
|
|
24
|
+
return importAesGcmKey(rawKey);
|
|
25
|
+
}
|
|
26
|
+
async function encryptChunk(plaintext, fileKey, aad) {
|
|
27
|
+
try {
|
|
28
|
+
return await encryptBytes(plaintext, fileKey, aad);
|
|
29
|
+
} finally {
|
|
30
|
+
plaintext.fill(0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function decryptChunk(encryptedBase64, fileKey, aad) {
|
|
34
|
+
return decryptBytes(encryptedBase64, fileKey, aad);
|
|
35
|
+
}
|
|
36
|
+
async function computeManifestRoot(input) {
|
|
37
|
+
return sha256JsonBase64({
|
|
38
|
+
file_id: input.fileId,
|
|
39
|
+
file_revision: input.fileRevision,
|
|
40
|
+
chunk_size: input.chunkSize,
|
|
41
|
+
chunk_count: input.chunkCount,
|
|
42
|
+
chunks: input.chunks.map((c) => ({
|
|
43
|
+
index: c.index,
|
|
44
|
+
storage_path: void 0,
|
|
45
|
+
plaintext_size: c.plaintext_size
|
|
46
|
+
}))
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function chunkCiphertextHash(ciphertextBase64) {
|
|
50
|
+
return sha256Base64(new TextEncoder().encode(ciphertextBase64));
|
|
51
|
+
}
|
|
52
|
+
async function encryptAttachment(input) {
|
|
53
|
+
const chunkSize = input.chunkSize ?? DEFAULT_CHUNK_SIZE;
|
|
54
|
+
if (chunkSize <= 0) throw new DisInvalidArgumentError("chunkSize must be positive");
|
|
55
|
+
const fileRevision = input.fileRevision ?? 1;
|
|
56
|
+
const chunkCount = Math.max(1, Math.ceil(input.totalSize / chunkSize));
|
|
57
|
+
const ctx = input.context;
|
|
58
|
+
const plannedChunks = Array.from({ length: chunkCount }, (_, index) => {
|
|
59
|
+
const start = index * chunkSize;
|
|
60
|
+
const end = Math.min(input.totalSize, start + chunkSize);
|
|
61
|
+
return { index, plaintext_size: end - start };
|
|
62
|
+
});
|
|
63
|
+
const manifestRoot = await computeManifestRoot({
|
|
64
|
+
fileId: ctx.fileId,
|
|
65
|
+
fileRevision,
|
|
66
|
+
chunkSize,
|
|
67
|
+
chunkCount,
|
|
68
|
+
chunks: plannedChunks
|
|
69
|
+
});
|
|
70
|
+
const fileKeyBytes = generateFileKeyBytes();
|
|
71
|
+
const fileKey = await importFileKey(fileKeyBytes);
|
|
72
|
+
const chunks = [];
|
|
73
|
+
try {
|
|
74
|
+
const wrappedFileKey = await input.wrapFileKey(fileKeyBytes, fileKeyAad(ctx));
|
|
75
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
76
|
+
const start = index * chunkSize;
|
|
77
|
+
const end = Math.min(input.totalSize, start + chunkSize);
|
|
78
|
+
const plaintext = await input.readChunk(start, end);
|
|
79
|
+
const aad = chunkAad(ctx, fileRevision, manifestRoot, index, chunkCount);
|
|
80
|
+
const ciphertext = await encryptChunk(plaintext, fileKey, aad);
|
|
81
|
+
const ciphertextSize = await input.writeChunk(index, ciphertext);
|
|
82
|
+
chunks.push({
|
|
83
|
+
index,
|
|
84
|
+
plaintext_size: plannedChunks[index].plaintext_size,
|
|
85
|
+
ciphertext_size: ciphertextSize,
|
|
86
|
+
ciphertext_sha256: await chunkCiphertextHash(ciphertext)
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const manifest = {
|
|
90
|
+
version: 1,
|
|
91
|
+
algorithm: "AES-256-GCM",
|
|
92
|
+
file_id: ctx.fileId,
|
|
93
|
+
file_revision: fileRevision,
|
|
94
|
+
previous_manifest_hash: null,
|
|
95
|
+
manifest_root: manifestRoot,
|
|
96
|
+
owner_id: ctx.ownerId,
|
|
97
|
+
vault_item_id: ctx.vaultItemId,
|
|
98
|
+
original_name: input.metadata.original_name,
|
|
99
|
+
mime_type: input.metadata.mime_type,
|
|
100
|
+
original_size: input.totalSize,
|
|
101
|
+
last_modified: input.metadata.last_modified,
|
|
102
|
+
uploaded_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
103
|
+
chunk_size: chunkSize,
|
|
104
|
+
chunk_count: chunkCount,
|
|
105
|
+
wrapped_file_key: wrappedFileKey,
|
|
106
|
+
chunks,
|
|
107
|
+
preview: null,
|
|
108
|
+
notes: null
|
|
109
|
+
};
|
|
110
|
+
return { manifest, manifestRoot };
|
|
111
|
+
} finally {
|
|
112
|
+
fileKeyBytes.fill(0);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function decryptAttachment(input) {
|
|
116
|
+
const { manifest, context: ctx } = input;
|
|
117
|
+
const verifyHashes = input.verifyChunkHashes ?? true;
|
|
118
|
+
const fileKeyBytes = await input.unwrapFileKey(manifest.wrapped_file_key, fileKeyAad(ctx));
|
|
119
|
+
const fileKey = await importFileKey(fileKeyBytes);
|
|
120
|
+
try {
|
|
121
|
+
fileKeyBytes.fill(0);
|
|
122
|
+
for (const chunkMeta of manifest.chunks) {
|
|
123
|
+
const ciphertext = await input.readChunk(chunkMeta.index, chunkMeta.ciphertext_sha256);
|
|
124
|
+
if (verifyHashes) {
|
|
125
|
+
const actual = await chunkCiphertextHash(ciphertext);
|
|
126
|
+
if (actual !== chunkMeta.ciphertext_sha256) {
|
|
127
|
+
throw new DisInvalidArgumentError(
|
|
128
|
+
`Chunk ${chunkMeta.index} ciphertext hash mismatch`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const aad = chunkAad(
|
|
133
|
+
ctx,
|
|
134
|
+
manifest.file_revision,
|
|
135
|
+
manifest.manifest_root,
|
|
136
|
+
chunkMeta.index,
|
|
137
|
+
manifest.chunk_count
|
|
138
|
+
);
|
|
139
|
+
const plaintext = await decryptChunk(ciphertext, fileKey, aad);
|
|
140
|
+
try {
|
|
141
|
+
await input.writeChunk(chunkMeta.index, plaintext);
|
|
142
|
+
} finally {
|
|
143
|
+
plaintext.fill(0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
fileKeyBytes.fill(0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { DEFAULT_CHUNK_SIZE, FILE_MANIFEST_V1_PREFIX, chunkAad, chunkCiphertextHash, computeManifestRoot, decryptAttachment, decryptChunk, encryptAttachment, encryptChunk, fileKeyAad, generateFileKeyBytes, importFileKey, manifestAad };
|
|
152
|
+
//# sourceMappingURL=chunk-EOXWR7DS.js.map
|
|
153
|
+
//# sourceMappingURL=chunk-EOXWR7DS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/file-encryption/index.ts"],"names":[],"mappings":"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","file":"chunk-EOXWR7DS.js","sourcesContent":["/**\r\n * dis-file-encryption / dis-attachment-streams — chunked file & attachment\r\n * encryption.\r\n *\r\n * Model (byte-compatible with Singra Premium):\r\n * - A fresh random per-file AES-256 key encrypts the file content.\r\n * - The file is split into fixed-size chunks; each chunk is sealed with\r\n * AES-256-GCM and a per-chunk AAD binding it to owner/item/file/revision/\r\n * manifest-root/index/count (defeats reorder, splice and cross-file swap).\r\n * - The file key is wrapped by an outer vault key (supplied as a callback),\r\n * bound by a file-key AAD.\r\n * - A manifest records chunk hashes and a manifest root; it is sealed with\r\n * the vault key under a manifest AAD and wrapped in `sv-file-manifest-v1:`.\r\n *\r\n * DIS owns the cryptography and the format. It is storage-agnostic: callers\r\n * supply chunk read/write callbacks, so transport (e.g. object storage, local\r\n * FS) stays in the application.\r\n */\r\n\r\nimport { decryptBytes, encryptBytes } from '../aead/index.js';\r\nimport { importAesGcmKey } from '../kdf/index.js';\r\nimport { randomBytes } from '../random/index.js';\r\nimport { sha256Base64, sha256JsonBase64 } from '../integrity/index.js';\r\nimport { AES_KEY_LENGTH } from '../core/constants.js';\r\nimport { DisInvalidArgumentError } from '../core/errors.js';\r\n\r\n/** Default chunk size (4 MiB), matching the Singra Premium format. */\r\nexport const DEFAULT_CHUNK_SIZE = 4 * 1024 * 1024;\r\n\r\nexport const FILE_MANIFEST_V1_PREFIX = 'sv-file-manifest-v1:';\r\n\r\n/** Opaque binding context for an attachment. Ids are treated as opaque strings. */\r\nexport interface AttachmentContext {\r\n readonly ownerId: string;\r\n readonly vaultItemId: string;\r\n readonly fileId: string;\r\n}\r\n\r\n/** Encrypts text with the outer vault key (e.g. DIS aead.encryptString). */\r\nexport type VaultEncryptText = (plaintext: string, aad?: string) => Promise<string>;\r\nexport type VaultDecryptText = (encrypted: string, aad?: string) => Promise<string>;\r\nexport type VaultEncryptBytes = (plaintext: Uint8Array, aad?: string) => Promise<string>;\r\nexport type VaultDecryptBytes = (encrypted: string, aad?: string) => Promise<Uint8Array>;\r\n\r\nexport interface FileChunkManifest {\r\n readonly index: number;\r\n readonly plaintext_size: number;\r\n readonly ciphertext_size: number;\r\n readonly ciphertext_sha256: string;\r\n}\r\n\r\nexport interface FileManifestV1 {\r\n readonly version: 1;\r\n readonly algorithm: 'AES-256-GCM';\r\n readonly file_id: string;\r\n readonly file_revision: number;\r\n readonly previous_manifest_hash: string | null;\r\n readonly manifest_root: string;\r\n readonly owner_id: string;\r\n readonly vault_item_id: string;\r\n readonly original_name: string;\r\n readonly mime_type: string | null;\r\n readonly original_size: number;\r\n readonly last_modified: number | null;\r\n readonly uploaded_at: string;\r\n readonly chunk_size: number;\r\n readonly chunk_count: number;\r\n readonly wrapped_file_key: string;\r\n readonly chunks: readonly FileChunkManifest[];\r\n readonly preview: null;\r\n readonly notes: null;\r\n}\r\n\r\n// ---- Canonical AAD scheme (format contract) -------------------------------\r\n\r\nexport function manifestAad(ctx: AttachmentContext): string {\r\n return `sv-file-manifest-v1:${ctx.ownerId}:${ctx.vaultItemId}:${ctx.fileId}`;\r\n}\r\n\r\nexport function fileKeyAad(ctx: AttachmentContext): string {\r\n return `sv-file-key-v1:${ctx.ownerId}:${ctx.vaultItemId}:${ctx.fileId}`;\r\n}\r\n\r\nexport function chunkAad(\r\n ctx: AttachmentContext,\r\n fileRevision: number,\r\n manifestRoot: string,\r\n chunkIndex: number,\r\n chunkCount: number,\r\n): string {\r\n return (\r\n `sv-file-chunk-v1:${ctx.ownerId}:${ctx.vaultItemId}:${ctx.fileId}:` +\r\n `${fileRevision}:${manifestRoot}:${chunkIndex}:${chunkCount}`\r\n );\r\n}\r\n\r\n// ---- File-key handling ----------------------------------------------------\r\n\r\n/** Generates fresh per-file AES-256 key bytes (caller must wipe). */\r\nexport function generateFileKeyBytes(): Uint8Array {\r\n return randomBytes(AES_KEY_LENGTH);\r\n}\r\n\r\n/** Imports raw file-key bytes as a non-extractable AES-GCM key. */\r\nexport function importFileKey(rawKey: Uint8Array): Promise<CryptoKey> {\r\n return importAesGcmKey(rawKey);\r\n}\r\n\r\n// ---- Chunk crypto ---------------------------------------------------------\r\n\r\n/** Seals one plaintext chunk. `plaintext` is wiped before returning. */\r\nexport async function encryptChunk(\r\n plaintext: Uint8Array,\r\n fileKey: CryptoKey,\r\n aad: string,\r\n): Promise<string> {\r\n try {\r\n return await encryptBytes(plaintext, fileKey, aad);\r\n } finally {\r\n plaintext.fill(0);\r\n }\r\n}\r\n\r\n/** Opens one chunk. Returned bytes are plaintext — caller must wipe. */\r\nexport async function decryptChunk(\r\n encryptedBase64: string,\r\n fileKey: CryptoKey,\r\n aad: string,\r\n): Promise<Uint8Array> {\r\n return decryptBytes(encryptedBase64, fileKey, aad);\r\n}\r\n\r\n// ---- Manifest helpers -----------------------------------------------------\r\n\r\ninterface PlannedChunk {\r\n readonly index: number;\r\n readonly plaintext_size: number;\r\n}\r\n\r\n/**\r\n * Computes the manifest root: a SHA-256 over the planned chunk layout. Binding\r\n * each chunk's AAD to this root prevents chunk-count / size tampering.\r\n */\r\nexport async function computeManifestRoot(input: {\r\n fileId: string;\r\n fileRevision: number;\r\n chunkSize: number;\r\n chunkCount: number;\r\n chunks: readonly PlannedChunk[];\r\n}): Promise<string> {\r\n return sha256JsonBase64({\r\n file_id: input.fileId,\r\n file_revision: input.fileRevision,\r\n chunk_size: input.chunkSize,\r\n chunk_count: input.chunkCount,\r\n chunks: input.chunks.map((c) => ({\r\n index: c.index,\r\n storage_path: undefined,\r\n plaintext_size: c.plaintext_size,\r\n })),\r\n });\r\n}\r\n\r\n/** SHA-256 (base64) of a ciphertext chunk, for the manifest. */\r\nexport function chunkCiphertextHash(ciphertextBase64: string): Promise<string> {\r\n return sha256Base64(new TextEncoder().encode(ciphertextBase64));\r\n}\r\n\r\nexport interface EncryptAttachmentInput {\r\n readonly context: AttachmentContext;\r\n readonly fileRevision?: number;\r\n readonly chunkSize?: number;\r\n /** Total plaintext size in bytes (used to plan chunks). */\r\n readonly totalSize: number;\r\n /** Returns the plaintext bytes for chunk `[start, end)`. */\r\n readonly readChunk: (start: number, end: number) => Promise<Uint8Array>;\r\n /** Persists a sealed chunk; returns its stored ciphertext size in bytes. */\r\n readonly writeChunk: (index: number, ciphertextBase64: string) => Promise<number>;\r\n /** Wraps the per-file key with the outer vault key. */\r\n readonly wrapFileKey: VaultEncryptBytes;\r\n readonly metadata: {\r\n readonly original_name: string;\r\n readonly mime_type: string | null;\r\n readonly last_modified: number | null;\r\n };\r\n}\r\n\r\nexport interface EncryptAttachmentResult {\r\n readonly manifest: FileManifestV1;\r\n readonly manifestRoot: string;\r\n}\r\n\r\n/**\r\n * Encrypts an attachment chunk-by-chunk and produces a sealed manifest.\r\n * Storage is delegated to `readChunk`/`writeChunk`. The returned manifest's\r\n * `wrapped_file_key` is bound to the file via {@link fileKeyAad}.\r\n */\r\nexport async function encryptAttachment(\r\n input: EncryptAttachmentInput,\r\n): Promise<EncryptAttachmentResult> {\r\n const chunkSize = input.chunkSize ?? DEFAULT_CHUNK_SIZE;\r\n if (chunkSize <= 0) throw new DisInvalidArgumentError('chunkSize must be positive');\r\n const fileRevision = input.fileRevision ?? 1;\r\n const chunkCount = Math.max(1, Math.ceil(input.totalSize / chunkSize));\r\n const ctx = input.context;\r\n\r\n const plannedChunks: PlannedChunk[] = Array.from({ length: chunkCount }, (_, index) => {\r\n const start = index * chunkSize;\r\n const end = Math.min(input.totalSize, start + chunkSize);\r\n return { index, plaintext_size: end - start };\r\n });\r\n const manifestRoot = await computeManifestRoot({\r\n fileId: ctx.fileId,\r\n fileRevision,\r\n chunkSize,\r\n chunkCount,\r\n chunks: plannedChunks,\r\n });\r\n\r\n const fileKeyBytes = generateFileKeyBytes();\r\n const fileKey = await importFileKey(fileKeyBytes);\r\n const chunks: FileChunkManifest[] = [];\r\n try {\r\n const wrappedFileKey = await input.wrapFileKey(fileKeyBytes, fileKeyAad(ctx));\r\n for (let index = 0; index < chunkCount; index += 1) {\r\n const start = index * chunkSize;\r\n const end = Math.min(input.totalSize, start + chunkSize);\r\n const plaintext = await input.readChunk(start, end);\r\n const aad = chunkAad(ctx, fileRevision, manifestRoot, index, chunkCount);\r\n const ciphertext = await encryptChunk(plaintext, fileKey, aad);\r\n const ciphertextSize = await input.writeChunk(index, ciphertext);\r\n chunks.push({\r\n index,\r\n plaintext_size: plannedChunks[index]!.plaintext_size,\r\n ciphertext_size: ciphertextSize,\r\n ciphertext_sha256: await chunkCiphertextHash(ciphertext),\r\n });\r\n }\r\n\r\n const manifest: FileManifestV1 = {\r\n version: 1,\r\n algorithm: 'AES-256-GCM',\r\n file_id: ctx.fileId,\r\n file_revision: fileRevision,\r\n previous_manifest_hash: null,\r\n manifest_root: manifestRoot,\r\n owner_id: ctx.ownerId,\r\n vault_item_id: ctx.vaultItemId,\r\n original_name: input.metadata.original_name,\r\n mime_type: input.metadata.mime_type,\r\n original_size: input.totalSize,\r\n last_modified: input.metadata.last_modified,\r\n uploaded_at: new Date().toISOString(),\r\n chunk_size: chunkSize,\r\n chunk_count: chunkCount,\r\n wrapped_file_key: wrappedFileKey,\r\n chunks,\r\n preview: null,\r\n notes: null,\r\n };\r\n return { manifest, manifestRoot };\r\n } finally {\r\n fileKeyBytes.fill(0);\r\n }\r\n}\r\n\r\nexport interface DecryptAttachmentInput {\r\n readonly context: AttachmentContext;\r\n readonly manifest: FileManifestV1;\r\n /** Reads a stored ciphertext chunk by index. */\r\n readonly readChunk: (index: number, storedSha256: string) => Promise<string>;\r\n /** Receives a decrypted plaintext chunk (caller may stream to disk). */\r\n readonly writeChunk: (index: number, plaintext: Uint8Array) => Promise<void>;\r\n /** Unwraps the per-file key using the outer vault key. */\r\n readonly unwrapFileKey: VaultDecryptBytes;\r\n /** If true (default), verify each chunk's stored ciphertext hash. */\r\n readonly verifyChunkHashes?: boolean;\r\n}\r\n\r\n/**\r\n * Decrypts an attachment by streaming chunks through `readChunk`/`writeChunk`.\r\n * Each chunk is authenticated by its AAD; when `verifyChunkHashes` is set the\r\n * stored ciphertext hash is additionally checked before decryption.\r\n */\r\nexport async function decryptAttachment(input: DecryptAttachmentInput): Promise<void> {\r\n const { manifest, context: ctx } = input;\r\n const verifyHashes = input.verifyChunkHashes ?? true;\r\n const fileKeyBytes = await input.unwrapFileKey(manifest.wrapped_file_key, fileKeyAad(ctx));\r\n const fileKey = await importFileKey(fileKeyBytes);\r\n try {\r\n fileKeyBytes.fill(0);\r\n for (const chunkMeta of manifest.chunks) {\r\n const ciphertext = await input.readChunk(chunkMeta.index, chunkMeta.ciphertext_sha256);\r\n if (verifyHashes) {\r\n const actual = await chunkCiphertextHash(ciphertext);\r\n if (actual !== chunkMeta.ciphertext_sha256) {\r\n throw new DisInvalidArgumentError(\r\n `Chunk ${chunkMeta.index} ciphertext hash mismatch`,\r\n );\r\n }\r\n }\r\n const aad = chunkAad(\r\n ctx,\r\n manifest.file_revision,\r\n manifest.manifest_root,\r\n chunkMeta.index,\r\n manifest.chunk_count,\r\n );\r\n const plaintext = await decryptChunk(ciphertext, fileKey, aad);\r\n try {\r\n await input.writeChunk(chunkMeta.index, plaintext);\r\n } finally {\r\n plaintext.fill(0);\r\n }\r\n }\r\n } finally {\r\n fileKeyBytes.fill(0);\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { DisInvalidArgumentError, DisError } from './chunk-MJO7IJZC.js';
|
|
2
|
+
|
|
3
|
+
// src/migrations/index.ts
|
|
4
|
+
var MigrationRegistry = class {
|
|
5
|
+
migrations = /* @__PURE__ */ new Map();
|
|
6
|
+
keyOf(subject, fromVersion) {
|
|
7
|
+
return `${subject}@${fromVersion}`;
|
|
8
|
+
}
|
|
9
|
+
/** Registers a migration. Throws if one already exists for the same step. */
|
|
10
|
+
register(migration) {
|
|
11
|
+
const key = this.keyOf(migration.subject, migration.fromVersion);
|
|
12
|
+
if (this.migrations.has(key)) {
|
|
13
|
+
throw new DisInvalidArgumentError(`Duplicate migration for ${key}`);
|
|
14
|
+
}
|
|
15
|
+
this.migrations.set(key, migration);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Applies all applicable migrations in sequence until no further migration
|
|
20
|
+
* exists for the payload's detected version. Guards against cycles.
|
|
21
|
+
*/
|
|
22
|
+
async migrateToLatest(subject, payload, detect, context) {
|
|
23
|
+
let current = payload;
|
|
24
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25
|
+
for (; ; ) {
|
|
26
|
+
const version = detect(current);
|
|
27
|
+
if (seen.has(version)) {
|
|
28
|
+
throw new DisError(
|
|
29
|
+
"INVALID_ARGUMENT",
|
|
30
|
+
`Migration cycle detected for ${subject}@${version}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
seen.add(version);
|
|
34
|
+
const migration = this.migrations.get(this.keyOf(subject, version));
|
|
35
|
+
if (!migration) return current;
|
|
36
|
+
current = await migration.migrate(current, context);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { MigrationRegistry };
|
|
42
|
+
//# sourceMappingURL=chunk-FUDDBD2G.js.map
|
|
43
|
+
//# sourceMappingURL=chunk-FUDDBD2G.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/migrations/index.ts"],"names":[],"mappings":";;;AAkCO,IAAM,oBAAN,MAAwB;AAAA,EACV,UAAA,uBAAiB,GAAA,EAAuB;AAAA,EAEjD,KAAA,CAAM,SAAiB,WAAA,EAAwC;AACnE,IAAA,OAAO,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,WAAW,CAAA,CAAA;AAAA,EACpC;AAAA;AAAA,EAGA,SAAS,SAAA,EAA4B;AACjC,IAAA,MAAM,MAAM,IAAA,CAAK,KAAA,CAAM,SAAA,CAAU,OAAA,EAAS,UAAU,WAAW,CAAA;AAC/D,IAAA,IAAI,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,GAAG,CAAA,EAAG;AAC1B,MAAA,MAAM,IAAI,uBAAA,CAAwB,CAAA,wBAAA,EAA2B,GAAG,CAAA,CAAE,CAAA;AAAA,IACtE;AACA,IAAA,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,GAAA,EAAK,SAAS,CAAA;AAClC,IAAA,OAAO,IAAA;AAAA,EACX;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAA,CACF,OAAA,EACA,OAAA,EACA,QACA,OAAA,EACe;AACf,IAAA,IAAI,OAAA,GAAU,OAAA;AACd,IAAA,MAAM,IAAA,uBAAW,GAAA,EAAuB;AACxC,IAAA,WAAS;AACL,MAAA,MAAM,OAAA,GAAU,OAAO,OAAO,CAAA;AAC9B,MAAA,IAAI,IAAA,CAAK,GAAA,CAAI,OAAO,CAAA,EAAG;AACnB,QAAA,MAAM,IAAI,QAAA;AAAA,UACN,kBAAA;AAAA,UACA,CAAA,6BAAA,EAAgC,OAAO,CAAA,CAAA,EAAI,OAAO,CAAA;AAAA,SACtD;AAAA,MACJ;AACA,MAAA,IAAA,CAAK,IAAI,OAAO,CAAA;AAChB,MAAA,MAAM,SAAA,GAAY,KAAK,UAAA,CAAW,GAAA,CAAI,KAAK,KAAA,CAAM,OAAA,EAAS,OAAO,CAAC,CAAA;AAClE,MAAA,IAAI,CAAC,WAAW,OAAO,OAAA;AACvB,MAAA,OAAA,GAAU,MAAM,SAAA,CAAU,OAAA,CAAQ,OAAA,EAAS,OAAO,CAAA;AAAA,IACtD;AAAA,EACJ;AACJ","file":"chunk-FUDDBD2G.js","sourcesContent":["/**\r\n * dis-migrations — explicit, ordered transformation of encrypted payloads.\r\n *\r\n * Migrations are registered against a (subject, fromVersion) key and run in a\r\n * deterministic order. This gives applications a single, testable place to\r\n * evolve formats (e.g. re-wrap legacy no-AAD vault items, bump KDF parameters,\r\n * re-encrypt under a new cipher) without scattering ad-hoc upgrade code.\r\n *\r\n * DIS provides the framework and the crypto; the application decides which\r\n * migrations to register and how persistence happens.\r\n */\r\n\r\nimport { DisError, DisInvalidArgumentError } from '../core/errors.js';\r\n\r\n/** Context passed to a migration step. `key` material is caller-supplied. */\r\nexport interface MigrationContext {\r\n readonly key: CryptoKey;\r\n /** Stable identifier the payload is bound to (e.g. entry id). */\r\n readonly bindingId?: string;\r\n}\r\n\r\n/** A single migration: transforms a payload from `fromVersion` to `toVersion`. */\r\nexport interface Migration {\r\n readonly subject: string;\r\n readonly fromVersion: number | 'legacy';\r\n readonly toVersion: number;\r\n /** Returns the migrated payload string. Must be idempotent on its output. */\r\n migrate(payload: string, context: MigrationContext): Promise<string>;\r\n}\r\n\r\n/** Detects the current version of a payload for a subject. */\r\nexport type VersionDetector = (payload: string) => number | 'legacy';\r\n\r\n/** An ordered registry of migrations for one or more subjects. */\r\nexport class MigrationRegistry {\r\n private readonly migrations = new Map<string, Migration>();\r\n\r\n private keyOf(subject: string, fromVersion: number | 'legacy'): string {\r\n return `${subject}@${fromVersion}`;\r\n }\r\n\r\n /** Registers a migration. Throws if one already exists for the same step. */\r\n register(migration: Migration): this {\r\n const key = this.keyOf(migration.subject, migration.fromVersion);\r\n if (this.migrations.has(key)) {\r\n throw new DisInvalidArgumentError(`Duplicate migration for ${key}`);\r\n }\r\n this.migrations.set(key, migration);\r\n return this;\r\n }\r\n\r\n /**\r\n * Applies all applicable migrations in sequence until no further migration\r\n * exists for the payload's detected version. Guards against cycles.\r\n */\r\n async migrateToLatest(\r\n subject: string,\r\n payload: string,\r\n detect: VersionDetector,\r\n context: MigrationContext,\r\n ): Promise<string> {\r\n let current = payload;\r\n const seen = new Set<number | 'legacy'>();\r\n for (;;) {\r\n const version = detect(current);\r\n if (seen.has(version)) {\r\n throw new DisError(\r\n 'INVALID_ARGUMENT',\r\n `Migration cycle detected for ${subject}@${version}`,\r\n );\r\n }\r\n seen.add(version);\r\n const migration = this.migrations.get(this.keyOf(subject, version));\r\n if (!migration) return current;\r\n current = await migration.migrate(current, context);\r\n }\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/core/encoding.ts
|
|
2
|
+
function bytesToBase64(bytes) {
|
|
3
|
+
let binary = "";
|
|
4
|
+
const chunkSize = 32768;
|
|
5
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
6
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
7
|
+
binary += String.fromCharCode(...chunk);
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary);
|
|
10
|
+
}
|
|
11
|
+
function base64ToBytes(base64) {
|
|
12
|
+
const binary = atob(base64);
|
|
13
|
+
const bytes = new Uint8Array(binary.length);
|
|
14
|
+
for (let i = 0; i < binary.length; i++) {
|
|
15
|
+
bytes[i] = binary.charCodeAt(i);
|
|
16
|
+
}
|
|
17
|
+
return bytes;
|
|
18
|
+
}
|
|
19
|
+
var textEncoder = new TextEncoder();
|
|
20
|
+
var textDecoder = new TextDecoder();
|
|
21
|
+
function utf8ToBytes(text) {
|
|
22
|
+
return textEncoder.encode(text);
|
|
23
|
+
}
|
|
24
|
+
function bytesToUtf8(bytes) {
|
|
25
|
+
return textDecoder.decode(bytes);
|
|
26
|
+
}
|
|
27
|
+
function bytesToBase64Url(bytes) {
|
|
28
|
+
return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
29
|
+
}
|
|
30
|
+
function base64UrlToBytes(base64url) {
|
|
31
|
+
const normalized = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
32
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
33
|
+
return base64ToBytes(padded);
|
|
34
|
+
}
|
|
35
|
+
function bytesToHex(bytes) {
|
|
36
|
+
let hex = "";
|
|
37
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
38
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
39
|
+
}
|
|
40
|
+
return hex;
|
|
41
|
+
}
|
|
42
|
+
function concatBytes(...parts) {
|
|
43
|
+
let total = 0;
|
|
44
|
+
for (const part of parts) total += part.length;
|
|
45
|
+
const out = new Uint8Array(total);
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
out.set(part, offset);
|
|
49
|
+
offset += part.length;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { base64ToBytes, base64UrlToBytes, bytesToBase64, bytesToBase64Url, bytesToHex, bytesToUtf8, concatBytes, utf8ToBytes };
|
|
55
|
+
//# sourceMappingURL=chunk-JSKIWIEC.js.map
|
|
56
|
+
//# sourceMappingURL=chunk-JSKIWIEC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/encoding.ts"],"names":[],"mappings":";AAeO,SAAS,cAAc,KAAA,EAA2B;AACrD,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,MAAM,SAAA,GAAY,KAAA;AAClB,EAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,SAAA,EAAW;AAC9C,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,QAAA,CAAS,CAAA,EAAG,IAAI,SAAS,CAAA;AAC7C,IAAA,MAAA,IAAU,MAAA,CAAO,YAAA,CAAa,GAAG,KAAK,CAAA;AAAA,EAC1C;AACA,EAAA,OAAO,KAAK,MAAM,CAAA;AACtB;AAGO,SAAS,cAAc,MAAA,EAA4B;AACtD,EAAA,MAAM,MAAA,GAAS,KAAK,MAAM,CAAA;AAC1B,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,MAAA,CAAO,MAAM,CAAA;AAC1C,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AACpC,IAAA,KAAA,CAAM,CAAC,CAAA,GAAI,MAAA,CAAO,UAAA,CAAW,CAAC,CAAA;AAAA,EAClC;AACA,EAAA,OAAO,KAAA;AACX;AAEA,IAAM,WAAA,GAAc,IAAI,WAAA,EAAY;AACpC,IAAM,WAAA,GAAc,IAAI,WAAA,EAAY;AAG7B,SAAS,YAAY,IAAA,EAA0B;AAClD,EAAA,OAAO,WAAA,CAAY,OAAO,IAAI,CAAA;AAClC;AAGO,SAAS,YAAY,KAAA,EAA2B;AACnD,EAAA,OAAO,WAAA,CAAY,OAAO,KAAK,CAAA;AACnC;AASO,SAAS,iBAAiB,KAAA,EAA2B;AACxD,EAAA,OAAO,aAAA,CAAc,KAAK,CAAA,CACrB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAClB,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAC3B;AAGO,SAAS,iBAAiB,SAAA,EAA+B;AAC5D,EAAA,MAAM,UAAA,GAAa,UAAU,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA,CAAE,OAAA,CAAQ,MAAM,GAAG,CAAA;AACjE,EAAA,MAAM,MAAA,GAAS,aAAa,GAAA,CAAI,MAAA,CAAA,CAAQ,IAAK,UAAA,CAAW,MAAA,GAAS,KAAM,CAAC,CAAA;AACxE,EAAA,OAAO,cAAc,MAAM,CAAA;AAC/B;AAGO,SAAS,WAAW,KAAA,EAA2B;AAClD,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACnC,IAAA,GAAA,IAAO,KAAA,CAAM,CAAC,CAAA,CAAG,QAAA,CAAS,EAAE,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAAA,EACjD;AACA,EAAA,OAAO,GAAA;AACX;AAGO,SAAS,eAAe,KAAA,EAAiC;AAC5D,EAAA,IAAI,KAAA,GAAQ,CAAA;AACZ,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,EAAO,KAAA,IAAS,IAAA,CAAK,MAAA;AACxC,EAAA,MAAM,GAAA,GAAM,IAAI,UAAA,CAAW,KAAK,CAAA;AAChC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACtB,IAAA,GAAA,CAAI,GAAA,CAAI,MAAM,MAAM,CAAA;AACpB,IAAA,MAAA,IAAU,IAAA,CAAK,MAAA;AAAA,EACnB;AACA,EAAA,OAAO,GAAA;AACX","file":"chunk-JSKIWIEC.js","sourcesContent":["/**\r\n * Byte/string/base64 encoding helpers.\r\n *\r\n * These intentionally reproduce the exact base64 encoding used by Singra Vault\r\n * and Singra Premium so that DIS is byte-compatible with already-stored\r\n * ciphertext. Do not \"optimise\" the algorithm in a way that changes output.\r\n */\r\n\r\n/**\r\n * Encodes bytes to a standard (RFC 4648) base64 string.\r\n *\r\n * Uses a chunked loop over `String.fromCharCode` + `btoa` to stay identical to\r\n * the legacy `uint8ArrayToBase64` implementation while avoiding call-stack\r\n * overflows on large inputs.\r\n */\r\nexport function bytesToBase64(bytes: Uint8Array): string {\r\n let binary = '';\r\n const chunkSize = 0x8000;\r\n for (let i = 0; i < bytes.length; i += chunkSize) {\r\n const chunk = bytes.subarray(i, i + chunkSize);\r\n binary += String.fromCharCode(...chunk);\r\n }\r\n return btoa(binary);\r\n}\r\n\r\n/** Decodes a standard base64 string to bytes. */\r\nexport function base64ToBytes(base64: string): Uint8Array {\r\n const binary = atob(base64);\r\n const bytes = new Uint8Array(binary.length);\r\n for (let i = 0; i < binary.length; i++) {\r\n bytes[i] = binary.charCodeAt(i);\r\n }\r\n return bytes;\r\n}\r\n\r\nconst textEncoder = new TextEncoder();\r\nconst textDecoder = new TextDecoder();\r\n\r\n/** UTF-8 encodes a string to bytes. */\r\nexport function utf8ToBytes(text: string): Uint8Array {\r\n return textEncoder.encode(text);\r\n}\r\n\r\n/** UTF-8 decodes bytes to a string. */\r\nexport function bytesToUtf8(bytes: Uint8Array): string {\r\n return textDecoder.decode(bytes);\r\n}\r\n\r\n/**\r\n * Encodes bytes to an unpadded base64url (RFC 4648 §5) string.\r\n *\r\n * Reproduces the exact transform used by Singra Vault's `encodeBase64Url`\r\n * (standard base64 with `+`→`-`, `/`→`_`, trailing `=` stripped) so DIS is\r\n * byte-compatible with stored op-log signatures, hashes and public keys.\r\n */\r\nexport function bytesToBase64Url(bytes: Uint8Array): string {\r\n return bytesToBase64(bytes)\r\n .replace(/\\+/g, '-')\r\n .replace(/\\//g, '_')\r\n .replace(/=+$/g, '');\r\n}\r\n\r\n/** Decodes an unpadded base64url string to bytes. */\r\nexport function base64UrlToBytes(base64url: string): Uint8Array {\r\n const normalized = base64url.replace(/-/g, '+').replace(/_/g, '/');\r\n const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);\r\n return base64ToBytes(padded);\r\n}\r\n\r\n/** Lower-case hex string of the given bytes. */\r\nexport function bytesToHex(bytes: Uint8Array): string {\r\n let hex = '';\r\n for (let i = 0; i < bytes.length; i++) {\r\n hex += bytes[i]!.toString(16).padStart(2, '0');\r\n }\r\n return hex;\r\n}\r\n\r\n/** Concatenates byte arrays into a single new buffer. */\r\nexport function concatBytes(...parts: Uint8Array[]): Uint8Array {\r\n let total = 0;\r\n for (const part of parts) total += part.length;\r\n const out = new Uint8Array(total);\r\n let offset = 0;\r\n for (const part of parts) {\r\n out.set(part, offset);\r\n offset += part.length;\r\n }\r\n return out;\r\n}\r\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { encryptString, decryptString } from './chunk-U65A4HIY.js';
|
|
2
|
+
import { formatEnvelope, parseEnvelope, isCurrentEnvelope } from './chunk-SCHZI6YY.js';
|
|
3
|
+
import { DisInvalidArgumentError, DisLegacyPayloadError } from './chunk-MJO7IJZC.js';
|
|
4
|
+
|
|
5
|
+
// src/vault-encryption/index.ts
|
|
6
|
+
var VAULT_ITEM_ENVELOPE_V1_PREFIX = "sv-vault-v1:";
|
|
7
|
+
var VAULT_ITEM_ENVELOPE_FAMILY_PREFIX = "sv-vault-";
|
|
8
|
+
var VAULT_ITEM_ENVELOPE_SPEC = {
|
|
9
|
+
currentPrefix: VAULT_ITEM_ENVELOPE_V1_PREFIX,
|
|
10
|
+
familyPrefix: VAULT_ITEM_ENVELOPE_FAMILY_PREFIX,
|
|
11
|
+
subject: "vault item"
|
|
12
|
+
};
|
|
13
|
+
async function encryptVaultEntry(data, key, entryId) {
|
|
14
|
+
if (!entryId) {
|
|
15
|
+
throw new DisInvalidArgumentError("entryId is required to bind vault entry ciphertext");
|
|
16
|
+
}
|
|
17
|
+
const json = JSON.stringify(data);
|
|
18
|
+
return formatEnvelope(VAULT_ITEM_ENVELOPE_SPEC, await encryptString(json, key, entryId));
|
|
19
|
+
}
|
|
20
|
+
async function decryptVaultEntry(encryptedData, key, entryId) {
|
|
21
|
+
const envelope = parseEnvelope(VAULT_ITEM_ENVELOPE_SPEC, encryptedData);
|
|
22
|
+
if (envelope.version === 1) {
|
|
23
|
+
return JSON.parse(await decryptString(envelope.payload, key, entryId));
|
|
24
|
+
}
|
|
25
|
+
if (entryId) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(
|
|
28
|
+
await decryptString(envelope.payload, key, entryId)
|
|
29
|
+
);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new DisLegacyPayloadError("Legacy vault item without AAD requires migration.");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new DisLegacyPayloadError("Legacy vault item without AAD requires migration.");
|
|
35
|
+
}
|
|
36
|
+
async function decryptVaultEntryForMigration(encryptedData, key, entryId) {
|
|
37
|
+
const envelope = parseEnvelope(VAULT_ITEM_ENVELOPE_SPEC, encryptedData);
|
|
38
|
+
if (envelope.version === 1) {
|
|
39
|
+
return {
|
|
40
|
+
data: JSON.parse(await decryptString(envelope.payload, key, entryId)),
|
|
41
|
+
legacyEnvelopeUsed: false,
|
|
42
|
+
legacyNoAadFallbackUsed: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (entryId) {
|
|
46
|
+
try {
|
|
47
|
+
return {
|
|
48
|
+
data: JSON.parse(
|
|
49
|
+
await decryptString(envelope.payload, key, entryId)
|
|
50
|
+
),
|
|
51
|
+
legacyEnvelopeUsed: true,
|
|
52
|
+
legacyNoAadFallbackUsed: false
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const data = JSON.parse(await decryptString(envelope.payload, key));
|
|
58
|
+
return { data, legacyEnvelopeUsed: true, legacyNoAadFallbackUsed: true };
|
|
59
|
+
}
|
|
60
|
+
function isCurrentVaultEntryEnvelope(encryptedData) {
|
|
61
|
+
return isCurrentEnvelope(VAULT_ITEM_ENVELOPE_SPEC, encryptedData);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { VAULT_ITEM_ENVELOPE_SPEC, VAULT_ITEM_ENVELOPE_V1_PREFIX, decryptVaultEntry, decryptVaultEntryForMigration, encryptVaultEntry, isCurrentVaultEntryEnvelope };
|
|
65
|
+
//# sourceMappingURL=chunk-JVFP2GAO.js.map
|
|
66
|
+
//# sourceMappingURL=chunk-JVFP2GAO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vault-encryption/index.ts"],"names":[],"mappings":";;;;;AAqBO,IAAM,6BAAA,GAAgC;AAC7C,IAAM,iCAAA,GAAoC,WAAA;AAEnC,IAAM,wBAAA,GAAwD;AAAA,EACjE,aAAA,EAAe,6BAAA;AAAA,EACf,YAAA,EAAc,iCAAA;AAAA,EACd,OAAA,EAAS;AACb;AAKA,eAAsB,iBAAA,CAClB,IAAA,EACA,GAAA,EACA,OAAA,EACe;AACf,EAAA,IAAI,CAAC,OAAA,EAAS;AACV,IAAA,MAAM,IAAI,wBAAwB,oDAAoD,CAAA;AAAA,EAC1F;AACA,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAChC,EAAA,OAAO,eAAe,wBAAA,EAA0B,MAAM,cAAc,IAAA,EAAM,GAAA,EAAK,OAAO,CAAC,CAAA;AAC3F;AAOA,eAAsB,iBAAA,CAClB,aAAA,EACA,GAAA,EACA,OAAA,EACuB;AACvB,EAAA,MAAM,QAAA,GAAW,aAAA,CAAc,wBAAA,EAA0B,aAAa,CAAA;AACtE,EAAA,IAAI,QAAA,CAAS,YAAY,CAAA,EAAG;AACxB,IAAA,OAAO,IAAA,CAAK,MAAM,MAAM,aAAA,CAAc,SAAS,OAAA,EAAS,GAAA,EAAK,OAAO,CAAC,CAAA;AAAA,EACzE;AAEA,EAAA,IAAI,OAAA,EAAS;AACT,IAAA,IAAI;AACA,MAAA,OAAO,IAAA,CAAK,KAAA;AAAA,QACR,MAAM,aAAA,CAAc,QAAA,CAAS,OAAA,EAAS,KAAK,OAAO;AAAA,OACtD;AAAA,IACJ,CAAA,CAAA,MAAQ;AACJ,MAAA,MAAM,IAAI,sBAAsB,mDAAmD,CAAA;AAAA,IACvF;AAAA,EACJ;AACA,EAAA,MAAM,IAAI,sBAAsB,mDAAmD,CAAA;AACvF;AAaA,eAAsB,6BAAA,CAClB,aAAA,EACA,GAAA,EACA,OAAA,EACkC;AAClC,EAAA,MAAM,QAAA,GAAW,aAAA,CAAc,wBAAA,EAA0B,aAAa,CAAA;AACtE,EAAA,IAAI,QAAA,CAAS,YAAY,CAAA,EAAG;AACxB,IAAA,OAAO;AAAA,MACH,IAAA,EAAM,KAAK,KAAA,CAAM,MAAM,cAAc,QAAA,CAAS,OAAA,EAAS,GAAA,EAAK,OAAO,CAAC,CAAA;AAAA,MACpE,kBAAA,EAAoB,KAAA;AAAA,MACpB,uBAAA,EAAyB;AAAA,KAC7B;AAAA,EACJ;AACA,EAAA,IAAI,OAAA,EAAS;AACT,IAAA,IAAI;AACA,MAAA,OAAO;AAAA,QACH,MAAM,IAAA,CAAK,KAAA;AAAA,UACP,MAAM,aAAA,CAAc,QAAA,CAAS,OAAA,EAAS,KAAK,OAAO;AAAA,SACtD;AAAA,QACA,kBAAA,EAAoB,IAAA;AAAA,QACpB,uBAAA,EAAyB;AAAA,OAC7B;AAAA,IACJ,CAAA,CAAA,MAAQ;AAAA,IAER;AAAA,EACJ;AACA,EAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,MAAM,cAAc,QAAA,CAAS,OAAA,EAAS,GAAG,CAAC,CAAA;AAClE,EAAA,OAAO,EAAE,IAAA,EAAM,kBAAA,EAAoB,IAAA,EAAM,yBAAyB,IAAA,EAAK;AAC3E;AAGO,SAAS,4BAA4B,aAAA,EAAgC;AACxE,EAAA,OAAO,iBAAA,CAAkB,0BAA0B,aAAa,CAAA;AACpE","file":"chunk-JVFP2GAO.js","sourcesContent":["/**\r\n * dis-vault-encryption — encryption of structured vault entries.\r\n *\r\n * A vault entry is an arbitrary JSON-serialisable record. It is sealed with\r\n * AES-256-GCM and wrapped in the versioned `sv-vault-v1:` envelope, byte\r\n * compatible with Singra Vault. The entry id is passed as AEAD associated data\r\n * so ciphertext is cryptographically bound to its row (defeats swap attacks).\r\n *\r\n * Legacy (pre-versioning, no-AAD) payloads fail closed on the runtime read\r\n * path and are only readable through the explicit migration helper.\r\n */\r\n\r\nimport { decryptString, encryptString } from '../aead/index.js';\r\nimport {\r\n formatEnvelope,\r\n isCurrentEnvelope,\r\n parseEnvelope,\r\n type VersionedCipherEnvelopeSpec,\r\n} from '../format-versioning/index.js';\r\nimport { DisInvalidArgumentError, DisLegacyPayloadError } from '../core/errors.js';\r\n\r\nexport const VAULT_ITEM_ENVELOPE_V1_PREFIX = 'sv-vault-v1:';\r\nconst VAULT_ITEM_ENVELOPE_FAMILY_PREFIX = 'sv-vault-';\r\n\r\nexport const VAULT_ITEM_ENVELOPE_SPEC: VersionedCipherEnvelopeSpec = {\r\n currentPrefix: VAULT_ITEM_ENVELOPE_V1_PREFIX,\r\n familyPrefix: VAULT_ITEM_ENVELOPE_FAMILY_PREFIX,\r\n subject: 'vault item',\r\n};\r\n\r\nexport type VaultEntryData = Record<string, unknown>;\r\n\r\n/** Seals a vault entry, binding it to `entryId` via AEAD associated data. */\r\nexport async function encryptVaultEntry(\r\n data: VaultEntryData,\r\n key: CryptoKey,\r\n entryId: string,\r\n): Promise<string> {\r\n if (!entryId) {\r\n throw new DisInvalidArgumentError('entryId is required to bind vault entry ciphertext');\r\n }\r\n const json = JSON.stringify(data);\r\n return formatEnvelope(VAULT_ITEM_ENVELOPE_SPEC, await encryptString(json, key, entryId));\r\n}\r\n\r\n/**\r\n * Opens a vault entry. Versioned payloads are read with `entryId` as AAD.\r\n * Legacy no-AAD payloads throw {@link DisLegacyPayloadError} on the runtime\r\n * path; use {@link decryptVaultEntryForMigration} to read and rewrite them.\r\n */\r\nexport async function decryptVaultEntry(\r\n encryptedData: string,\r\n key: CryptoKey,\r\n entryId: string,\r\n): Promise<VaultEntryData> {\r\n const envelope = parseEnvelope(VAULT_ITEM_ENVELOPE_SPEC, encryptedData);\r\n if (envelope.version === 1) {\r\n return JSON.parse(await decryptString(envelope.payload, key, entryId)) as VaultEntryData;\r\n }\r\n // Legacy payloads written after AAD rollout still authenticate with entryId.\r\n if (entryId) {\r\n try {\r\n return JSON.parse(\r\n await decryptString(envelope.payload, key, entryId),\r\n ) as VaultEntryData;\r\n } catch {\r\n throw new DisLegacyPayloadError('Legacy vault item without AAD requires migration.');\r\n }\r\n }\r\n throw new DisLegacyPayloadError('Legacy vault item without AAD requires migration.');\r\n}\r\n\r\nexport interface VaultEntryMigrationResult {\r\n readonly data: VaultEntryData;\r\n readonly legacyEnvelopeUsed: boolean;\r\n readonly legacyNoAadFallbackUsed: boolean;\r\n}\r\n\r\n/**\r\n * Decrypts an entry on an explicit migration path, permitting the no-AAD\r\n * fallback for the oldest payloads so they can be rewritten as versioned,\r\n * AAD-bound items. Never use on the normal runtime read path.\r\n */\r\nexport async function decryptVaultEntryForMigration(\r\n encryptedData: string,\r\n key: CryptoKey,\r\n entryId: string,\r\n): Promise<VaultEntryMigrationResult> {\r\n const envelope = parseEnvelope(VAULT_ITEM_ENVELOPE_SPEC, encryptedData);\r\n if (envelope.version === 1) {\r\n return {\r\n data: JSON.parse(await decryptString(envelope.payload, key, entryId)) as VaultEntryData,\r\n legacyEnvelopeUsed: false,\r\n legacyNoAadFallbackUsed: false,\r\n };\r\n }\r\n if (entryId) {\r\n try {\r\n return {\r\n data: JSON.parse(\r\n await decryptString(envelope.payload, key, entryId),\r\n ) as VaultEntryData,\r\n legacyEnvelopeUsed: true,\r\n legacyNoAadFallbackUsed: false,\r\n };\r\n } catch {\r\n // Fall through to the no-AAD fallback below.\r\n }\r\n }\r\n const data = JSON.parse(await decryptString(envelope.payload, key)) as VaultEntryData;\r\n return { data, legacyEnvelopeUsed: true, legacyNoAadFallbackUsed: true };\r\n}\r\n\r\n/** True if `encryptedData` is a current versioned vault-item envelope. */\r\nexport function isCurrentVaultEntryEnvelope(encryptedData: string): boolean {\r\n return isCurrentEnvelope(VAULT_ITEM_ENVELOPE_SPEC, encryptedData);\r\n}\r\n"]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { utf8ToBytes, bytesToBase64, base64ToBytes, bytesToUtf8 } from './chunk-JSKIWIEC.js';
|
|
2
|
+
import { subtle } from './chunk-CYIGDF63.js';
|
|
3
|
+
|
|
4
|
+
// src/asymmetric/index.ts
|
|
5
|
+
var RSA_OAEP_MODULUS_LENGTH = 4096;
|
|
6
|
+
var RSA_OAEP_ALGORITHM = {
|
|
7
|
+
name: "RSA-OAEP",
|
|
8
|
+
hash: "SHA-256"
|
|
9
|
+
};
|
|
10
|
+
async function generateRsaOaepKeyPair() {
|
|
11
|
+
return subtle().generateKey(
|
|
12
|
+
{
|
|
13
|
+
name: "RSA-OAEP",
|
|
14
|
+
modulusLength: RSA_OAEP_MODULUS_LENGTH,
|
|
15
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
16
|
+
hash: "SHA-256"
|
|
17
|
+
},
|
|
18
|
+
true,
|
|
19
|
+
["encrypt", "decrypt"]
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
async function exportJwk(key) {
|
|
23
|
+
return subtle().exportKey("jwk", key);
|
|
24
|
+
}
|
|
25
|
+
async function importRsaOaepPublicKey(jwk) {
|
|
26
|
+
return subtle().importKey("jwk", jwk, RSA_OAEP_ALGORITHM, true, ["encrypt"]);
|
|
27
|
+
}
|
|
28
|
+
async function importRsaOaepPrivateKey(jwk) {
|
|
29
|
+
return subtle().importKey("jwk", jwk, RSA_OAEP_ALGORITHM, false, ["decrypt"]);
|
|
30
|
+
}
|
|
31
|
+
async function rsaOaepEncrypt(plaintext, publicKey) {
|
|
32
|
+
const encoded = utf8ToBytes(plaintext);
|
|
33
|
+
try {
|
|
34
|
+
const encrypted = await subtle().encrypt({ name: "RSA-OAEP" }, publicKey, encoded);
|
|
35
|
+
return bytesToBase64(new Uint8Array(encrypted));
|
|
36
|
+
} finally {
|
|
37
|
+
encoded.fill(0);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function rsaOaepDecrypt(ciphertextBase64, privateKey) {
|
|
41
|
+
const encrypted = base64ToBytes(ciphertextBase64);
|
|
42
|
+
let plaintextBytes = null;
|
|
43
|
+
try {
|
|
44
|
+
const decrypted = await subtle().decrypt({ name: "RSA-OAEP" }, privateKey, encrypted);
|
|
45
|
+
plaintextBytes = new Uint8Array(decrypted);
|
|
46
|
+
return bytesToUtf8(plaintextBytes);
|
|
47
|
+
} finally {
|
|
48
|
+
encrypted.fill(0);
|
|
49
|
+
plaintextBytes?.fill(0);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { RSA_OAEP_MODULUS_LENGTH, exportJwk, generateRsaOaepKeyPair, importRsaOaepPrivateKey, importRsaOaepPublicKey, rsaOaepDecrypt, rsaOaepEncrypt };
|
|
54
|
+
//# sourceMappingURL=chunk-KNCZMIZA.js.map
|
|
55
|
+
//# sourceMappingURL=chunk-KNCZMIZA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/asymmetric/index.ts"],"names":[],"mappings":";;;;AAkBO,IAAM,uBAAA,GAA0B;AAEvC,IAAM,kBAAA,GAAqB;AAAA,EACvB,IAAA,EAAM,UAAA;AAAA,EACN,IAAA,EAAM;AACV,CAAA;AAMA,eAAsB,sBAAA,GAAiD;AACnE,EAAA,OAAO,QAAO,CAAE,WAAA;AAAA,IACZ;AAAA,MACI,IAAA,EAAM,UAAA;AAAA,MACN,aAAA,EAAe,uBAAA;AAAA,MACf,gBAAgB,IAAI,UAAA,CAAW,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAC,CAAA;AAAA,MACxC,IAAA,EAAM;AAAA,KACV;AAAA,IACA,IAAA;AAAA,IACA,CAAC,WAAW,SAAS;AAAA,GACzB;AACJ;AAGA,eAAsB,UAAU,GAAA,EAAqC;AACjE,EAAA,OAAO,MAAA,EAAO,CAAE,SAAA,CAAU,KAAA,EAAO,GAAG,CAAA;AACxC;AAGA,eAAsB,uBAAuB,GAAA,EAAqC;AAC9E,EAAA,OAAO,MAAA,GAAS,SAAA,CAAU,KAAA,EAAO,KAAK,kBAAA,EAAoB,IAAA,EAAM,CAAC,SAAS,CAAC,CAAA;AAC/E;AAGA,eAAsB,wBAAwB,GAAA,EAAqC;AAC/E,EAAA,OAAO,MAAA,GAAS,SAAA,CAAU,KAAA,EAAO,KAAK,kBAAA,EAAoB,KAAA,EAAO,CAAC,SAAS,CAAC,CAAA;AAChF;AAGA,eAAsB,cAAA,CAAe,WAAmB,SAAA,EAAuC;AAC3F,EAAA,MAAM,OAAA,GAAU,YAAY,SAAS,CAAA;AACrC,EAAA,IAAI;AACA,IAAA,MAAM,SAAA,GAAY,MAAM,MAAA,EAAO,CAAE,OAAA,CAAQ,EAAE,IAAA,EAAM,UAAA,EAAW,EAAG,SAAA,EAAW,OAAuB,CAAA;AACjG,IAAA,OAAO,aAAA,CAAc,IAAI,UAAA,CAAW,SAAS,CAAC,CAAA;AAAA,EAClD,CAAA,SAAE;AACE,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAClB;AACJ;AAGA,eAAsB,cAAA,CAAe,kBAA0B,UAAA,EAAwC;AACnG,EAAA,MAAM,SAAA,GAAY,cAAc,gBAAgB,CAAA;AAChD,EAAA,IAAI,cAAA,GAAoC,IAAA;AACxC,EAAA,IAAI;AACA,IAAA,MAAM,SAAA,GAAY,MAAM,MAAA,EAAO,CAAE,OAAA,CAAQ,EAAE,IAAA,EAAM,UAAA,EAAW,EAAG,UAAA,EAAY,SAAyB,CAAA;AACpG,IAAA,cAAA,GAAiB,IAAI,WAAW,SAAS,CAAA;AACzC,IAAA,OAAO,YAAY,cAAc,CAAA;AAAA,EACrC,CAAA,SAAE;AACE,IAAA,SAAA,CAAU,KAAK,CAAC,CAAA;AAChB,IAAA,cAAA,EAAgB,KAAK,CAAC,CAAA;AAAA,EAC1B;AACJ","file":"chunk-KNCZMIZA.js","sourcesContent":["/**\r\n * dis-asymmetric — RSA-OAEP public-key operations.\r\n *\r\n * Primitive: RSA-OAEP (4096-bit modulus, SHA-256) via WebCrypto. Used by the\r\n * Singra sharing / emergency-access profile to wrap symmetric material for a\r\n * recipient's public key. DIS does not invent any asymmetric scheme — this is\r\n * a thin, audited wrapper over WebCrypto so applications never touch the raw\r\n * `crypto.subtle` surface.\r\n *\r\n * Key material is exported/imported as JWK (the format Singra persists), so\r\n * existing stored keys remain byte-compatible. Wire format for ciphertext is\r\n * `base64(rsa_oaep_output)`, identical to the legacy implementation.\r\n */\r\n\r\nimport { subtle } from '../core/provider.js';\r\nimport { base64ToBytes, bytesToBase64, bytesToUtf8, utf8ToBytes } from '../core/encoding.js';\r\n\r\n/** RSA-OAEP modulus length in bits. Part of the key-generation format contract. */\r\nexport const RSA_OAEP_MODULUS_LENGTH = 4096;\r\n\r\nconst RSA_OAEP_ALGORITHM = {\r\n name: 'RSA-OAEP',\r\n hash: 'SHA-256',\r\n} as const;\r\n\r\n/**\r\n * Generates an extractable RSA-OAEP-4096 key pair (SHA-256, e=65537).\r\n * Extractable so the private key can be exported as JWK and wrapped at rest.\r\n */\r\nexport async function generateRsaOaepKeyPair(): Promise<CryptoKeyPair> {\r\n return subtle().generateKey(\r\n {\r\n name: 'RSA-OAEP',\r\n modulusLength: RSA_OAEP_MODULUS_LENGTH,\r\n publicExponent: new Uint8Array([1, 0, 1]),\r\n hash: 'SHA-256',\r\n },\r\n true,\r\n ['encrypt', 'decrypt'],\r\n );\r\n}\r\n\r\n/** Exports an RSA key (public or private) as a JWK object. */\r\nexport async function exportJwk(key: CryptoKey): Promise<JsonWebKey> {\r\n return subtle().exportKey('jwk', key);\r\n}\r\n\r\n/** Imports an RSA-OAEP public key (JWK) for `encrypt`. Extractable. */\r\nexport async function importRsaOaepPublicKey(jwk: JsonWebKey): Promise<CryptoKey> {\r\n return subtle().importKey('jwk', jwk, RSA_OAEP_ALGORITHM, true, ['encrypt']);\r\n}\r\n\r\n/** Imports an RSA-OAEP private key (JWK) for `decrypt`. Non-extractable. */\r\nexport async function importRsaOaepPrivateKey(jwk: JsonWebKey): Promise<CryptoKey> {\r\n return subtle().importKey('jwk', jwk, RSA_OAEP_ALGORITHM, false, ['decrypt']);\r\n}\r\n\r\n/** Encrypts a UTF-8 string under an RSA-OAEP public key. Returns base64. */\r\nexport async function rsaOaepEncrypt(plaintext: string, publicKey: CryptoKey): Promise<string> {\r\n const encoded = utf8ToBytes(plaintext);\r\n try {\r\n const encrypted = await subtle().encrypt({ name: 'RSA-OAEP' }, publicKey, encoded as BufferSource);\r\n return bytesToBase64(new Uint8Array(encrypted));\r\n } finally {\r\n encoded.fill(0);\r\n }\r\n}\r\n\r\n/** Decrypts base64 RSA-OAEP ciphertext under an RSA-OAEP private key. */\r\nexport async function rsaOaepDecrypt(ciphertextBase64: string, privateKey: CryptoKey): Promise<string> {\r\n const encrypted = base64ToBytes(ciphertextBase64);\r\n let plaintextBytes: Uint8Array | null = null;\r\n try {\r\n const decrypted = await subtle().decrypt({ name: 'RSA-OAEP' }, privateKey, encrypted as BufferSource);\r\n plaintextBytes = new Uint8Array(decrypted);\r\n return bytesToUtf8(plaintextBytes);\r\n } finally {\r\n encrypted.fill(0);\r\n plaintextBytes?.fill(0);\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var DisError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "DisError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var DisInvalidArgumentError = class extends DisError {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super("INVALID_ARGUMENT", message);
|
|
14
|
+
this.name = "DisInvalidArgumentError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var DisDecryptionError = class extends DisError {
|
|
18
|
+
constructor(message = "Decryption failed") {
|
|
19
|
+
super("DECRYPTION_FAILED", message);
|
|
20
|
+
this.name = "DisDecryptionError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var DisUnsupportedFormatVersionError = class extends DisError {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super("UNSUPPORTED_FORMAT_VERSION", message);
|
|
26
|
+
this.name = "DisUnsupportedFormatVersionError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var DisIntegrityError = class extends DisError {
|
|
30
|
+
constructor(message = "Integrity check failed") {
|
|
31
|
+
super("INTEGRITY_CHECK_FAILED", message);
|
|
32
|
+
this.name = "DisIntegrityError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var DisLegacyPayloadError = class extends DisError {
|
|
36
|
+
constructor(message = "Legacy payload requires explicit migration") {
|
|
37
|
+
super("LEGACY_PAYLOAD_REQUIRES_MIGRATION", message);
|
|
38
|
+
this.name = "DisLegacyPayloadError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { DisDecryptionError, DisError, DisIntegrityError, DisInvalidArgumentError, DisLegacyPayloadError, DisUnsupportedFormatVersionError };
|
|
43
|
+
//# sourceMappingURL=chunk-MJO7IJZC.js.map
|
|
44
|
+
//# sourceMappingURL=chunk-MJO7IJZC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/errors.ts"],"names":[],"mappings":";AAmBO,IAAM,QAAA,GAAN,cAAuB,KAAA,CAAM;AAAA,EACvB,IAAA;AAAA,EAET,WAAA,CAAY,MAAoB,OAAA,EAAiB;AAC7C,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAEZ,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EACpD;AACJ;AAGO,IAAM,uBAAA,GAAN,cAAsC,QAAA,CAAS;AAAA,EAClD,YAAY,OAAA,EAAiB;AACzB,IAAA,KAAA,CAAM,oBAAoB,OAAO,CAAA;AACjC,IAAA,IAAA,CAAK,IAAA,GAAO,yBAAA;AAAA,EAChB;AACJ;AAOO,IAAM,kBAAA,GAAN,cAAiC,QAAA,CAAS;AAAA,EAC7C,WAAA,CAAY,UAAU,mBAAA,EAAqB;AACvC,IAAA,KAAA,CAAM,qBAAqB,OAAO,CAAA;AAClC,IAAA,IAAA,CAAK,IAAA,GAAO,oBAAA;AAAA,EAChB;AACJ;AAGO,IAAM,gCAAA,GAAN,cAA+C,QAAA,CAAS;AAAA,EAC3D,YAAY,OAAA,EAAiB;AACzB,IAAA,KAAA,CAAM,8BAA8B,OAAO,CAAA;AAC3C,IAAA,IAAA,CAAK,IAAA,GAAO,kCAAA;AAAA,EAChB;AACJ;AAGO,IAAM,iBAAA,GAAN,cAAgC,QAAA,CAAS;AAAA,EAC5C,WAAA,CAAY,UAAU,wBAAA,EAA0B;AAC5C,IAAA,KAAA,CAAM,0BAA0B,OAAO,CAAA;AACvC,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AAAA,EAChB;AACJ;AAGO,IAAM,qBAAA,GAAN,cAAoC,QAAA,CAAS;AAAA,EAChD,WAAA,CAAY,UAAU,4CAAA,EAA8C;AAChE,IAAA,KAAA,CAAM,qCAAqC,OAAO,CAAA;AAClD,IAAA,IAAA,CAAK,IAAA,GAAO,uBAAA;AAAA,EAChB;AACJ","file":"chunk-MJO7IJZC.js","sourcesContent":["/**\r\n * Central, typed error hierarchy for DIS.\r\n *\r\n * Errors never include secret material (keys, plaintext, passwords) in their\r\n * message or properties. Callers may safely log `error.code` and `error.message`.\r\n */\r\n\r\nexport type DisErrorCode =\r\n | 'INVALID_ARGUMENT'\r\n | 'UNSUPPORTED_FORMAT_VERSION'\r\n | 'DECRYPTION_FAILED'\r\n | 'INTEGRITY_CHECK_FAILED'\r\n | 'KEY_DERIVATION_FAILED'\r\n | 'UNSUPPORTED_KDF_VERSION'\r\n | 'LEGACY_PAYLOAD_REQUIRES_MIGRATION'\r\n | 'PROVIDER_UNAVAILABLE'\r\n | 'USE_AFTER_DESTROY';\r\n\r\n/** Base class for all errors thrown by DIS. */\r\nexport class DisError extends Error {\r\n readonly code: DisErrorCode;\r\n\r\n constructor(code: DisErrorCode, message: string) {\r\n super(message);\r\n this.name = 'DisError';\r\n this.code = code;\r\n // Maintain prototype chain when compiled to older targets.\r\n Object.setPrototypeOf(this, new.target.prototype);\r\n }\r\n}\r\n\r\n/** Thrown when an argument is missing or structurally invalid. */\r\nexport class DisInvalidArgumentError extends DisError {\r\n constructor(message: string) {\r\n super('INVALID_ARGUMENT', message);\r\n this.name = 'DisInvalidArgumentError';\r\n }\r\n}\r\n\r\n/**\r\n * Thrown when AEAD decryption or authentication fails. The cause (wrong key,\r\n * tampered ciphertext, or AAD mismatch) is intentionally not distinguished to\r\n * avoid leaking an oracle.\r\n */\r\nexport class DisDecryptionError extends DisError {\r\n constructor(message = 'Decryption failed') {\r\n super('DECRYPTION_FAILED', message);\r\n this.name = 'DisDecryptionError';\r\n }\r\n}\r\n\r\n/** Thrown when a versioned payload carries a version DIS cannot read. */\r\nexport class DisUnsupportedFormatVersionError extends DisError {\r\n constructor(message: string) {\r\n super('UNSUPPORTED_FORMAT_VERSION', message);\r\n this.name = 'DisUnsupportedFormatVersionError';\r\n }\r\n}\r\n\r\n/** Thrown when an integrity / hash verification fails. */\r\nexport class DisIntegrityError extends DisError {\r\n constructor(message = 'Integrity check failed') {\r\n super('INTEGRITY_CHECK_FAILED', message);\r\n this.name = 'DisIntegrityError';\r\n }\r\n}\r\n\r\n/** Thrown when a legacy, non-migratable payload is read on a runtime path. */\r\nexport class DisLegacyPayloadError extends DisError {\r\n constructor(message = 'Legacy payload requires explicit migration') {\r\n super('LEGACY_PAYLOAD_REQUIRES_MIGRATION', message);\r\n this.name = 'DisLegacyPayloadError';\r\n }\r\n}\r\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { bytesToBase64, bytesToBase64Url, bytesToHex, utf8ToBytes, base64ToBytes } from './chunk-JSKIWIEC.js';
|
|
2
|
+
import { subtle } from './chunk-CYIGDF63.js';
|
|
3
|
+
import { DisIntegrityError } from './chunk-MJO7IJZC.js';
|
|
4
|
+
|
|
5
|
+
// src/integrity/index.ts
|
|
6
|
+
async function sha256Bytes(data) {
|
|
7
|
+
const digest = await subtle().digest("SHA-256", data);
|
|
8
|
+
return new Uint8Array(digest);
|
|
9
|
+
}
|
|
10
|
+
async function sha256Base64(data) {
|
|
11
|
+
return bytesToBase64(await sha256Bytes(data));
|
|
12
|
+
}
|
|
13
|
+
async function sha256Base64Url(data) {
|
|
14
|
+
return bytesToBase64Url(await sha256Bytes(data));
|
|
15
|
+
}
|
|
16
|
+
async function sha256Hex(data) {
|
|
17
|
+
return bytesToHex(await sha256Bytes(data));
|
|
18
|
+
}
|
|
19
|
+
async function sha1Hex(data) {
|
|
20
|
+
const digest = await subtle().digest("SHA-1", data);
|
|
21
|
+
return bytesToHex(new Uint8Array(digest));
|
|
22
|
+
}
|
|
23
|
+
async function importHmacSha256Key(keyBytes, usages = ["sign", "verify"]) {
|
|
24
|
+
return subtle().importKey(
|
|
25
|
+
"raw",
|
|
26
|
+
keyBytes,
|
|
27
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
28
|
+
false,
|
|
29
|
+
usages
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
async function hmacSha256WithKey(key, data) {
|
|
33
|
+
const sig = await subtle().sign("HMAC", key, data);
|
|
34
|
+
return new Uint8Array(sig);
|
|
35
|
+
}
|
|
36
|
+
async function hmacSha256(keyBytes, data) {
|
|
37
|
+
const key = await importHmacSha256Key(keyBytes, ["sign"]);
|
|
38
|
+
return hmacSha256WithKey(key, data);
|
|
39
|
+
}
|
|
40
|
+
async function sha256StringBase64(data) {
|
|
41
|
+
return sha256Base64(utf8ToBytes(data));
|
|
42
|
+
}
|
|
43
|
+
async function sha256JsonBase64(value) {
|
|
44
|
+
return sha256StringBase64(JSON.stringify(value));
|
|
45
|
+
}
|
|
46
|
+
function constantTimeEqual(a, b) {
|
|
47
|
+
if (a.length !== b.length) return false;
|
|
48
|
+
let result = 0;
|
|
49
|
+
for (let i = 0; i < a.length; i++) {
|
|
50
|
+
result |= a[i] ^ b[i];
|
|
51
|
+
}
|
|
52
|
+
return result === 0;
|
|
53
|
+
}
|
|
54
|
+
function constantTimeEqualBase64(a, b) {
|
|
55
|
+
return constantTimeEqual(base64ToBytes(a), base64ToBytes(b));
|
|
56
|
+
}
|
|
57
|
+
async function verifyPayloadIntegrity(data, expectedBase64) {
|
|
58
|
+
const actual = await sha256Base64(data);
|
|
59
|
+
if (!constantTimeEqualBase64(actual, expectedBase64)) {
|
|
60
|
+
throw new DisIntegrityError();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { constantTimeEqual, constantTimeEqualBase64, hmacSha256, hmacSha256WithKey, importHmacSha256Key, sha1Hex, sha256Base64, sha256Base64Url, sha256Bytes, sha256Hex, sha256JsonBase64, sha256StringBase64, verifyPayloadIntegrity };
|
|
65
|
+
//# sourceMappingURL=chunk-MPWYZXW7.js.map
|
|
66
|
+
//# sourceMappingURL=chunk-MPWYZXW7.js.map
|