@offlinejs/encryption 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.
@@ -0,0 +1,7 @@
1
+ import { StorageAdapter, EncryptionCodec } from '@offlinejs/types';
2
+
3
+ declare const createJsonEncryptionStorage: (storage: StorageAdapter, codec: EncryptionCodec) => StorageAdapter;
4
+ declare const createWebCryptoAesGcmCodec: (key: CryptoKey) => Promise<EncryptionCodec>;
5
+ declare const generateAesGcmKey: () => Promise<CryptoKey>;
6
+
7
+ export { createJsonEncryptionStorage, createWebCryptoAesGcmCodec, generateAesGcmKey };
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ import { STORAGE_ADAPTER_CONTRACT_VERSION } from '@offlinejs/types';
2
+ import { withIndexForwarding, applyQuery } from '@offlinejs/utils';
3
+
4
+ // src/index.ts
5
+ var createJsonEncryptionStorage = (storage, codec) => {
6
+ const createEncryptedStore = (store) => ({
7
+ clear: (collection) => store.clear(collection),
8
+ delete: (collection, id) => store.delete(collection, id),
9
+ async find(collection, query) {
10
+ const encryptedRecords = await store.find(collection);
11
+ const records = await Promise.all(
12
+ encryptedRecords.map((record) => decryptRecord(record, codec))
13
+ );
14
+ return applyQuery(records, query);
15
+ },
16
+ async get(collection, id) {
17
+ const record = await store.get(collection, id);
18
+ return record ? decryptRecord(record, codec) : null;
19
+ },
20
+ async set(collection, value) {
21
+ await store.set(collection, await encryptRecord(value, codec));
22
+ }
23
+ });
24
+ const wrapper = {
25
+ name: `${storage.name}:encrypted`,
26
+ contractVersion: STORAGE_ADAPTER_CONTRACT_VERSION,
27
+ ...storage.capabilities ? { capabilities: storage.capabilities } : {},
28
+ clear: (collection) => storage.clear(collection),
29
+ delete: (collection, id) => storage.delete(collection, id),
30
+ async find(collection, query) {
31
+ const encryptedRecords = await storage.find(collection);
32
+ const records = await Promise.all(
33
+ encryptedRecords.map((record) => decryptRecord(record, codec))
34
+ );
35
+ return applyQuery(records, query);
36
+ },
37
+ async get(collection, id) {
38
+ const record = await storage.get(collection, id);
39
+ return record ? decryptRecord(record, codec) : null;
40
+ },
41
+ async set(collection, value) {
42
+ await storage.set(collection, await encryptRecord(value, codec));
43
+ },
44
+ transaction: (scope, run) => storage.transaction(scope, (store) => run(createEncryptedStore(store))),
45
+ ...storage.migrate ? { migrate: storage.migrate.bind(storage) } : {}
46
+ };
47
+ return withIndexForwarding(wrapper, storage);
48
+ };
49
+ var createWebCryptoAesGcmCodec = async (key) => ({
50
+ async decrypt(value) {
51
+ const iv = value.slice(0, 12);
52
+ const ciphertext = value.slice(12);
53
+ const decrypted = await globalThis.crypto.subtle.decrypt(
54
+ { iv, name: "AES-GCM" },
55
+ key,
56
+ ciphertext
57
+ );
58
+ return new Uint8Array(decrypted);
59
+ },
60
+ async encrypt(value) {
61
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
62
+ const encrypted = await globalThis.crypto.subtle.encrypt({ iv, name: "AES-GCM" }, key, value);
63
+ const output = new Uint8Array(iv.byteLength + encrypted.byteLength);
64
+ output.set(iv, 0);
65
+ output.set(new Uint8Array(encrypted), iv.byteLength);
66
+ return output;
67
+ }
68
+ });
69
+ var generateAesGcmKey = () => globalThis.crypto.subtle.generateKey({ length: 256, name: "AES-GCM" }, true, [
70
+ "encrypt",
71
+ "decrypt"
72
+ ]);
73
+ var encryptRecord = async (record, codec) => {
74
+ const bytes = new TextEncoder().encode(JSON.stringify(record));
75
+ const encrypted = await codec.encrypt(bytes);
76
+ return {
77
+ id: record.id,
78
+ __offlinejsEncrypted: true,
79
+ data: bytesToBase64(encrypted)
80
+ };
81
+ };
82
+ var decryptRecord = async (record, codec) => {
83
+ const encrypted = base64ToBytes(record.data);
84
+ const decrypted = await codec.decrypt(encrypted);
85
+ return JSON.parse(new TextDecoder().decode(decrypted));
86
+ };
87
+ var bytesToBase64 = (bytes) => {
88
+ let binary = "";
89
+ for (const byte of bytes) {
90
+ binary += String.fromCharCode(byte);
91
+ }
92
+ return globalThis.btoa(binary);
93
+ };
94
+ var base64ToBytes = (value) => {
95
+ const binary = globalThis.atob(value);
96
+ const bytes = new Uint8Array(binary.length);
97
+ for (let index = 0; index < binary.length; index += 1) {
98
+ bytes[index] = binary.charCodeAt(index);
99
+ }
100
+ return bytes;
101
+ };
102
+
103
+ export { createJsonEncryptionStorage, createWebCryptoAesGcmCodec, generateAesGcmKey };
104
+ //# sourceMappingURL=index.js.map
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import {\n STORAGE_ADAPTER_CONTRACT_VERSION,\n type EncryptionCodec,\n type EntityRecord,\n type QueryOptions,\n type StorageAdapter,\n type TransactionStore\n} from \"@offlinejs/types\";\nimport { applyQuery, withIndexForwarding } from \"@offlinejs/utils\";\n\ninterface EncryptedRecord extends EntityRecord {\n __offlinejsEncrypted: true;\n data: string;\n}\n\nexport const createJsonEncryptionStorage = (\n storage: StorageAdapter,\n codec: EncryptionCodec\n): StorageAdapter => {\n const createEncryptedStore = (store: TransactionStore): TransactionStore => ({\n clear: (collection) => store.clear(collection),\n delete: (collection, id) => store.delete(collection, id),\n async find<TRecord extends EntityRecord>(\n collection: string,\n query?: QueryOptions<TRecord>\n ): Promise<TRecord[]> {\n const encryptedRecords = await store.find<EncryptedRecord>(collection);\n const records = await Promise.all(\n encryptedRecords.map((record) => decryptRecord<TRecord>(record, codec))\n );\n return applyQuery(records, query);\n },\n async get<TRecord extends EntityRecord>(\n collection: string,\n id: string\n ): Promise<TRecord | null> {\n const record = await store.get<EncryptedRecord>(collection, id);\n return record ? decryptRecord<TRecord>(record, codec) : null;\n },\n async set<TRecord extends EntityRecord>(collection: string, value: TRecord): Promise<void> {\n await store.set(collection, await encryptRecord(value, codec));\n }\n });\n\n const wrapper: StorageAdapter = {\n name: `${storage.name}:encrypted`,\n contractVersion: STORAGE_ADAPTER_CONTRACT_VERSION,\n ...(storage.capabilities ? { capabilities: storage.capabilities } : {}),\n clear: (collection) => storage.clear(collection),\n delete: (collection, id) => storage.delete(collection, id),\n async find<TRecord extends EntityRecord>(\n collection: string,\n query?: QueryOptions<TRecord>\n ): Promise<TRecord[]> {\n const encryptedRecords = await storage.find<EncryptedRecord>(collection);\n const records = await Promise.all(\n encryptedRecords.map((record) => decryptRecord<TRecord>(record, codec))\n );\n\n return applyQuery(records, query);\n },\n async get<TRecord extends EntityRecord>(\n collection: string,\n id: string\n ): Promise<TRecord | null> {\n const record = await storage.get<EncryptedRecord>(collection, id);\n return record ? decryptRecord<TRecord>(record, codec) : null;\n },\n async set<TRecord extends EntityRecord>(collection: string, value: TRecord): Promise<void> {\n await storage.set(collection, await encryptRecord(value, codec));\n },\n transaction: <TValue>(scope: string[], run: (store: TransactionStore) => Promise<TValue>) =>\n storage.transaction(scope, (store) => run(createEncryptedStore(store))),\n ...(storage.migrate ? { migrate: storage.migrate.bind(storage) } : {})\n };\n\n return withIndexForwarding(wrapper, storage);\n};\n\nexport const createWebCryptoAesGcmCodec = async (key: CryptoKey): Promise<EncryptionCodec> => ({\n async decrypt(value) {\n const iv = value.slice(0, 12);\n const ciphertext = value.slice(12);\n const decrypted = await globalThis.crypto.subtle.decrypt(\n { iv, name: \"AES-GCM\" },\n key,\n ciphertext\n );\n return new Uint8Array(decrypted);\n },\n async encrypt(value) {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));\n const encrypted = await globalThis.crypto.subtle.encrypt({ iv, name: \"AES-GCM\" }, key, value);\n const output = new Uint8Array(iv.byteLength + encrypted.byteLength);\n output.set(iv, 0);\n output.set(new Uint8Array(encrypted), iv.byteLength);\n return output;\n }\n});\n\nexport const generateAesGcmKey = (): Promise<CryptoKey> =>\n globalThis.crypto.subtle.generateKey({ length: 256, name: \"AES-GCM\" }, true, [\n \"encrypt\",\n \"decrypt\"\n ]);\n\nconst encryptRecord = async <TRecord extends EntityRecord>(\n record: TRecord,\n codec: EncryptionCodec\n): Promise<EncryptedRecord> => {\n const bytes = new TextEncoder().encode(JSON.stringify(record));\n const encrypted = await codec.encrypt(bytes);\n\n return {\n id: record.id,\n __offlinejsEncrypted: true,\n data: bytesToBase64(encrypted)\n };\n};\n\nconst decryptRecord = async <TRecord extends EntityRecord>(\n record: EncryptedRecord,\n codec: EncryptionCodec\n): Promise<TRecord> => {\n const encrypted = base64ToBytes(record.data);\n const decrypted = await codec.decrypt(encrypted);\n return JSON.parse(new TextDecoder().decode(decrypted)) as TRecord;\n};\n\nconst bytesToBase64 = (bytes: Uint8Array): string => {\n let binary = \"\";\n\n for (const byte of bytes) {\n binary += String.fromCharCode(byte);\n }\n\n return globalThis.btoa(binary);\n};\n\nconst base64ToBytes = (value: string): Uint8Array => {\n const binary = globalThis.atob(value);\n const bytes = new Uint8Array(binary.length);\n\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n\n return bytes;\n};\n"]}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@offlinejs/encryption",
3
+ "version": "0.1.0",
4
+ "description": "Encryption codec and encrypted storage wrapper for OfflineJS.",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "dependencies": {
17
+ "@offlinejs/types": "0.1.0",
18
+ "@offlinejs/utils": "0.1.0"
19
+ },
20
+ "devDependencies": {
21
+ "tsup": "latest",
22
+ "typescript": "latest"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public",
26
+ "registry": "https://registry.npmjs.org/"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "typecheck": "tsc --noEmit"
31
+ }
32
+ }