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