@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.
Files changed (223) hide show
  1. package/README.md +431 -0
  2. package/dist/chunk/bmt.d.ts +17 -0
  3. package/dist/chunk/bmt.d.ts.map +1 -0
  4. package/dist/chunk/cac.d.ts +18 -0
  5. package/dist/chunk/cac.d.ts.map +1 -0
  6. package/dist/chunk/constants.d.ts +10 -0
  7. package/dist/chunk/constants.d.ts.map +1 -0
  8. package/dist/chunk/encrypted-cac.d.ts +48 -0
  9. package/dist/chunk/encrypted-cac.d.ts.map +1 -0
  10. package/dist/chunk/encryption.d.ts +86 -0
  11. package/dist/chunk/encryption.d.ts.map +1 -0
  12. package/dist/chunk/index.d.ts +6 -0
  13. package/dist/chunk/index.d.ts.map +1 -0
  14. package/dist/index.d.ts +46 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/proxy/act/act.d.ts +78 -0
  17. package/dist/proxy/act/act.d.ts.map +1 -0
  18. package/dist/proxy/act/crypto.d.ts +44 -0
  19. package/dist/proxy/act/crypto.d.ts.map +1 -0
  20. package/dist/proxy/act/grantee-list.d.ts +82 -0
  21. package/dist/proxy/act/grantee-list.d.ts.map +1 -0
  22. package/dist/proxy/act/history.d.ts +183 -0
  23. package/dist/proxy/act/history.d.ts.map +1 -0
  24. package/dist/proxy/act/index.d.ts +104 -0
  25. package/dist/proxy/act/index.d.ts.map +1 -0
  26. package/dist/proxy/chunking-encrypted.d.ts +14 -0
  27. package/dist/proxy/chunking-encrypted.d.ts.map +1 -0
  28. package/dist/proxy/chunking.d.ts +15 -0
  29. package/dist/proxy/chunking.d.ts.map +1 -0
  30. package/dist/proxy/download-data.d.ts +16 -0
  31. package/dist/proxy/download-data.d.ts.map +1 -0
  32. package/dist/proxy/feed-manifest.d.ts +62 -0
  33. package/dist/proxy/feed-manifest.d.ts.map +1 -0
  34. package/dist/proxy/feeds/epochs/async-finder.d.ts +77 -0
  35. package/dist/proxy/feeds/epochs/async-finder.d.ts.map +1 -0
  36. package/dist/proxy/feeds/epochs/epoch.d.ts +88 -0
  37. package/dist/proxy/feeds/epochs/epoch.d.ts.map +1 -0
  38. package/dist/proxy/feeds/epochs/finder.d.ts +67 -0
  39. package/dist/proxy/feeds/epochs/finder.d.ts.map +1 -0
  40. package/dist/proxy/feeds/epochs/index.d.ts +35 -0
  41. package/dist/proxy/feeds/epochs/index.d.ts.map +1 -0
  42. package/dist/proxy/feeds/epochs/test-utils.d.ts +93 -0
  43. package/dist/proxy/feeds/epochs/test-utils.d.ts.map +1 -0
  44. package/dist/proxy/feeds/epochs/types.d.ts +109 -0
  45. package/dist/proxy/feeds/epochs/types.d.ts.map +1 -0
  46. package/dist/proxy/feeds/epochs/updater.d.ts +68 -0
  47. package/dist/proxy/feeds/epochs/updater.d.ts.map +1 -0
  48. package/dist/proxy/feeds/epochs/utils.d.ts +22 -0
  49. package/dist/proxy/feeds/epochs/utils.d.ts.map +1 -0
  50. package/dist/proxy/feeds/index.d.ts +5 -0
  51. package/dist/proxy/feeds/index.d.ts.map +1 -0
  52. package/dist/proxy/feeds/sequence/async-finder.d.ts +14 -0
  53. package/dist/proxy/feeds/sequence/async-finder.d.ts.map +1 -0
  54. package/dist/proxy/feeds/sequence/finder.d.ts +17 -0
  55. package/dist/proxy/feeds/sequence/finder.d.ts.map +1 -0
  56. package/dist/proxy/feeds/sequence/index.d.ts +23 -0
  57. package/dist/proxy/feeds/sequence/index.d.ts.map +1 -0
  58. package/dist/proxy/feeds/sequence/types.d.ts +80 -0
  59. package/dist/proxy/feeds/sequence/types.d.ts.map +1 -0
  60. package/dist/proxy/feeds/sequence/updater.d.ts +26 -0
  61. package/dist/proxy/feeds/sequence/updater.d.ts.map +1 -0
  62. package/dist/proxy/index.d.ts +6 -0
  63. package/dist/proxy/index.d.ts.map +1 -0
  64. package/dist/proxy/manifest-builder.d.ts +183 -0
  65. package/dist/proxy/manifest-builder.d.ts.map +1 -0
  66. package/dist/proxy/mantaray-encrypted.d.ts +27 -0
  67. package/dist/proxy/mantaray-encrypted.d.ts.map +1 -0
  68. package/dist/proxy/mantaray.d.ts +26 -0
  69. package/dist/proxy/mantaray.d.ts.map +1 -0
  70. package/dist/proxy/types.d.ts +29 -0
  71. package/dist/proxy/types.d.ts.map +1 -0
  72. package/dist/proxy/upload-data.d.ts +17 -0
  73. package/dist/proxy/upload-data.d.ts.map +1 -0
  74. package/dist/proxy/upload-encrypted-data.d.ts +103 -0
  75. package/dist/proxy/upload-encrypted-data.d.ts.map +1 -0
  76. package/dist/schemas.d.ts +240 -0
  77. package/dist/schemas.d.ts.map +1 -0
  78. package/dist/storage/debounced-uploader.d.ts +62 -0
  79. package/dist/storage/debounced-uploader.d.ts.map +1 -0
  80. package/dist/storage/utilization-store.d.ts +108 -0
  81. package/dist/storage/utilization-store.d.ts.map +1 -0
  82. package/dist/swarm-id-auth.d.ts +74 -0
  83. package/dist/swarm-id-auth.d.ts.map +1 -0
  84. package/dist/swarm-id-auth.js +2 -0
  85. package/dist/swarm-id-auth.js.map +1 -0
  86. package/dist/swarm-id-client.d.ts +878 -0
  87. package/dist/swarm-id-client.d.ts.map +1 -0
  88. package/dist/swarm-id-client.js +2 -0
  89. package/dist/swarm-id-client.js.map +1 -0
  90. package/dist/swarm-id-proxy.d.ts +236 -0
  91. package/dist/swarm-id-proxy.d.ts.map +1 -0
  92. package/dist/swarm-id-proxy.js +2 -0
  93. package/dist/swarm-id-proxy.js.map +1 -0
  94. package/dist/swarm-id.esm.js +2 -0
  95. package/dist/swarm-id.esm.js.map +1 -0
  96. package/dist/swarm-id.umd.js +2 -0
  97. package/dist/swarm-id.umd.js.map +1 -0
  98. package/dist/sync/index.d.ts +9 -0
  99. package/dist/sync/index.d.ts.map +1 -0
  100. package/dist/sync/key-derivation.d.ts +25 -0
  101. package/dist/sync/key-derivation.d.ts.map +1 -0
  102. package/dist/sync/restore-account.d.ts +28 -0
  103. package/dist/sync/restore-account.d.ts.map +1 -0
  104. package/dist/sync/serialization.d.ts +16 -0
  105. package/dist/sync/serialization.d.ts.map +1 -0
  106. package/dist/sync/store-interfaces.d.ts +53 -0
  107. package/dist/sync/store-interfaces.d.ts.map +1 -0
  108. package/dist/sync/sync-account.d.ts +44 -0
  109. package/dist/sync/sync-account.d.ts.map +1 -0
  110. package/dist/sync/types.d.ts +13 -0
  111. package/dist/sync/types.d.ts.map +1 -0
  112. package/dist/test-fixtures.d.ts +17 -0
  113. package/dist/test-fixtures.d.ts.map +1 -0
  114. package/dist/types-BD_VkNn0.js +2 -0
  115. package/dist/types-BD_VkNn0.js.map +1 -0
  116. package/dist/types-lJCaT-50.js +2 -0
  117. package/dist/types-lJCaT-50.js.map +1 -0
  118. package/dist/types.d.ts +2157 -0
  119. package/dist/types.d.ts.map +1 -0
  120. package/dist/utils/account-payload.d.ts +94 -0
  121. package/dist/utils/account-payload.d.ts.map +1 -0
  122. package/dist/utils/account-state-snapshot.d.ts +38 -0
  123. package/dist/utils/account-state-snapshot.d.ts.map +1 -0
  124. package/dist/utils/backup-encryption.d.ts +127 -0
  125. package/dist/utils/backup-encryption.d.ts.map +1 -0
  126. package/dist/utils/batch-utilization.d.ts +432 -0
  127. package/dist/utils/batch-utilization.d.ts.map +1 -0
  128. package/dist/utils/constants.d.ts +11 -0
  129. package/dist/utils/constants.d.ts.map +1 -0
  130. package/dist/utils/hex.d.ts +17 -0
  131. package/dist/utils/hex.d.ts.map +1 -0
  132. package/dist/utils/key-derivation.d.ts +92 -0
  133. package/dist/utils/key-derivation.d.ts.map +1 -0
  134. package/dist/utils/storage-managers.d.ts +65 -0
  135. package/dist/utils/storage-managers.d.ts.map +1 -0
  136. package/dist/utils/swarm-id-export.d.ts +24 -0
  137. package/dist/utils/swarm-id-export.d.ts.map +1 -0
  138. package/dist/utils/ttl.d.ts +49 -0
  139. package/dist/utils/ttl.d.ts.map +1 -0
  140. package/dist/utils/url.d.ts +41 -0
  141. package/dist/utils/url.d.ts.map +1 -0
  142. package/dist/utils/versioned-storage.d.ts +131 -0
  143. package/dist/utils/versioned-storage.d.ts.map +1 -0
  144. package/package.json +78 -0
  145. package/src/chunk/bmt.test.ts +217 -0
  146. package/src/chunk/bmt.ts +57 -0
  147. package/src/chunk/cac.test.ts +214 -0
  148. package/src/chunk/cac.ts +65 -0
  149. package/src/chunk/constants.ts +18 -0
  150. package/src/chunk/encrypted-cac.test.ts +385 -0
  151. package/src/chunk/encrypted-cac.ts +131 -0
  152. package/src/chunk/encryption.test.ts +352 -0
  153. package/src/chunk/encryption.ts +300 -0
  154. package/src/chunk/index.ts +47 -0
  155. package/src/index.ts +430 -0
  156. package/src/proxy/act/act.test.ts +278 -0
  157. package/src/proxy/act/act.ts +158 -0
  158. package/src/proxy/act/bee-compat.test.ts +948 -0
  159. package/src/proxy/act/crypto.test.ts +436 -0
  160. package/src/proxy/act/crypto.ts +376 -0
  161. package/src/proxy/act/grantee-list.test.ts +393 -0
  162. package/src/proxy/act/grantee-list.ts +239 -0
  163. package/src/proxy/act/history.test.ts +360 -0
  164. package/src/proxy/act/history.ts +413 -0
  165. package/src/proxy/act/index.test.ts +748 -0
  166. package/src/proxy/act/index.ts +853 -0
  167. package/src/proxy/chunking-encrypted.ts +95 -0
  168. package/src/proxy/chunking.ts +65 -0
  169. package/src/proxy/download-data.ts +448 -0
  170. package/src/proxy/feed-manifest.ts +174 -0
  171. package/src/proxy/feeds/epochs/async-finder.ts +372 -0
  172. package/src/proxy/feeds/epochs/epoch.test.ts +249 -0
  173. package/src/proxy/feeds/epochs/epoch.ts +181 -0
  174. package/src/proxy/feeds/epochs/finder.ts +282 -0
  175. package/src/proxy/feeds/epochs/index.ts +73 -0
  176. package/src/proxy/feeds/epochs/integration.test.ts +1336 -0
  177. package/src/proxy/feeds/epochs/test-utils.ts +274 -0
  178. package/src/proxy/feeds/epochs/types.ts +128 -0
  179. package/src/proxy/feeds/epochs/updater.ts +192 -0
  180. package/src/proxy/feeds/epochs/utils.ts +62 -0
  181. package/src/proxy/feeds/index.ts +5 -0
  182. package/src/proxy/feeds/sequence/async-finder.ts +31 -0
  183. package/src/proxy/feeds/sequence/finder.ts +73 -0
  184. package/src/proxy/feeds/sequence/index.ts +54 -0
  185. package/src/proxy/feeds/sequence/integration.test.ts +966 -0
  186. package/src/proxy/feeds/sequence/types.ts +103 -0
  187. package/src/proxy/feeds/sequence/updater.ts +71 -0
  188. package/src/proxy/index.ts +5 -0
  189. package/src/proxy/manifest-builder.test.ts +427 -0
  190. package/src/proxy/manifest-builder.ts +679 -0
  191. package/src/proxy/mantaray-encrypted.ts +78 -0
  192. package/src/proxy/mantaray.ts +104 -0
  193. package/src/proxy/types.ts +32 -0
  194. package/src/proxy/upload-data.ts +189 -0
  195. package/src/proxy/upload-encrypted-data.ts +658 -0
  196. package/src/schemas.ts +299 -0
  197. package/src/storage/debounced-uploader.ts +192 -0
  198. package/src/storage/utilization-store.ts +397 -0
  199. package/src/swarm-id-client.test.ts +99 -0
  200. package/src/swarm-id-client.ts +3095 -0
  201. package/src/swarm-id-proxy.ts +3891 -0
  202. package/src/sync/index.ts +28 -0
  203. package/src/sync/restore-account.ts +90 -0
  204. package/src/sync/serialization.ts +39 -0
  205. package/src/sync/store-interfaces.ts +62 -0
  206. package/src/sync/sync-account.test.ts +302 -0
  207. package/src/sync/sync-account.ts +396 -0
  208. package/src/sync/types.ts +11 -0
  209. package/src/test-fixtures.ts +109 -0
  210. package/src/types.ts +1651 -0
  211. package/src/utils/account-state-snapshot.test.ts +595 -0
  212. package/src/utils/account-state-snapshot.ts +94 -0
  213. package/src/utils/backup-encryption.test.ts +442 -0
  214. package/src/utils/backup-encryption.ts +352 -0
  215. package/src/utils/batch-utilization.ts +1309 -0
  216. package/src/utils/constants.ts +20 -0
  217. package/src/utils/hex.ts +27 -0
  218. package/src/utils/key-derivation.ts +197 -0
  219. package/src/utils/storage-managers.ts +365 -0
  220. package/src/utils/ttl.ts +129 -0
  221. package/src/utils/url.test.ts +136 -0
  222. package/src/utils/url.ts +71 -0
  223. package/src/utils/versioned-storage.ts +323 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Unit tests for Grantee List Management
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest"
6
+ import {
7
+ serializeGranteeList,
8
+ deserializeGranteeList,
9
+ encryptGranteeList,
10
+ decryptGranteeList,
11
+ serializeAndEncryptGranteeList,
12
+ decryptAndDeserializeGranteeList,
13
+ addToGranteeList,
14
+ removeFromGranteeList,
15
+ deriveGranteeListEncryptionKey,
16
+ type UncompressedPublicKey,
17
+ } from "./grantee-list"
18
+ import { publicKeyFromPrivate } from "./crypto"
19
+
20
+ // Constants
21
+ const UNCOMPRESSED_PUBLIC_KEY_SIZE = 65
22
+ const UNCOMPRESSED_PREFIX = 0x04
23
+ const PUBLIC_KEY_COORD_SIZE = 32
24
+
25
+ // Helper to create a random 32-byte array
26
+ function randomBytes(length: number): Uint8Array {
27
+ const bytes = new Uint8Array(length)
28
+ crypto.getRandomValues(bytes)
29
+ return bytes
30
+ }
31
+
32
+ // Helper to create a test public key from seed
33
+ function createTestPublicKey(seed: number): UncompressedPublicKey {
34
+ const privKey = new Uint8Array(32)
35
+ privKey[31] = seed
36
+ return publicKeyFromPrivate(privKey)
37
+ }
38
+
39
+ // Helper to create multiple test public keys
40
+ function createTestPublicKeys(
41
+ count: number,
42
+ startSeed = 1,
43
+ ): UncompressedPublicKey[] {
44
+ return Array.from({ length: count }, (_, i) =>
45
+ createTestPublicKey(startSeed + i),
46
+ )
47
+ }
48
+
49
+ describe("Serialization format", () => {
50
+ it("serializeGranteeList should produce 65-byte entries per grantee", () => {
51
+ const grantees = createTestPublicKeys(3)
52
+ const serialized = serializeGranteeList(grantees)
53
+
54
+ expect(serialized.length).toBe(3 * UNCOMPRESSED_PUBLIC_KEY_SIZE)
55
+ })
56
+
57
+ it("serializeGranteeList should use 0x04 prefix for uncompressed keys", () => {
58
+ const grantees = createTestPublicKeys(2)
59
+ const serialized = serializeGranteeList(grantees)
60
+
61
+ // Check prefix for first grantee
62
+ expect(serialized[0]).toBe(UNCOMPRESSED_PREFIX)
63
+
64
+ // Check prefix for second grantee
65
+ expect(serialized[UNCOMPRESSED_PUBLIC_KEY_SIZE]).toBe(UNCOMPRESSED_PREFIX)
66
+ })
67
+
68
+ it("serializeGranteeList should correctly encode x and y coordinates", () => {
69
+ const grantee: UncompressedPublicKey = {
70
+ x: new Uint8Array(32).fill(0xaa),
71
+ y: new Uint8Array(32).fill(0xbb),
72
+ }
73
+ const serialized = serializeGranteeList([grantee])
74
+
75
+ // Check prefix
76
+ expect(serialized[0]).toBe(UNCOMPRESSED_PREFIX)
77
+
78
+ // Check x coordinate (bytes 1-32)
79
+ for (let i = 1; i <= PUBLIC_KEY_COORD_SIZE; i++) {
80
+ expect(serialized[i]).toBe(0xaa)
81
+ }
82
+
83
+ // Check y coordinate (bytes 33-64)
84
+ for (
85
+ let i = 1 + PUBLIC_KEY_COORD_SIZE;
86
+ i < UNCOMPRESSED_PUBLIC_KEY_SIZE;
87
+ i++
88
+ ) {
89
+ expect(serialized[i]).toBe(0xbb)
90
+ }
91
+ })
92
+
93
+ it("serializeGranteeList should handle empty list", () => {
94
+ const serialized = serializeGranteeList([])
95
+ expect(serialized.length).toBe(0)
96
+ })
97
+
98
+ it("deserializeGranteeList should parse concatenated keys", () => {
99
+ const grantees = createTestPublicKeys(3)
100
+ const serialized = serializeGranteeList(grantees)
101
+ const deserialized = deserializeGranteeList(serialized)
102
+
103
+ expect(deserialized.length).toBe(3)
104
+
105
+ for (let i = 0; i < 3; i++) {
106
+ expect(deserialized[i].x).toEqual(grantees[i].x)
107
+ expect(deserialized[i].y).toEqual(grantees[i].y)
108
+ }
109
+ })
110
+
111
+ it("deserializeGranteeList should handle empty data", () => {
112
+ const deserialized = deserializeGranteeList(new Uint8Array(0))
113
+ expect(deserialized).toEqual([])
114
+ })
115
+
116
+ it("deserializeGranteeList should throw on invalid length", () => {
117
+ // Not a multiple of 65
118
+ const invalidData = new Uint8Array(100)
119
+ expect(() => deserializeGranteeList(invalidData)).toThrow(
120
+ "Invalid grantee list length",
121
+ )
122
+ })
123
+
124
+ it("deserializeGranteeList should throw on invalid prefix", () => {
125
+ // Create valid-length data but with wrong prefix
126
+ const invalidData = new Uint8Array(65)
127
+ invalidData[0] = 0x02 // Wrong prefix (should be 0x04)
128
+
129
+ expect(() => deserializeGranteeList(invalidData)).toThrow(
130
+ "Invalid public key prefix",
131
+ )
132
+ })
133
+ })
134
+
135
+ describe("Encryption", () => {
136
+ it("deriveGranteeListEncryptionKey should derive consistent key", () => {
137
+ const publisherPrivKey = randomBytes(32)
138
+
139
+ const key1 = deriveGranteeListEncryptionKey(publisherPrivKey)
140
+ const key2 = deriveGranteeListEncryptionKey(publisherPrivKey)
141
+
142
+ expect(key1).toEqual(key2)
143
+ expect(key1.length).toBe(32)
144
+ })
145
+
146
+ it("deriveGranteeListEncryptionKey should produce different keys for different publishers", () => {
147
+ const privKey1 = randomBytes(32)
148
+ const privKey2 = randomBytes(32)
149
+
150
+ const key1 = deriveGranteeListEncryptionKey(privKey1)
151
+ const key2 = deriveGranteeListEncryptionKey(privKey2)
152
+
153
+ expect(key1).not.toEqual(key2)
154
+ })
155
+
156
+ it("encryptGranteeList/decryptGranteeList should roundtrip", () => {
157
+ const publisherPrivKey = randomBytes(32)
158
+ const grantees = createTestPublicKeys(3)
159
+ const serialized = serializeGranteeList(grantees)
160
+
161
+ const encrypted = encryptGranteeList(serialized, publisherPrivKey)
162
+ const decrypted = decryptGranteeList(encrypted, publisherPrivKey)
163
+
164
+ expect(decrypted).toEqual(serialized)
165
+ })
166
+
167
+ it("encryptGranteeList should produce different output than input", () => {
168
+ const publisherPrivKey = randomBytes(32)
169
+ const grantees = createTestPublicKeys(2)
170
+ const serialized = serializeGranteeList(grantees)
171
+
172
+ const encrypted = encryptGranteeList(serialized, publisherPrivKey)
173
+
174
+ // Encrypted data should differ from original
175
+ expect(encrypted).not.toEqual(serialized)
176
+ // But same length
177
+ expect(encrypted.length).toBe(serialized.length)
178
+ })
179
+
180
+ it("serializeAndEncryptGranteeList should combine operations", () => {
181
+ const publisherPrivKey = randomBytes(32)
182
+ const grantees = createTestPublicKeys(3)
183
+
184
+ const combined = serializeAndEncryptGranteeList(grantees, publisherPrivKey)
185
+
186
+ // Manual approach
187
+ const serialized = serializeGranteeList(grantees)
188
+ const encrypted = encryptGranteeList(serialized, publisherPrivKey)
189
+
190
+ expect(combined).toEqual(encrypted)
191
+ })
192
+
193
+ it("decryptAndDeserializeGranteeList should combine operations", () => {
194
+ const publisherPrivKey = randomBytes(32)
195
+ const grantees = createTestPublicKeys(3)
196
+
197
+ const encrypted = serializeAndEncryptGranteeList(grantees, publisherPrivKey)
198
+ const decrypted = decryptAndDeserializeGranteeList(
199
+ encrypted,
200
+ publisherPrivKey,
201
+ )
202
+
203
+ expect(decrypted.length).toBe(3)
204
+ for (let i = 0; i < 3; i++) {
205
+ expect(decrypted[i].x).toEqual(grantees[i].x)
206
+ expect(decrypted[i].y).toEqual(grantees[i].y)
207
+ }
208
+ })
209
+ })
210
+
211
+ describe("List operations", () => {
212
+ it("addToGranteeList should add new grantees to existing list", () => {
213
+ const publisherPrivKey = randomBytes(32)
214
+ const existingGrantees = createTestPublicKeys(2, 1)
215
+ const newGrantees = createTestPublicKeys(2, 100)
216
+
217
+ const existingEncrypted = serializeAndEncryptGranteeList(
218
+ existingGrantees,
219
+ publisherPrivKey,
220
+ )
221
+
222
+ const updatedEncrypted = addToGranteeList(
223
+ existingEncrypted,
224
+ newGrantees,
225
+ publisherPrivKey,
226
+ )
227
+
228
+ const result = decryptAndDeserializeGranteeList(
229
+ updatedEncrypted,
230
+ publisherPrivKey,
231
+ )
232
+
233
+ expect(result.length).toBe(4)
234
+
235
+ // Check existing grantees are present
236
+ for (const grantee of existingGrantees) {
237
+ expect(result.some((g) => g.x.every((v, i) => v === grantee.x[i]))).toBe(
238
+ true,
239
+ )
240
+ }
241
+
242
+ // Check new grantees are present
243
+ for (const grantee of newGrantees) {
244
+ expect(result.some((g) => g.x.every((v, i) => v === grantee.x[i]))).toBe(
245
+ true,
246
+ )
247
+ }
248
+ })
249
+
250
+ it("addToGranteeList should handle empty existing list", () => {
251
+ const publisherPrivKey = randomBytes(32)
252
+ const newGrantees = createTestPublicKeys(2)
253
+
254
+ const emptyEncrypted = serializeAndEncryptGranteeList([], publisherPrivKey)
255
+
256
+ const updatedEncrypted = addToGranteeList(
257
+ emptyEncrypted,
258
+ newGrantees,
259
+ publisherPrivKey,
260
+ )
261
+
262
+ const result = decryptAndDeserializeGranteeList(
263
+ updatedEncrypted,
264
+ publisherPrivKey,
265
+ )
266
+
267
+ expect(result.length).toBe(2)
268
+ })
269
+
270
+ it("removeFromGranteeList should remove specified grantees", () => {
271
+ const publisherPrivKey = randomBytes(32)
272
+ const grantees = createTestPublicKeys(4, 1)
273
+ const toRemove = [grantees[1], grantees[3]] // Remove 2nd and 4th
274
+
275
+ const encrypted = serializeAndEncryptGranteeList(grantees, publisherPrivKey)
276
+
277
+ const updatedEncrypted = removeFromGranteeList(
278
+ encrypted,
279
+ toRemove,
280
+ publisherPrivKey,
281
+ )
282
+
283
+ const result = decryptAndDeserializeGranteeList(
284
+ updatedEncrypted,
285
+ publisherPrivKey,
286
+ )
287
+
288
+ expect(result.length).toBe(2)
289
+
290
+ // Check removed grantees are not present
291
+ for (const removed of toRemove) {
292
+ expect(result.some((g) => g.x.every((v, i) => v === removed.x[i]))).toBe(
293
+ false,
294
+ )
295
+ }
296
+
297
+ // Check remaining grantees are present
298
+ expect(
299
+ result.some((g) => g.x.every((v, i) => v === grantees[0].x[i])),
300
+ ).toBe(true)
301
+ expect(
302
+ result.some((g) => g.x.every((v, i) => v === grantees[2].x[i])),
303
+ ).toBe(true)
304
+ })
305
+
306
+ it("removeFromGranteeList should handle removing non-existent grantees", () => {
307
+ const publisherPrivKey = randomBytes(32)
308
+ const grantees = createTestPublicKeys(2, 1)
309
+ const nonExistent = createTestPublicKeys(1, 100)
310
+
311
+ const encrypted = serializeAndEncryptGranteeList(grantees, publisherPrivKey)
312
+
313
+ const updatedEncrypted = removeFromGranteeList(
314
+ encrypted,
315
+ nonExistent,
316
+ publisherPrivKey,
317
+ )
318
+
319
+ const result = decryptAndDeserializeGranteeList(
320
+ updatedEncrypted,
321
+ publisherPrivKey,
322
+ )
323
+
324
+ // List should be unchanged
325
+ expect(result.length).toBe(2)
326
+ })
327
+
328
+ it("removeFromGranteeList should handle removing all grantees", () => {
329
+ const publisherPrivKey = randomBytes(32)
330
+ const grantees = createTestPublicKeys(2, 1)
331
+
332
+ const encrypted = serializeAndEncryptGranteeList(grantees, publisherPrivKey)
333
+
334
+ const updatedEncrypted = removeFromGranteeList(
335
+ encrypted,
336
+ grantees,
337
+ publisherPrivKey,
338
+ )
339
+
340
+ const result = decryptAndDeserializeGranteeList(
341
+ updatedEncrypted,
342
+ publisherPrivKey,
343
+ )
344
+
345
+ expect(result.length).toBe(0)
346
+ })
347
+ })
348
+
349
+ describe("Roundtrip tests", () => {
350
+ it("should preserve all data through serialize/encrypt/decrypt/deserialize cycle", () => {
351
+ const publisherPrivKey = randomBytes(32)
352
+ const grantees = createTestPublicKeys(5, 1)
353
+
354
+ const encrypted = serializeAndEncryptGranteeList(grantees, publisherPrivKey)
355
+ const result = decryptAndDeserializeGranteeList(encrypted, publisherPrivKey)
356
+
357
+ expect(result.length).toBe(5)
358
+
359
+ for (let i = 0; i < 5; i++) {
360
+ expect(result[i].x).toEqual(grantees[i].x)
361
+ expect(result[i].y).toEqual(grantees[i].y)
362
+ }
363
+ })
364
+
365
+ it("should handle large number of grantees", () => {
366
+ const publisherPrivKey = randomBytes(32)
367
+ const grantees = createTestPublicKeys(100, 1)
368
+
369
+ const encrypted = serializeAndEncryptGranteeList(grantees, publisherPrivKey)
370
+ const result = decryptAndDeserializeGranteeList(encrypted, publisherPrivKey)
371
+
372
+ expect(result.length).toBe(100)
373
+ })
374
+
375
+ it("different publishers cannot decrypt each other's grantee lists", () => {
376
+ const publisherPrivKey1 = randomBytes(32)
377
+ const publisherPrivKey2 = randomBytes(32)
378
+ const grantees = createTestPublicKeys(2)
379
+
380
+ const encrypted = serializeAndEncryptGranteeList(
381
+ grantees,
382
+ publisherPrivKey1,
383
+ )
384
+
385
+ // Attempting to decrypt with wrong key will produce garbage
386
+ // The decrypted data may not be a valid grantee list format
387
+ const wrongDecrypted = decryptGranteeList(encrypted, publisherPrivKey2)
388
+
389
+ // The data will be different from original serialized
390
+ const correctSerialized = serializeGranteeList(grantees)
391
+ expect(wrongDecrypted).not.toEqual(correctSerialized)
392
+ })
393
+ })
@@ -0,0 +1,239 @@
1
+ import { Binary } from "cafe-utility"
2
+ import { counterModeEncrypt, counterModeDecrypt } from "./crypto"
3
+
4
+ // Grantee list format constants
5
+ const UNCOMPRESSED_PUBLIC_KEY_SIZE = 65 // 0x04 prefix + 32 byte X + 32 byte Y
6
+ const UNCOMPRESSED_PREFIX = 0x04
7
+ const PUBLIC_KEY_COORD_SIZE = 32
8
+
9
+ // Grantee list encryption key derivation suffix
10
+ const GRANTEE_LIST_KEY_SUFFIX = new TextEncoder().encode("act-grantee-list")
11
+
12
+ /**
13
+ * Public key in uncompressed format
14
+ */
15
+ export interface UncompressedPublicKey {
16
+ x: Uint8Array // 32 bytes
17
+ y: Uint8Array // 32 bytes
18
+ }
19
+
20
+ /**
21
+ * Derive the encryption key for the grantee list
22
+ *
23
+ * This is different from the legacy format key derivation.
24
+ * The grantee list is encrypted with a key derived from the publisher's private key.
25
+ */
26
+ export function deriveGranteeListEncryptionKey(
27
+ publisherPrivKey: Uint8Array,
28
+ ): Uint8Array {
29
+ const input = Binary.concatBytes(publisherPrivKey, GRANTEE_LIST_KEY_SUFFIX)
30
+ return Binary.keccak256(input)
31
+ }
32
+
33
+ /**
34
+ * Serialize grantee list to Bee-compatible format
35
+ *
36
+ * Bee stores grantees as concatenated 65-byte uncompressed secp256k1 public keys.
37
+ * Format: [0x04 || X (32 bytes) || Y (32 bytes)] for each key
38
+ *
39
+ * @param grantees - Array of public keys with x and y coordinates
40
+ * @returns Serialized grantee list
41
+ */
42
+ export function serializeGranteeList(
43
+ grantees: UncompressedPublicKey[],
44
+ ): Uint8Array {
45
+ const result = new Uint8Array(grantees.length * UNCOMPRESSED_PUBLIC_KEY_SIZE)
46
+
47
+ for (let i = 0; i < grantees.length; i++) {
48
+ const offset = i * UNCOMPRESSED_PUBLIC_KEY_SIZE
49
+
50
+ // 0x04 prefix for uncompressed point
51
+ result[offset] = UNCOMPRESSED_PREFIX
52
+
53
+ // X coordinate (32 bytes)
54
+ result.set(grantees[i].x, offset + 1)
55
+
56
+ // Y coordinate (32 bytes)
57
+ result.set(grantees[i].y, offset + 1 + PUBLIC_KEY_COORD_SIZE)
58
+ }
59
+
60
+ return result
61
+ }
62
+
63
+ /**
64
+ * Deserialize grantee list from Bee-compatible format
65
+ *
66
+ * @param data - Serialized grantee list (concatenated 65-byte uncompressed keys)
67
+ * @returns Array of public keys with x and y coordinates
68
+ */
69
+ export function deserializeGranteeList(
70
+ data: Uint8Array,
71
+ ): UncompressedPublicKey[] {
72
+ if (data.length === 0) {
73
+ return []
74
+ }
75
+
76
+ if (data.length % UNCOMPRESSED_PUBLIC_KEY_SIZE !== 0) {
77
+ throw new Error(
78
+ `Invalid grantee list length: ${data.length} is not a multiple of ${UNCOMPRESSED_PUBLIC_KEY_SIZE}`,
79
+ )
80
+ }
81
+
82
+ const granteeCount = data.length / UNCOMPRESSED_PUBLIC_KEY_SIZE
83
+ const grantees: UncompressedPublicKey[] = []
84
+
85
+ for (let i = 0; i < granteeCount; i++) {
86
+ const offset = i * UNCOMPRESSED_PUBLIC_KEY_SIZE
87
+
88
+ // Verify 0x04 prefix
89
+ if (data[offset] !== UNCOMPRESSED_PREFIX) {
90
+ throw new Error(
91
+ `Invalid public key prefix at index ${i}: expected 0x04, got 0x${data[offset].toString(16)}`,
92
+ )
93
+ }
94
+
95
+ // Extract X coordinate
96
+ const x = data.slice(offset + 1, offset + 1 + PUBLIC_KEY_COORD_SIZE)
97
+
98
+ // Extract Y coordinate
99
+ const y = data.slice(
100
+ offset + 1 + PUBLIC_KEY_COORD_SIZE,
101
+ offset + UNCOMPRESSED_PUBLIC_KEY_SIZE,
102
+ )
103
+
104
+ grantees.push({ x, y })
105
+ }
106
+
107
+ return grantees
108
+ }
109
+
110
+ /**
111
+ * Encrypt a serialized grantee list for storage
112
+ *
113
+ * @param granteeList - Serialized grantee list
114
+ * @param publisherPrivKey - Publisher's private key for key derivation
115
+ * @returns Encrypted grantee list
116
+ */
117
+ export function encryptGranteeList(
118
+ granteeList: Uint8Array,
119
+ publisherPrivKey: Uint8Array,
120
+ ): Uint8Array {
121
+ const encryptionKey = deriveGranteeListEncryptionKey(publisherPrivKey)
122
+ return counterModeEncrypt(granteeList, encryptionKey)
123
+ }
124
+
125
+ /**
126
+ * Decrypt an encrypted grantee list
127
+ *
128
+ * @param encryptedList - Encrypted grantee list
129
+ * @param publisherPrivKey - Publisher's private key for key derivation
130
+ * @returns Decrypted serialized grantee list
131
+ */
132
+ export function decryptGranteeList(
133
+ encryptedList: Uint8Array,
134
+ publisherPrivKey: Uint8Array,
135
+ ): Uint8Array {
136
+ const encryptionKey = deriveGranteeListEncryptionKey(publisherPrivKey)
137
+ return counterModeDecrypt(encryptedList, encryptionKey)
138
+ }
139
+
140
+ /**
141
+ * Serialize and encrypt grantee list in one step
142
+ *
143
+ * @param grantees - Array of public keys
144
+ * @param publisherPrivKey - Publisher's private key
145
+ * @returns Encrypted serialized grantee list ready for upload
146
+ */
147
+ export function serializeAndEncryptGranteeList(
148
+ grantees: UncompressedPublicKey[],
149
+ publisherPrivKey: Uint8Array,
150
+ ): Uint8Array {
151
+ const serialized = serializeGranteeList(grantees)
152
+ return encryptGranteeList(serialized, publisherPrivKey)
153
+ }
154
+
155
+ /**
156
+ * Decrypt and deserialize grantee list in one step
157
+ *
158
+ * @param encryptedList - Encrypted serialized grantee list
159
+ * @param publisherPrivKey - Publisher's private key
160
+ * @returns Array of public keys
161
+ */
162
+ export function decryptAndDeserializeGranteeList(
163
+ encryptedList: Uint8Array,
164
+ publisherPrivKey: Uint8Array,
165
+ ): UncompressedPublicKey[] {
166
+ const decrypted = decryptGranteeList(encryptedList, publisherPrivKey)
167
+ return deserializeGranteeList(decrypted)
168
+ }
169
+
170
+ /**
171
+ * Add grantees to an existing encrypted grantee list
172
+ *
173
+ * @param encryptedList - Existing encrypted grantee list
174
+ * @param newGrantees - New grantees to add
175
+ * @param publisherPrivKey - Publisher's private key
176
+ * @returns New encrypted grantee list with added grantees
177
+ */
178
+ export function addToGranteeList(
179
+ encryptedList: Uint8Array,
180
+ newGrantees: UncompressedPublicKey[],
181
+ publisherPrivKey: Uint8Array,
182
+ ): Uint8Array {
183
+ // Decrypt existing list
184
+ const existingGrantees =
185
+ encryptedList.length > 0
186
+ ? decryptAndDeserializeGranteeList(encryptedList, publisherPrivKey)
187
+ : []
188
+
189
+ // Add new grantees
190
+ const updatedGrantees = [...existingGrantees, ...newGrantees]
191
+
192
+ // Re-encrypt
193
+ return serializeAndEncryptGranteeList(updatedGrantees, publisherPrivKey)
194
+ }
195
+
196
+ /**
197
+ * Remove grantees from an existing encrypted grantee list
198
+ *
199
+ * @param encryptedList - Existing encrypted grantee list
200
+ * @param revokeGrantees - Grantees to remove
201
+ * @param publisherPrivKey - Publisher's private key
202
+ * @returns New encrypted grantee list with removed grantees
203
+ */
204
+ export function removeFromGranteeList(
205
+ encryptedList: Uint8Array,
206
+ revokeGrantees: UncompressedPublicKey[],
207
+ publisherPrivKey: Uint8Array,
208
+ ): Uint8Array {
209
+ // Decrypt existing list
210
+ const existingGrantees = decryptAndDeserializeGranteeList(
211
+ encryptedList,
212
+ publisherPrivKey,
213
+ )
214
+
215
+ // Filter out revoked grantees
216
+ const remainingGrantees = existingGrantees.filter((existing) => {
217
+ return !revokeGrantees.some((revoke) => publicKeysEqual(existing, revoke))
218
+ })
219
+
220
+ // Re-encrypt
221
+ return serializeAndEncryptGranteeList(remainingGrantees, publisherPrivKey)
222
+ }
223
+
224
+ /**
225
+ * Check if two public keys are equal
226
+ */
227
+ function publicKeysEqual(
228
+ a: UncompressedPublicKey,
229
+ b: UncompressedPublicKey,
230
+ ): boolean {
231
+ if (a.x.length !== b.x.length || a.y.length !== b.y.length) return false
232
+ for (let i = 0; i < a.x.length; i++) {
233
+ if (a.x[i] !== b.x[i]) return false
234
+ }
235
+ for (let i = 0; i < a.y.length; i++) {
236
+ if (a.y[i] !== b.y[i]) return false
237
+ }
238
+ return true
239
+ }