@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,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
|
+
}
|