@snaha/swarm-id 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +431 -0
- package/dist/chunk/bmt.d.ts +17 -0
- package/dist/chunk/bmt.d.ts.map +1 -0
- package/dist/chunk/cac.d.ts +18 -0
- package/dist/chunk/cac.d.ts.map +1 -0
- package/dist/chunk/constants.d.ts +10 -0
- package/dist/chunk/constants.d.ts.map +1 -0
- package/dist/chunk/encrypted-cac.d.ts +48 -0
- package/dist/chunk/encrypted-cac.d.ts.map +1 -0
- package/dist/chunk/encryption.d.ts +86 -0
- package/dist/chunk/encryption.d.ts.map +1 -0
- package/dist/chunk/index.d.ts +6 -0
- package/dist/chunk/index.d.ts.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/proxy/act/act.d.ts +78 -0
- package/dist/proxy/act/act.d.ts.map +1 -0
- package/dist/proxy/act/crypto.d.ts +44 -0
- package/dist/proxy/act/crypto.d.ts.map +1 -0
- package/dist/proxy/act/grantee-list.d.ts +82 -0
- package/dist/proxy/act/grantee-list.d.ts.map +1 -0
- package/dist/proxy/act/history.d.ts +183 -0
- package/dist/proxy/act/history.d.ts.map +1 -0
- package/dist/proxy/act/index.d.ts +104 -0
- package/dist/proxy/act/index.d.ts.map +1 -0
- package/dist/proxy/chunking-encrypted.d.ts +14 -0
- package/dist/proxy/chunking-encrypted.d.ts.map +1 -0
- package/dist/proxy/chunking.d.ts +15 -0
- package/dist/proxy/chunking.d.ts.map +1 -0
- package/dist/proxy/download-data.d.ts +16 -0
- package/dist/proxy/download-data.d.ts.map +1 -0
- package/dist/proxy/feed-manifest.d.ts +62 -0
- package/dist/proxy/feed-manifest.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts +77 -0
- package/dist/proxy/feeds/epochs/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts +88 -0
- package/dist/proxy/feeds/epochs/epoch.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/finder.d.ts +67 -0
- package/dist/proxy/feeds/epochs/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/index.d.ts +35 -0
- package/dist/proxy/feeds/epochs/index.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts +93 -0
- package/dist/proxy/feeds/epochs/test-utils.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/types.d.ts +109 -0
- package/dist/proxy/feeds/epochs/types.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/updater.d.ts +68 -0
- package/dist/proxy/feeds/epochs/updater.d.ts.map +1 -0
- package/dist/proxy/feeds/epochs/utils.d.ts +22 -0
- package/dist/proxy/feeds/epochs/utils.d.ts.map +1 -0
- package/dist/proxy/feeds/index.d.ts +5 -0
- package/dist/proxy/feeds/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
- package/dist/proxy/feeds/sequence/async-finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/finder.d.ts +17 -0
- package/dist/proxy/feeds/sequence/finder.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/index.d.ts +23 -0
- package/dist/proxy/feeds/sequence/index.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/types.d.ts +80 -0
- package/dist/proxy/feeds/sequence/types.d.ts.map +1 -0
- package/dist/proxy/feeds/sequence/updater.d.ts +26 -0
- package/dist/proxy/feeds/sequence/updater.d.ts.map +1 -0
- package/dist/proxy/index.d.ts +6 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/manifest-builder.d.ts +183 -0
- package/dist/proxy/manifest-builder.d.ts.map +1 -0
- package/dist/proxy/mantaray-encrypted.d.ts +27 -0
- package/dist/proxy/mantaray-encrypted.d.ts.map +1 -0
- package/dist/proxy/mantaray.d.ts +26 -0
- package/dist/proxy/mantaray.d.ts.map +1 -0
- package/dist/proxy/types.d.ts +29 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/upload-data.d.ts +17 -0
- package/dist/proxy/upload-data.d.ts.map +1 -0
- package/dist/proxy/upload-encrypted-data.d.ts +103 -0
- package/dist/proxy/upload-encrypted-data.d.ts.map +1 -0
- package/dist/schemas.d.ts +240 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/storage/debounced-uploader.d.ts +62 -0
- package/dist/storage/debounced-uploader.d.ts.map +1 -0
- package/dist/storage/utilization-store.d.ts +108 -0
- package/dist/storage/utilization-store.d.ts.map +1 -0
- package/dist/swarm-id-auth.d.ts +74 -0
- package/dist/swarm-id-auth.d.ts.map +1 -0
- package/dist/swarm-id-auth.js +2 -0
- package/dist/swarm-id-auth.js.map +1 -0
- package/dist/swarm-id-client.d.ts +878 -0
- package/dist/swarm-id-client.d.ts.map +1 -0
- package/dist/swarm-id-client.js +2 -0
- package/dist/swarm-id-client.js.map +1 -0
- package/dist/swarm-id-proxy.d.ts +236 -0
- package/dist/swarm-id-proxy.d.ts.map +1 -0
- package/dist/swarm-id-proxy.js +2 -0
- package/dist/swarm-id-proxy.js.map +1 -0
- package/dist/swarm-id.esm.js +2 -0
- package/dist/swarm-id.esm.js.map +1 -0
- package/dist/swarm-id.umd.js +2 -0
- package/dist/swarm-id.umd.js.map +1 -0
- package/dist/sync/index.d.ts +9 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/key-derivation.d.ts +25 -0
- package/dist/sync/key-derivation.d.ts.map +1 -0
- package/dist/sync/restore-account.d.ts +28 -0
- package/dist/sync/restore-account.d.ts.map +1 -0
- package/dist/sync/serialization.d.ts +16 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/store-interfaces.d.ts +53 -0
- package/dist/sync/store-interfaces.d.ts.map +1 -0
- package/dist/sync/sync-account.d.ts +44 -0
- package/dist/sync/sync-account.d.ts.map +1 -0
- package/dist/sync/types.d.ts +13 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/test-fixtures.d.ts +17 -0
- package/dist/test-fixtures.d.ts.map +1 -0
- package/dist/types-BD_VkNn0.js +2 -0
- package/dist/types-BD_VkNn0.js.map +1 -0
- package/dist/types-lJCaT-50.js +2 -0
- package/dist/types-lJCaT-50.js.map +1 -0
- package/dist/types.d.ts +2157 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/account-payload.d.ts +94 -0
- package/dist/utils/account-payload.d.ts.map +1 -0
- package/dist/utils/account-state-snapshot.d.ts +38 -0
- package/dist/utils/account-state-snapshot.d.ts.map +1 -0
- package/dist/utils/backup-encryption.d.ts +127 -0
- package/dist/utils/backup-encryption.d.ts.map +1 -0
- package/dist/utils/batch-utilization.d.ts +432 -0
- package/dist/utils/batch-utilization.d.ts.map +1 -0
- package/dist/utils/constants.d.ts +11 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/hex.d.ts +17 -0
- package/dist/utils/hex.d.ts.map +1 -0
- package/dist/utils/key-derivation.d.ts +92 -0
- package/dist/utils/key-derivation.d.ts.map +1 -0
- package/dist/utils/storage-managers.d.ts +65 -0
- package/dist/utils/storage-managers.d.ts.map +1 -0
- package/dist/utils/swarm-id-export.d.ts +24 -0
- package/dist/utils/swarm-id-export.d.ts.map +1 -0
- package/dist/utils/ttl.d.ts +49 -0
- package/dist/utils/ttl.d.ts.map +1 -0
- package/dist/utils/url.d.ts +41 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/versioned-storage.d.ts +131 -0
- package/dist/utils/versioned-storage.d.ts.map +1 -0
- package/package.json +78 -0
- package/src/chunk/bmt.test.ts +217 -0
- package/src/chunk/bmt.ts +57 -0
- package/src/chunk/cac.test.ts +214 -0
- package/src/chunk/cac.ts +65 -0
- package/src/chunk/constants.ts +18 -0
- package/src/chunk/encrypted-cac.test.ts +385 -0
- package/src/chunk/encrypted-cac.ts +131 -0
- package/src/chunk/encryption.test.ts +352 -0
- package/src/chunk/encryption.ts +300 -0
- package/src/chunk/index.ts +47 -0
- package/src/index.ts +430 -0
- package/src/proxy/act/act.test.ts +278 -0
- package/src/proxy/act/act.ts +158 -0
- package/src/proxy/act/bee-compat.test.ts +948 -0
- package/src/proxy/act/crypto.test.ts +436 -0
- package/src/proxy/act/crypto.ts +376 -0
- package/src/proxy/act/grantee-list.test.ts +393 -0
- package/src/proxy/act/grantee-list.ts +239 -0
- package/src/proxy/act/history.test.ts +360 -0
- package/src/proxy/act/history.ts +413 -0
- package/src/proxy/act/index.test.ts +748 -0
- package/src/proxy/act/index.ts +853 -0
- package/src/proxy/chunking-encrypted.ts +95 -0
- package/src/proxy/chunking.ts +65 -0
- package/src/proxy/download-data.ts +448 -0
- package/src/proxy/feed-manifest.ts +174 -0
- package/src/proxy/feeds/epochs/async-finder.ts +372 -0
- package/src/proxy/feeds/epochs/epoch.test.ts +249 -0
- package/src/proxy/feeds/epochs/epoch.ts +181 -0
- package/src/proxy/feeds/epochs/finder.ts +282 -0
- package/src/proxy/feeds/epochs/index.ts +73 -0
- package/src/proxy/feeds/epochs/integration.test.ts +1336 -0
- package/src/proxy/feeds/epochs/test-utils.ts +274 -0
- package/src/proxy/feeds/epochs/types.ts +128 -0
- package/src/proxy/feeds/epochs/updater.ts +192 -0
- package/src/proxy/feeds/epochs/utils.ts +62 -0
- package/src/proxy/feeds/index.ts +5 -0
- package/src/proxy/feeds/sequence/async-finder.ts +31 -0
- package/src/proxy/feeds/sequence/finder.ts +73 -0
- package/src/proxy/feeds/sequence/index.ts +54 -0
- package/src/proxy/feeds/sequence/integration.test.ts +966 -0
- package/src/proxy/feeds/sequence/types.ts +103 -0
- package/src/proxy/feeds/sequence/updater.ts +71 -0
- package/src/proxy/index.ts +5 -0
- package/src/proxy/manifest-builder.test.ts +427 -0
- package/src/proxy/manifest-builder.ts +679 -0
- package/src/proxy/mantaray-encrypted.ts +78 -0
- package/src/proxy/mantaray.ts +104 -0
- package/src/proxy/types.ts +32 -0
- package/src/proxy/upload-data.ts +189 -0
- package/src/proxy/upload-encrypted-data.ts +658 -0
- package/src/schemas.ts +299 -0
- package/src/storage/debounced-uploader.ts +192 -0
- package/src/storage/utilization-store.ts +397 -0
- package/src/swarm-id-client.test.ts +99 -0
- package/src/swarm-id-client.ts +3095 -0
- package/src/swarm-id-proxy.ts +3891 -0
- package/src/sync/index.ts +28 -0
- package/src/sync/restore-account.ts +90 -0
- package/src/sync/serialization.ts +39 -0
- package/src/sync/store-interfaces.ts +62 -0
- package/src/sync/sync-account.test.ts +302 -0
- package/src/sync/sync-account.ts +396 -0
- package/src/sync/types.ts +11 -0
- package/src/test-fixtures.ts +109 -0
- package/src/types.ts +1651 -0
- package/src/utils/account-state-snapshot.test.ts +595 -0
- package/src/utils/account-state-snapshot.ts +94 -0
- package/src/utils/backup-encryption.test.ts +442 -0
- package/src/utils/backup-encryption.ts +352 -0
- package/src/utils/batch-utilization.ts +1309 -0
- package/src/utils/constants.ts +20 -0
- package/src/utils/hex.ts +27 -0
- package/src/utils/key-derivation.ts +197 -0
- package/src/utils/storage-managers.ts +365 -0
- package/src/utils/ttl.ts +129 -0
- package/src/utils/url.test.ts +136 -0
- package/src/utils/url.ts +71 -0
- package/src/utils/versioned-storage.ts +323 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Encryption Module
|
|
3
|
+
*
|
|
4
|
+
* Provides AES-GCM-256 encryption for .swarmid backup files.
|
|
5
|
+
* The encryption key is derived from the account's swarmEncryptionKey
|
|
6
|
+
* (which is always persisted on all account types), so export requires
|
|
7
|
+
* NO re-authentication. Import requires auth to re-derive the key.
|
|
8
|
+
*
|
|
9
|
+
* Key chain: masterKey → swarmEncryptionKey (stored) → backupKey (HMAC-SHA256)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from "zod"
|
|
13
|
+
import { deriveSecret, hexToUint8Array } from "./key-derivation"
|
|
14
|
+
import {
|
|
15
|
+
serializeAccountStateSnapshot,
|
|
16
|
+
deserializeAccountStateSnapshot,
|
|
17
|
+
} from "./account-state-snapshot"
|
|
18
|
+
import type { AccountStateSnapshotResult } from "./account-state-snapshot"
|
|
19
|
+
import type { Account, Identity, ConnectedApp, PostageStamp } from "../schemas"
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Constants
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
const BACKUP_KEY_DERIVATION_CONTEXT = "swarm-id-backup-encryption-v1"
|
|
26
|
+
const BACKUP_VERSION = 1
|
|
27
|
+
const IV_LENGTH_BYTES = 12
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Schemas
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const BackupHeaderBaseSchemaV1 = z.object({
|
|
34
|
+
version: z.literal(BACKUP_VERSION),
|
|
35
|
+
accountName: z.string(),
|
|
36
|
+
exportedAt: z.number(),
|
|
37
|
+
ciphertext: z.string(),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const PasskeyBackupHeaderSchemaV1 = BackupHeaderBaseSchemaV1.extend({
|
|
41
|
+
accountType: z.literal("passkey"),
|
|
42
|
+
credentialId: z.string(),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Ethereum backup header fields (plaintext, not encrypted):
|
|
47
|
+
*
|
|
48
|
+
* - `ethereumAddress` — validates the user is connecting the correct wallet
|
|
49
|
+
* before attempting decryption.
|
|
50
|
+
*
|
|
51
|
+
* Key material is NOT stored in the export header. During import the user
|
|
52
|
+
* must enter their secret seed, and the master key is re-derived from
|
|
53
|
+
* `secretSeed + publicKey` via `deriveMasterKey()`.
|
|
54
|
+
*/
|
|
55
|
+
export const EthereumBackupHeaderSchemaV1 = BackupHeaderBaseSchemaV1.extend({
|
|
56
|
+
accountType: z.literal("ethereum"),
|
|
57
|
+
ethereumAddress: z.string(),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
export const AgentBackupHeaderSchemaV1 = BackupHeaderBaseSchemaV1.extend({
|
|
61
|
+
accountType: z.literal("agent"),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
export const EncryptedSwarmIdExportSchemaV1 = z.discriminatedUnion(
|
|
65
|
+
"accountType",
|
|
66
|
+
[
|
|
67
|
+
PasskeyBackupHeaderSchemaV1,
|
|
68
|
+
EthereumBackupHeaderSchemaV1,
|
|
69
|
+
AgentBackupHeaderSchemaV1,
|
|
70
|
+
],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Types
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
export type PasskeyBackupHeader = z.infer<typeof PasskeyBackupHeaderSchemaV1>
|
|
78
|
+
export type EthereumBackupHeader = z.infer<typeof EthereumBackupHeaderSchemaV1>
|
|
79
|
+
export type AgentBackupHeader = z.infer<typeof AgentBackupHeaderSchemaV1>
|
|
80
|
+
export type EncryptedSwarmIdExport = z.infer<
|
|
81
|
+
typeof EncryptedSwarmIdExportSchemaV1
|
|
82
|
+
>
|
|
83
|
+
|
|
84
|
+
export type ParseHeaderResult =
|
|
85
|
+
| { success: true; header: EncryptedSwarmIdExport }
|
|
86
|
+
| { success: false; error: string }
|
|
87
|
+
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// Key Derivation
|
|
90
|
+
// ============================================================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Derive an AES-GCM-256 CryptoKey for backup encryption from the stored
|
|
94
|
+
* swarmEncryptionKey. Uses HMAC-SHA256 with a fixed context string.
|
|
95
|
+
*/
|
|
96
|
+
export async function deriveBackupEncryptionKey(
|
|
97
|
+
swarmEncryptionKeyHex: string,
|
|
98
|
+
): Promise<CryptoKey> {
|
|
99
|
+
const backupKeyHex = await deriveSecret(
|
|
100
|
+
swarmEncryptionKeyHex,
|
|
101
|
+
BACKUP_KEY_DERIVATION_CONTEXT,
|
|
102
|
+
)
|
|
103
|
+
const keyBytes = hexToUint8Array(backupKeyHex)
|
|
104
|
+
|
|
105
|
+
return crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, [
|
|
106
|
+
"encrypt",
|
|
107
|
+
"decrypt",
|
|
108
|
+
])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Encrypt / Decrypt Payload
|
|
113
|
+
// ============================================================================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Encrypt a plaintext JSON string with AES-GCM-256.
|
|
117
|
+
* Returns base64-encoded [IV (12 bytes) || ciphertext+tag].
|
|
118
|
+
*/
|
|
119
|
+
export async function encryptBackupPayload(
|
|
120
|
+
plaintextJson: string,
|
|
121
|
+
key: CryptoKey,
|
|
122
|
+
): Promise<string> {
|
|
123
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES))
|
|
124
|
+
const encoded = new TextEncoder().encode(plaintextJson)
|
|
125
|
+
|
|
126
|
+
const ciphertextBuffer = await crypto.subtle.encrypt(
|
|
127
|
+
{ name: "AES-GCM", iv },
|
|
128
|
+
key,
|
|
129
|
+
encoded,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Concatenate IV + ciphertext+tag
|
|
133
|
+
const combined = new Uint8Array(iv.length + ciphertextBuffer.byteLength)
|
|
134
|
+
combined.set(iv, 0)
|
|
135
|
+
combined.set(new Uint8Array(ciphertextBuffer), iv.length)
|
|
136
|
+
|
|
137
|
+
return uint8ArrayToBase64(combined)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Decrypt a base64-encoded [IV (12 bytes) || ciphertext+tag] with AES-GCM-256.
|
|
142
|
+
* Returns the plaintext JSON string.
|
|
143
|
+
*/
|
|
144
|
+
export async function decryptBackupPayload(
|
|
145
|
+
ciphertextBase64: string,
|
|
146
|
+
key: CryptoKey,
|
|
147
|
+
): Promise<string> {
|
|
148
|
+
const combined = base64ToUint8Array(ciphertextBase64)
|
|
149
|
+
const iv = combined.slice(0, IV_LENGTH_BYTES)
|
|
150
|
+
const ciphertext = combined.slice(IV_LENGTH_BYTES)
|
|
151
|
+
|
|
152
|
+
const plaintextBuffer = await crypto.subtle.decrypt(
|
|
153
|
+
{ name: "AES-GCM", iv },
|
|
154
|
+
key,
|
|
155
|
+
ciphertext,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return new TextDecoder().decode(plaintextBuffer)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// Header Construction
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Build the plaintext header fields for an encrypted backup,
|
|
167
|
+
* based on the account type.
|
|
168
|
+
*
|
|
169
|
+
* `accountName` and `exportedAt` are deliberately kept in the plaintext
|
|
170
|
+
* header: accountName helps the user identify which credential to use,
|
|
171
|
+
* and exportedAt lets them assess backup recency.
|
|
172
|
+
*/
|
|
173
|
+
export type BackupHeaderWithoutCiphertext =
|
|
174
|
+
| Omit<PasskeyBackupHeader, "ciphertext">
|
|
175
|
+
| Omit<EthereumBackupHeader, "ciphertext">
|
|
176
|
+
| Omit<AgentBackupHeader, "ciphertext">
|
|
177
|
+
|
|
178
|
+
export function buildBackupHeader(
|
|
179
|
+
account: Account,
|
|
180
|
+
): BackupHeaderWithoutCiphertext {
|
|
181
|
+
const base = {
|
|
182
|
+
version: BACKUP_VERSION as typeof BACKUP_VERSION,
|
|
183
|
+
accountName: account.name,
|
|
184
|
+
exportedAt: Date.now(),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (account.type === "passkey") {
|
|
188
|
+
return {
|
|
189
|
+
...base,
|
|
190
|
+
accountType: "passkey" as const,
|
|
191
|
+
credentialId: account.credentialId,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (account.type === "ethereum") {
|
|
196
|
+
return {
|
|
197
|
+
...base,
|
|
198
|
+
accountType: "ethereum" as const,
|
|
199
|
+
ethereumAddress: account.ethereumAddress.toHex(),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// agent — no extra fields
|
|
204
|
+
return { ...base, accountType: "agent" as const }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// High-Level API
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create a fully encrypted .swarmid export object.
|
|
213
|
+
*
|
|
214
|
+
* 1. Serializes account data to plaintext JSON via serializeAccountStateSnapshot
|
|
215
|
+
* 2. Derives an AES-GCM-256 key from swarmEncryptionKey
|
|
216
|
+
* 3. Encrypts the JSON payload
|
|
217
|
+
* 4. Builds a header with account metadata + ciphertext
|
|
218
|
+
*/
|
|
219
|
+
export async function createEncryptedExport(
|
|
220
|
+
account: Account,
|
|
221
|
+
identities: Identity[],
|
|
222
|
+
connectedApps: ConnectedApp[],
|
|
223
|
+
postageStamps: PostageStamp[],
|
|
224
|
+
swarmEncryptionKeyHex: string,
|
|
225
|
+
): Promise<EncryptedSwarmIdExport> {
|
|
226
|
+
const now = Date.now()
|
|
227
|
+
const exportData = serializeAccountStateSnapshot({
|
|
228
|
+
accountId: account.id.toHex(),
|
|
229
|
+
metadata: {
|
|
230
|
+
accountName: account.name,
|
|
231
|
+
defaultPostageStampBatchID: account.defaultPostageStampBatchID?.toHex(),
|
|
232
|
+
createdAt: account.createdAt,
|
|
233
|
+
lastModified: now,
|
|
234
|
+
},
|
|
235
|
+
identities,
|
|
236
|
+
connectedApps,
|
|
237
|
+
postageStamps,
|
|
238
|
+
timestamp: now,
|
|
239
|
+
})
|
|
240
|
+
const plaintextJson = JSON.stringify(exportData)
|
|
241
|
+
const key = await deriveBackupEncryptionKey(swarmEncryptionKeyHex)
|
|
242
|
+
const ciphertext = await encryptBackupPayload(plaintextJson, key)
|
|
243
|
+
|
|
244
|
+
const header = buildBackupHeader(account)
|
|
245
|
+
return EncryptedSwarmIdExportSchemaV1.parse({ ...header, ciphertext })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Decrypt an encrypted .swarmid export and return the parsed inner data.
|
|
250
|
+
*
|
|
251
|
+
* 1. Validates the encrypted header with Zod
|
|
252
|
+
* 2. Derives the AES-GCM-256 key from swarmEncryptionKey
|
|
253
|
+
* 3. Decrypts the ciphertext
|
|
254
|
+
* 4. Parses the inner plaintext via deserializeAccountStateSnapshot
|
|
255
|
+
*/
|
|
256
|
+
export async function decryptEncryptedExport(
|
|
257
|
+
encryptedData: unknown,
|
|
258
|
+
swarmEncryptionKeyHex: string,
|
|
259
|
+
): Promise<AccountStateSnapshotResult> {
|
|
260
|
+
const headerResult = parseEncryptedExportHeader(encryptedData)
|
|
261
|
+
if (!headerResult.success) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
error: new z.ZodError([
|
|
265
|
+
{
|
|
266
|
+
code: "custom",
|
|
267
|
+
message: headerResult.error,
|
|
268
|
+
path: [],
|
|
269
|
+
},
|
|
270
|
+
]),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const key = await deriveBackupEncryptionKey(swarmEncryptionKeyHex)
|
|
275
|
+
|
|
276
|
+
let plaintextJson: string
|
|
277
|
+
try {
|
|
278
|
+
plaintextJson = await decryptBackupPayload(
|
|
279
|
+
headerResult.header.ciphertext,
|
|
280
|
+
key,
|
|
281
|
+
)
|
|
282
|
+
} catch {
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
error: new z.ZodError([
|
|
286
|
+
{
|
|
287
|
+
code: "custom",
|
|
288
|
+
message: "Decryption failed: wrong key or corrupted data",
|
|
289
|
+
path: ["ciphertext"],
|
|
290
|
+
},
|
|
291
|
+
]),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let innerData: unknown
|
|
296
|
+
try {
|
|
297
|
+
innerData = JSON.parse(plaintextJson)
|
|
298
|
+
} catch {
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
error: new z.ZodError([
|
|
302
|
+
{
|
|
303
|
+
code: "custom",
|
|
304
|
+
message: "Decrypted data is not valid JSON",
|
|
305
|
+
path: ["ciphertext"],
|
|
306
|
+
},
|
|
307
|
+
]),
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return deserializeAccountStateSnapshot(innerData)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Parse and validate just the encrypted export header (without decrypting).
|
|
315
|
+
* Useful for reading account metadata before attempting decryption.
|
|
316
|
+
*/
|
|
317
|
+
export function parseEncryptedExportHeader(data: unknown): ParseHeaderResult {
|
|
318
|
+
if (typeof data !== "object" || data === null) {
|
|
319
|
+
return { success: false, error: "Input must be a non-null object" }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const result = EncryptedSwarmIdExportSchemaV1.safeParse(data)
|
|
323
|
+
if (!result.success) {
|
|
324
|
+
return {
|
|
325
|
+
success: false,
|
|
326
|
+
error: result.error.issues.map((i) => i.message).join("; "),
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { success: true, header: result.data }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ============================================================================
|
|
334
|
+
// Base64 Helpers
|
|
335
|
+
// ============================================================================
|
|
336
|
+
|
|
337
|
+
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
338
|
+
let binary = ""
|
|
339
|
+
for (const byte of bytes) {
|
|
340
|
+
binary += String.fromCharCode(byte)
|
|
341
|
+
}
|
|
342
|
+
return btoa(binary)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function base64ToUint8Array(base64: string): Uint8Array {
|
|
346
|
+
const binary = atob(base64)
|
|
347
|
+
const bytes = new Uint8Array(binary.length)
|
|
348
|
+
for (let i = 0; i < binary.length; i++) {
|
|
349
|
+
bytes[i] = binary.charCodeAt(i)
|
|
350
|
+
}
|
|
351
|
+
return bytes
|
|
352
|
+
}
|