|
| 1 | +--- |
| 2 | +lip: 29 |
| 3 | +title: Encrypted Assets |
| 4 | +author: b00ste |
| 5 | +discussions-to: |
| 6 | +status: Draft |
| 7 | +type: LSP |
| 8 | +created: 2025-01-08 |
| 9 | +requires: ERC725Y, LSP2 |
| 10 | +--- |
| 11 | + |
| 12 | +## Simple Summary |
| 13 | + |
| 14 | +A standard for storing encrypted digital assets in [ERC725Y] smart contracts, enabling creators to manage token-gated content directly on their Universal Profile. |
| 15 | + |
| 16 | +## Abstract |
| 17 | + |
| 18 | +This standard defines a set of [ERC725Y] data keys for storing references to encrypted digital assets. The encrypted content is stored on IPFS, while metadata and access control information are encoded as [VerifiableURI] values in the Universal Profile's storage. The standard supports versioning, allowing creators to update content while preserving full revision history. Each asset can include social feed images for content preview and discovery. |
| 19 | + |
| 20 | +## Motivation |
| 21 | + |
| 22 | +LUKSO currently has no standard for storing encrypted, token-gated digital assets. While LSP4 defines metadata for digital assets (LSP7/LSP8 tokens), it only supports unencrypted content. Creators who want to offer exclusive, encrypted content to token holders have no standardized way to: |
| 23 | + |
| 24 | +1. **Store Encrypted Content**: No defined schema for encrypted asset metadata |
| 25 | +2. **Link to Creator**: No way to associate encrypted content with a Universal Profile |
| 26 | +3. **Track Versions**: No mechanism for updating content while preserving history |
| 27 | +4. **Enable Discovery**: No standard for enumerating a creator's encrypted offerings |
| 28 | + |
| 29 | +LSP29 introduces a complete solution by: |
| 30 | + |
| 31 | +1. **Defining a Schema**: Standardized JSON format for encrypted asset metadata |
| 32 | +2. **Centralizing on Profile**: All encrypted assets stored on the creator's Universal Profile |
| 33 | +3. **Supporting Versioning**: Append-only array with revision tracking preserves full history |
| 34 | +4. **Enabling Discovery**: Easy enumeration via array iteration and mapping lookups |
| 35 | +5. **Flexible Access Control**: Each asset can reference different token gates for decryption |
| 36 | +6. **Social Feed Integration**: Images for content preview and social discovery |
| 37 | + |
| 38 | +## Specification |
| 39 | + |
| 40 | +### ERC725Y Data Keys |
| 41 | + |
| 42 | +#### LSP29EncryptedAssets[] |
| 43 | + |
| 44 | +An [LSP2 Array] of [VerifiableURI] values, each pointing to an encrypted asset's JSON metadata on IPFS. |
| 45 | + |
| 46 | +```json |
| 47 | +{ |
| 48 | + "name": "LSP29EncryptedAssets[]", |
| 49 | + "key": "0x1965f98377ddff08e78c93d820cc8de4eeb331e684b7724bce0debb1958386c3", |
| 50 | + "keyType": "Array", |
| 51 | + "valueType": "bytes", |
| 52 | + "valueContent": "VerifiableURI" |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +For more information about how to access each index of the `LSP29EncryptedAssets[]` array, see [LSP2 Array]. |
| 57 | + |
| 58 | +#### LSP29EncryptedAssetsMap |
| 59 | + |
| 60 | +An [LSP2 Mapping] from a content identifier hash to the array index. This mapping supports two usage patterns: |
| 61 | + |
| 62 | +1. **Latest version**: Hash of content ID only → points to the most recent revision |
| 63 | +2. **Specific version**: Hash of content ID + revision → points to that exact revision |
| 64 | + |
| 65 | +```json |
| 66 | +{ |
| 67 | + "name": "LSP29EncryptedAssetsMap:<bytes20>", |
| 68 | + "key": "0x2b9a7a38a67cedc507c20000<bytes20>", |
| 69 | + "keyType": "Mapping", |
| 70 | + "valueType": "uint128", |
| 71 | + "valueContent": "Number" |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +**For latest version**: `<bytes20>` is the first 20 bytes of `keccak256(contentId)`, where `contentId` is the string identifier chosen by the creator. This entry is updated each time a new revision is added. |
| 76 | + |
| 77 | +**For specific version**: `<bytes20>` is the first 20 bytes of `keccak256(abi.encodePacked(contentId, uint32(revision)))`. This entry is immutable once set. |
| 78 | + |
| 79 | +**Examples** for content ID `"premium-content"`: |
| 80 | + |
| 81 | +| Lookup Type | Hash Input | Key | |
| 82 | +| ----------- | ----------------------------------------------------------- | ----------------------------------- | |
| 83 | +| Latest | `keccak256("premium-content")` | `0x2b9a7a38a67cedc507c200008a5b...` | |
| 84 | +| Revision 1 | `keccak256(abi.encodePacked("premium-content", uint32(1)))` | `0x2b9a7a38a67cedc507c2000012ab...` | |
| 85 | +| Revision 2 | `keccak256(abi.encodePacked("premium-content", uint32(2)))` | `0x2b9a7a38a67cedc507c200003c7f...` | |
| 86 | + |
| 87 | +#### LSP29EncryptedAssetRevisionCount |
| 88 | + |
| 89 | +An [LSP2 Mapping] from a content identifier hash to the total number of revisions for that content. |
| 90 | + |
| 91 | +```json |
| 92 | +{ |
| 93 | + "name": "LSP29EncryptedAssetRevisionCount:<bytes32>", |
| 94 | + "key": "0xb41f63e335c22bded8140000<bytes20>", |
| 95 | + "keyType": "Mapping", |
| 96 | + "valueType": "uint128", |
| 97 | + "valueContent": "Number" |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +Where `<bytes20>` is the first 20 bytes of `keccak256(contentId)`. |
| 102 | + |
| 103 | +### JSON Schema |
| 104 | + |
| 105 | +The [VerifiableURI] stored in the array MUST point to a JSON file on IPFS conforming to the following schema: |
| 106 | + |
| 107 | +```json |
| 108 | +{ |
| 109 | + "LSP29EncryptedAsset": { |
| 110 | + "version": "1.0.0", |
| 111 | + "id": "<string>", |
| 112 | + "title": "<string>", |
| 113 | + "description": "<string>", |
| 114 | + "images": "[[<image>, ...], ...]", |
| 115 | + "revision": "<number>", |
| 116 | + "createdAt": "<string>", |
| 117 | + "file": { |
| 118 | + "type": "<string>", |
| 119 | + "name": "<string>", |
| 120 | + "size": "<number>", |
| 121 | + "lastModified": "<number>", |
| 122 | + "hash": "<string>" |
| 123 | + }, |
| 124 | + "encryption": { |
| 125 | + "method": "<string>", |
| 126 | + "ciphertext": "<string>", |
| 127 | + "dataToEncryptHash": "<string>", |
| 128 | + "accessControlConditions": "<array>", |
| 129 | + "decryptionCode": "<string>", |
| 130 | + "decryptionParams": "<object>" |
| 131 | + }, |
| 132 | + "chunks": { |
| 133 | + "cids": "[<string>, ...]", |
| 134 | + "iv": "<string>", |
| 135 | + "totalSize": "<number>" |
| 136 | + } |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +#### LSP29EncryptedAsset |
| 142 | + |
| 143 | +| Key | Type | Required | Description | |
| 144 | +| ------------- | ------ | -------- | --------------------------------------------------------- | |
| 145 | +| `version` | string | Yes | Schema version (e.g., `"1.0.0"`) | |
| 146 | +| `id` | string | Yes | Unique content identifier chosen by creator | |
| 147 | +| `title` | string | Yes | Human-readable title for the content | |
| 148 | +| `description` | string | No | Human-readable description of the content | |
| 149 | +| `images` | array | No | Social feed images for content preview | |
| 150 | +| `revision` | number | Yes | Version number starting at 1, incremented for each update | |
| 151 | +| `createdAt` | string | Yes | ISO 8601 timestamp when this revision was created | |
| 152 | +| `file` | object | Yes | Technical metadata about the encrypted file | |
| 153 | +| `encryption` | object | Yes | Encryption metadata for decryption | |
| 154 | +| `chunks` | object | Yes | Chunked storage information | |
| 155 | + |
| 156 | +#### file |
| 157 | + |
| 158 | +| Key | Type | Required | Description | |
| 159 | +| -------------- | ------ | -------- | ---------------------------------------------------- | |
| 160 | +| `type` | string | Yes | MIME type of the original file (e.g., `"video/mp4"`) | |
| 161 | +| `name` | string | Yes | Original filename | |
| 162 | +| `size` | number | Yes | Original file size in bytes (before encryption) | |
| 163 | +| `lastModified` | number | No | Unix timestamp (ms) of file's last modification | |
| 164 | +| `hash` | string | Yes | Hash of the original file content (SHA-256, hex) | |
| 165 | + |
| 166 | +#### images |
| 167 | + |
| 168 | +An array of image arrays for social feed functionality. Each inner array contains different sizes of the same image, following the LSP4Metadata images structure. The first image in the first inner array should be used as the main preview image. |
| 169 | + |
| 170 | +**Structure:** |
| 171 | + |
| 172 | +```json |
| 173 | +"images": [ |
| 174 | + [ |
| 175 | + { |
| 176 | + "width": "number", |
| 177 | + "height": "number", |
| 178 | + "url": "string", |
| 179 | + "verification": { |
| 180 | + "method": "keccak256(bytes)", |
| 181 | + "data": "string" |
| 182 | + } |
| 183 | + }, |
| 184 | + { |
| 185 | + "width": "number", |
| 186 | + "height": "number", |
| 187 | + "url": "string", |
| 188 | + "verification": { |
| 189 | + "method": "ecdsa", |
| 190 | + "data": "string", |
| 191 | + "source": "string" |
| 192 | + } |
| 193 | + } |
| 194 | + ], |
| 195 | + [ |
| 196 | + // Additional image sets |
| 197 | + ] |
| 198 | +] |
| 199 | +``` |
| 200 | + |
| 201 | +| Key | Type | Required | Description | |
| 202 | +| -------------- | ------ | -------- | --------------------------------------------------- | |
| 203 | +| `width` | number | Yes | Image width in pixels | |
| 204 | +| `height` | number | Yes | Image height in pixels | |
| 205 | +| `url` | string | Yes | URL to the image file | |
| 206 | +| `verification` | object | Yes | Verification data for image authenticity | |
| 207 | +| `method` | string | Yes | Verification method (`keccak256(bytes)` or `ecdsa`) | |
| 208 | +| `data` | string | Yes | Hash or signature for verification | |
| 209 | +| `source` | string | No | URL for signature verification (ecdsa method only) | |
| 210 | + |
| 211 | +#### encryption |
| 212 | + |
| 213 | +| Key | Type | Required | Description | |
| 214 | +| ------------------------- | ------ | -------- | ---------------------------------------------------------- | |
| 215 | +| `method` | string | Yes | Encryption method identifier (see supported methods below) | |
| 216 | +| `ciphertext` | string | Yes | Encrypted symmetric key | |
| 217 | +| `dataToEncryptHash` | string | Yes | Hash of the encrypted data for verification | |
| 218 | +| `accessControlConditions` | array | Yes | Conditions for decryption access | |
| 219 | +| `decryptionCode` | string | Yes | Code or reference for decryption logic | |
| 220 | +| `decryptionParams` | object | Yes | Dynamic parameters embedded in `decryptionCode` | |
| 221 | + |
| 222 | +The `decryptionParams` object contains the dynamic values that are hardcoded into the `decryptionCode`. This enables UI display and content filtering without parsing the decryption code. See [Decryption Parameters Security](#decryption-parameters-security) for important security considerations. |
| 223 | + |
| 224 | +**Supported Encryption Methods:** |
| 225 | + |
| 226 | +| Method | Description | Example `decryptionParams` | |
| 227 | +| ------------------------------ | -------------------------------------- | -------------------------------------------------------------------------------------- | |
| 228 | +| `lit-digital-asset-balance-v1` | Digital asset balance via Lit Protocol | `{ "tokenAddress": "0x...", "requiredBalance": "1000000" }` | |
| 229 | +| `lit-lsp8-ownership-v1` | LSP8 NFT ownership via Lit Protocol | `{ "tokenAddress": "0x...", "requiredTokenId": "42" }` | |
| 230 | +| `lit-lsp26-follower-v1` | LSP26 on-chain follower check | `{ "followedAddresses": ["0x...", "0x..."] }` | |
| 231 | +| `lit-social-followers-v1` | Off-chain social verification | `{ "platform": "twitter", "creatorHandle": "@creator", "requiredFollowers": "10000" }` | |
| 232 | +| `lit-time-locked-v1` | Time-lock via Lit Protocol | `{ "unlockTimestamp": "1735689600" }` | |
| 233 | + |
| 234 | +#### chunks |
| 235 | + |
| 236 | +| Key | Type | Required | Description | |
| 237 | +| ----------- | ------ | -------- | ------------------------------------------------------- | |
| 238 | +| `cids` | array | Yes | Array of IPFS CIDs for encrypted content chunks | |
| 239 | +| `iv` | string | Yes | Initialization vector for symmetric encryption (base64) | |
| 240 | +| `totalSize` | number | Yes | Total size of encrypted content in bytes | |
| 241 | + |
| 242 | +### Data Flow |
| 243 | + |
| 244 | +#### Creating New Content |
| 245 | + |
| 246 | +1. Creator chooses a unique `id` (e.g., `"exclusive-album-2025"`) |
| 247 | +2. Set `revision` to `1` |
| 248 | +3. Set `createdAt` to current ISO 8601 timestamp |
| 249 | +4. Encrypt content and upload chunks to IPFS |
| 250 | +5. Create JSON metadata and upload to IPFS |
| 251 | +6. Encode as [VerifiableURI] |
| 252 | +7. Write to ERC725Y storage: |
| 253 | + - Append to `LSP29EncryptedAssets[]` array |
| 254 | + - Set `LSP29EncryptedAssetsMap:<keccak256(id)>` to new array index (latest) |
| 255 | + - Set `LSP29EncryptedAssetsMap:<keccak256(id + revision)>` to new array index (version 1) |
| 256 | + - Set `LSP29EncryptedAssetRevisionCount:<id>` to `1` |
| 257 | + |
| 258 | +#### Updating Content (New Revision) |
| 259 | + |
| 260 | +1. Use the same `id` as the original content |
| 261 | +2. Read `LSP29EncryptedAssetRevisionCount:<id>` to get current revision count |
| 262 | +3. Set `revision` to `currentCount + 1` |
| 263 | +4. Set `createdAt` to current ISO 8601 timestamp |
| 264 | +5. Encrypt new content and upload chunks to IPFS |
| 265 | +6. Create JSON metadata and upload to IPFS |
| 266 | +7. Encode as [VerifiableURI] |
| 267 | +8. Write to ERC725Y storage: |
| 268 | + - Append to `LSP29EncryptedAssets[]` array |
| 269 | + - Update `LSP29EncryptedAssetsMap:<keccak256(id)>` to new array index (latest) |
| 270 | + - Set `LSP29EncryptedAssetsMap:<keccak256(id + revision)>` to new array index (this version) |
| 271 | + - Increment `LSP29EncryptedAssetRevisionCount:<id>` |
| 272 | + |
| 273 | +#### Reading Latest Version |
| 274 | + |
| 275 | +1. Compute key: `LSP29EncryptedAssetsMap:<keccak256(id)>` |
| 276 | +2. Read array index from mapping |
| 277 | +3. Read [VerifiableURI] from `LSP29EncryptedAssets[index]` |
| 278 | +4. Fetch and verify JSON from IPFS |
| 279 | + |
| 280 | +#### Reading Specific Version |
| 281 | + |
| 282 | +1. Compute key: `LSP29EncryptedAssetsMap:<keccak256(abi.encodePacked(id, uint32(revision)))>` |
| 283 | +2. Read array index from mapping |
| 284 | +3. Read [VerifiableURI] from `LSP29EncryptedAssets[index]` |
| 285 | +4. Fetch and verify JSON from IPFS |
| 286 | + |
| 287 | +#### Enumerating All Versions |
| 288 | + |
| 289 | +1. Read `LSP29EncryptedAssetRevisionCount:<id>` to get total count |
| 290 | +2. For each revision 1 to count: |
| 291 | + - Compute key: `LSP29EncryptedAssetsMap:<keccak256(abi.encodePacked(id, uint32(revision)))>` |
| 292 | + - Read array index and fetch corresponding element |
| 293 | + |
| 294 | +## Rationale |
| 295 | + |
| 296 | +### Append-Only Array |
| 297 | + |
| 298 | +The array is designed to be append-only to ensure: |
| 299 | + |
| 300 | +- **Immutability**: Once content is published, it cannot be removed or altered |
| 301 | +- **Audit Trail**: Full history of all content versions is preserved |
| 302 | +- **Verifiability**: Third parties can verify the complete history |
| 303 | + |
| 304 | +### Single Mapping with Dual Purpose |
| 305 | + |
| 306 | +A single mapping (`LSP29EncryptedAssetsMap`) serves both use cases through different hash inputs: |
| 307 | + |
| 308 | +- **Latest version**: Hash of content ID only → always points to most recent revision (updated on each new version) |
| 309 | +- **Specific version**: Hash of content ID + revision → immutable pointer to that exact revision |
| 310 | + |
| 311 | +This design reduces the number of data keys while maintaining O(1) lookup for both patterns. |
| 312 | + |
| 313 | +### Content ID Design |
| 314 | + |
| 315 | +Content IDs are creator-chosen strings rather than hashes because: |
| 316 | + |
| 317 | +- **Human Readable**: IDs like `"premium-album-2025"` are meaningful |
| 318 | +- **Stable**: Same ID persists across revisions |
| 319 | +- **Flexible**: Creators control their namespace |
| 320 | + |
| 321 | +### Revision Count |
| 322 | + |
| 323 | +A separate revision count mapping enables: |
| 324 | + |
| 325 | +- **Enumeration**: List all versions without iterating entire array |
| 326 | +- **Validation**: Verify expected revision number before write |
| 327 | +- **Efficiency**: O(1) lookup of version count |
| 328 | + |
| 329 | +## Security Considerations |
| 330 | + |
| 331 | +### Content Immutability |
| 332 | + |
| 333 | +While the array is append-only at the application level, ERC725Y storage can technically be overwritten by the Universal Profile owner. Consumers should: |
| 334 | + |
| 335 | +- Verify content hashes match [VerifiableURI] declarations |
| 336 | +- Consider timestamps when multiple versions exist |
| 337 | +- Be aware that "latest" mapping can be updated |
| 338 | + |
| 339 | +### Decryption Parameters Security |
| 340 | + |
| 341 | +The `decryptionParams` field exists for UI/querying purposes and MUST match the hardcoded values in `decryptionCode`. Applications SHOULD: |
| 342 | + |
| 343 | +- Verify `decryptionParams` values match those embedded in `decryptionCode` when possible |
| 344 | +- Display warnings to users if discrepancies are detected |
| 345 | +- Never rely solely on `decryptionParams` for access control enforcement |
| 346 | +- Treat `decryptionCode` as the authoritative source of truth |
| 347 | + |
| 348 | +Actual access control MUST be enforced by the decryption mechanism (e.g., Lit Protocol access control conditions embedded in `decryptionCode`). Applications MUST NOT rely solely on the JSON `decryptionParams` field for security. |
| 349 | + |
| 350 | +### Method Versioning |
| 351 | + |
| 352 | +The `method` field includes a version suffix (e.g., `lit-lsp7-balance-v1`). When creating new encryption methods: |
| 353 | + |
| 354 | +- Use unique, descriptive method identifiers |
| 355 | +- Include version suffix for future compatibility (e.g., `-v1`, `-v2`) |
| 356 | +- Document required `decryptionParams` schema for each method |
| 357 | +- Maintain backward compatibility when incrementing versions |
| 358 | + |
| 359 | +### Content ID Collisions |
| 360 | + |
| 361 | +Content IDs are user-chosen strings. Applications SHOULD: |
| 362 | + |
| 363 | +- Validate uniqueness before creating new content with an ID that may already exist |
| 364 | +- Check `LSP29EncryptedAssetRevisionCount:<id>` to detect existing content |
| 365 | +- Handle collisions gracefully (e.g., append suffix or reject) |
| 366 | + |
| 367 | +### IPFS Persistence |
| 368 | + |
| 369 | +Content stored on IPFS requires pinning for persistence. Applications SHOULD: |
| 370 | + |
| 371 | +- Use pinning services for long-term storage |
| 372 | +- Provide mechanisms for creators to ensure content availability |
| 373 | +- Handle cases where IPFS content may become unavailable |
| 374 | + |
| 375 | +## Copyright |
| 376 | + |
| 377 | +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |
| 378 | + |
| 379 | +[ERC725Y]: https://github.com/ethereum/ercs/blob/master/ERCS/erc-725.md#erc725y |
| 380 | +[LSP2 Array]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#array |
| 381 | +[LSP2 Mapping]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#mapping |
| 382 | +[VerifiableURI]: https://github.com/lukso-network/LIPs/blob/main/LSPs/LSP-2-ERC725YJSONSchema.md#verifiableuri |
0 commit comments