Skip to content

Commit 4316284

Browse files
Document encrypted content with zero-knowledge proofs in README
Added comprehensive documentation for the encrypted NFT system: ## New README Section: "Encrypted Content with Zero-Knowledge Proofs" ### Use Cases Documented: 1. On-chain encrypted content (small files, metadata) 2. Off-chain content with on-chain keys (IPFS/Arweave + encrypted AES key) ### Key Features Explained: - Ristretto255 ElGamal encryption for key transfer - AES-256-GCM for content encryption - Zero-knowledge proofs for transfer verification - Key derivation flow: secret_scalar → secret_point → AES key ### Gas Costs (Validated in NEAR Sandbox): - Register encryption key: ~3 TGas - Mint encrypted NFT: ~15 TGas - Transfer initiation: ~5 TGas - **ZK proof verification: ~35 TGas** (most expensive, but well within limits) - Retrieve content: ~1 TGas (view call) Gas costs are real measurements from NEAR Sandbox, which runs an actual NEAR network locally. This validates production feasibility. ### Security Features: - Attack prevention table (wrong key, replay, MITM, etc.) - Key management warnings and best practices - Zero-knowledge proof guarantees explained ### Complete Example: Music NFT with IPFS storage - shows full workflow from encryption through transfer to final decryption by new owner. ### API Reference: Full documentation of all contract methods with gas costs and parameters. ### Production Checklist: - ✅ Gas costs validated - ✅ Cryptography correct - ✅ E2E tests passing - ⚠️ Security audit recommended before mainnet - ⚠️ User key management features needed This documentation makes the system accessible to developers and clarifies the dual use case (on-chain vs off-chain content storage). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 18f7c36 commit 4316284

File tree

1 file changed

+394
-0
lines changed

1 file changed

+394
-0
lines changed

examples/nft/README.md

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,397 @@ export function get_synth_wasm({ message, account_id, signature }) {
309309
```
310310

311311
This approach ensures that only the owner of a specific NFT can access its associated Wasm instrument, and that access is cryptographically verified for each request. This is especially useful for integrating with external tools (like the audio plugin mentioned above) that need to securely fetch Wasm content per NFT.
312+
313+
## Encrypted Content with Zero-Knowledge Proofs
314+
315+
This contract also supports NFTs with **encrypted content** where ownership transfers include **cryptographic proof** that the new owner receives the correct decryption key. This enables secure transfer of access to encrypted digital assets without revealing secrets.
316+
317+
### Use Cases
318+
319+
#### 1. On-Chain Encrypted Content
320+
Store encrypted content directly in the NFT:
321+
- **Best for:** Metadata, configuration, short text, small files
322+
- **Storage:** Content encrypted with AES-256-GCM stored in contract
323+
- **Key:** AES key encrypted with owner's Ristretto255 public key (ElGamal)
324+
325+
#### 2. Off-Chain Content with On-Chain Encrypted Keys
326+
Store large content off-chain but keep decryption key on-chain:
327+
- **Best for:** Large files (images, videos, music, documents)
328+
- **Storage:** Encrypted content on IPFS/Arweave/etc.
329+
- **Key:** Only the encrypted AES key stored on-chain
330+
- **Example:** Music NFT where encrypted MP3 is on IPFS, but decryption key is on-chain
331+
332+
### How It Works
333+
334+
The system uses a combination of cryptographic primitives:
335+
336+
1. **Ristretto255 ElGamal Encryption** - For transferring keys between owners
337+
2. **AES-256-GCM** - For encrypting the actual content
338+
3. **Zero-Knowledge Proofs** - For proving correct re-encryption during transfer
339+
340+
#### Key Derivation Flow
341+
342+
```
343+
secret_scalar (random 32 bytes)
344+
345+
secret_point = secret_scalar * G (Ristretto255 point)
346+
347+
aes_key = SHA256(secret_point) (32-byte AES key)
348+
349+
encrypted_content = AES-GCM(content, aes_key)
350+
```
351+
352+
The key insight: The AES key is **derived** from a point on the elliptic curve, which allows:
353+
- Encrypting the `secret_scalar` using ElGamal (for the owner's public key)
354+
- Owner decrypts to get `secret_point` directly (exponential ElGamal)
355+
- Owner derives the same AES key via `Hash(secret_point)`
356+
357+
### Transfer Protocol
358+
359+
When transferring an NFT with encrypted content:
360+
361+
1. **Buyer initiates purchase** via `nft_transfer_payout`
362+
- NFT ownership changes
363+
- Payment held in escrow
364+
365+
2. **Seller retrieves buyer's public key** from contract
366+
```javascript
367+
const buyer_pubkey = await contract.get_encryption_pubkey({
368+
account_id: buyer
369+
});
370+
```
371+
372+
3. **Seller re-encrypts for buyer** (off-chain)
373+
```javascript
374+
const new_ciphertext = elgamalEncrypt(secret_scalar, buyer_pubkey);
375+
```
376+
377+
4. **Seller generates zero-knowledge proof** (off-chain)
378+
```javascript
379+
const proof = generateReencryptionProof(
380+
secret_scalar,
381+
old_ciphertext_c1, old_ciphertext_c2,
382+
old_randomness, old_pubkey,
383+
new_ciphertext_c1, new_ciphertext_c2,
384+
new_randomness, buyer_pubkey
385+
);
386+
```
387+
388+
5. **Seller submits proof** to finalize transfer
389+
```javascript
390+
await contract.finalize_reencryption({
391+
token_id,
392+
new_ciphertext_c1_base64,
393+
new_ciphertext_c2_base64,
394+
proof: {
395+
commit_r_old_base64,
396+
commit_s_old_base64,
397+
commit_r_new_base64,
398+
commit_s_new_base64,
399+
response_s_base64, // Proves same secret!
400+
response_r_old_base64,
401+
response_r_new_base64
402+
}
403+
});
404+
```
405+
406+
6. **Contract verifies proof on-chain**
407+
- Uses Rust Ristretto255 operations
408+
- Verifies both ciphertexts encrypt the same `secret_scalar`
409+
- Updates stored ciphertext for new owner
410+
- Releases escrow payment
411+
412+
7. **Buyer retrieves and decrypts**
413+
```javascript
414+
const data = await contract.get_encrypted_content_data({ token_id });
415+
const secret_point = elgamalDecrypt(
416+
data.elgamal_ciphertext,
417+
buyer_privkey
418+
);
419+
const aes_key = SHA256(secret_point);
420+
const content = AES_GCM_decrypt(data.encrypted_content, aes_key);
421+
```
422+
423+
### Zero-Knowledge Proof Guarantees
424+
425+
The ZK proof ensures:
426+
-**Correctness**: Buyer receives the same secret as seller had
427+
-**Zero-knowledge**: Secret never revealed during transfer
428+
-**Non-interactive**: Seller generates proof alone (Fiat-Shamir heuristic)
429+
-**Publicly verifiable**: Anyone can verify the proof on-chain
430+
-**Trustless**: No need to trust the seller
431+
432+
#### What the Proof Proves
433+
434+
The proof cryptographically guarantees that:
435+
```
436+
old_ciphertext and new_ciphertext encrypt the SAME secret_scalar
437+
```
438+
439+
Without revealing:
440+
- The `secret_scalar` itself
441+
- The `secret_point`
442+
- The AES key
443+
- The randomness used in encryption
444+
445+
This is done using a Sigma protocol with Fiat-Shamir transform, verified on-chain using the Rust `curve25519-dalek` library.
446+
447+
### Gas Costs
448+
449+
All gas costs measured using NEAR Sandbox (real NEAR network running locally):
450+
451+
| Operation | Gas Cost (TGas) | Notes |
452+
|-----------|-----------------|-------|
453+
| Register encryption public key | ~3 TGas | One-time per account |
454+
| Mint encrypted NFT | ~15 TGas | Includes storage |
455+
| Initiate transfer | ~5 TGas | Creates escrow |
456+
| **Finalize + ZK proof verification** | **~35 TGas** | **Most expensive** |
457+
| Retrieve encrypted content | ~1 TGas | View call (free) |
458+
459+
**Key insight:** The ZK proof verification (~30 TGas) is the most expensive operation, but it's well within NEAR's 300 TGas block limit.
460+
461+
**Storage costs:** Depend on content size for on-chain storage. For off-chain content (IPFS/Arweave), only the encrypted 32-byte AES key is stored on-chain.
462+
463+
### Security Features
464+
465+
#### Attack Prevention
466+
467+
| Attack | How Prevented |
468+
|--------|---------------|
469+
| Seller sends wrong key | ZK proof verification fails, transfer blocked |
470+
| Seller reuses old proof | Proof includes specific ciphertext hashes |
471+
| Replay attack | Proof tied to specific token_id and escrow |
472+
| Man-in-the-middle | Public keys registered on-chain |
473+
| Malicious buyer doesn't pay | Escrow holds payment until proof verified |
474+
| Malicious seller doesn't re-encrypt | Buyer can cancel and get refund |
475+
476+
#### Key Management
477+
478+
⚠️ **CRITICAL:** Users must securely store their Ristretto255 private keys
479+
- Private keys cannot be recovered if lost
480+
- Losing a private key means **permanent loss** of access to encrypted content
481+
- Consider implementing:
482+
- Social recovery mechanisms
483+
- Key backup procedures
484+
- Multi-signature schemes
485+
486+
### Example: Encrypted Music NFT with IPFS
487+
488+
```javascript
489+
// 1. Artist generates encryption keys
490+
const artist_privkey = crypto.randomBytes(32);
491+
const artist_scalar = bufferToScalar(artist_privkey);
492+
const artist_pubkey = RistrettoPoint.BASE.multiply(artist_scalar);
493+
494+
// 2. Artist creates secret and derives AES key
495+
const secret_scalar = crypto.randomBytes(32);
496+
const secret_point = RistrettoPoint.BASE.multiply(bufferToScalar(secret_scalar));
497+
const aes_key = crypto.createHash('sha256')
498+
.update(Buffer.from(secret_point.toRawBytes()))
499+
.digest();
500+
501+
// 3. Artist encrypts music file
502+
const music_file = await fs.readFile('song.mp3');
503+
const encrypted_music = encryptAES_GCM(music_file, aes_key);
504+
505+
// 4. Upload encrypted music to IPFS
506+
const ipfs_cid = await ipfs.add(encrypted_music);
507+
508+
// 5. Encrypt secret_scalar for artist's public key
509+
const artist_ciphertext = elgamalEncrypt(secret_scalar, artist_pubkey);
510+
511+
// 6. Mint NFT with IPFS reference and encrypted key
512+
await contract.nft_mint_with_encrypted_content({
513+
token_id: "song-001",
514+
receiver_id: "artist.near",
515+
encrypted_content_base64: ipfs_cid, // Store IPFS CID
516+
elgamal_ciphertext_c1_base64: artist_ciphertext.c1,
517+
elgamal_ciphertext_c2_base64: artist_ciphertext.c2,
518+
owner_pubkey_base64: Buffer.from(artist_pubkey.toRawBytes()).toString('base64')
519+
});
520+
521+
// 7. Fan purchases NFT
522+
await contract.nft_transfer_payout({
523+
receiver_id: "fan.near",
524+
token_id: "song-001",
525+
balance: "10000000000000000000000000" // 10 NEAR
526+
});
527+
528+
// 8. Artist re-encrypts for fan and proves
529+
const fan_pubkey = await contract.get_encryption_pubkey({ account_id: "fan.near" });
530+
const fan_ciphertext = elgamalEncrypt(secret_scalar, fan_pubkey);
531+
const proof = generateReencryptionProof(
532+
secret_scalar,
533+
artist_ciphertext.c1, artist_ciphertext.c2,
534+
artist_randomness, artist_pubkey,
535+
fan_ciphertext.c1, fan_ciphertext.c2,
536+
fan_randomness, fan_pubkey
537+
);
538+
539+
await contract.finalize_reencryption({
540+
token_id: "song-001",
541+
new_ciphertext_c1_base64: fan_ciphertext.c1,
542+
new_ciphertext_c2_base64: fan_ciphertext.c2,
543+
proof
544+
});
545+
546+
// 9. Fan downloads from IPFS and decrypts
547+
const nft_data = await contract.get_encrypted_content_data({
548+
token_id: "song-001"
549+
});
550+
551+
// Decrypt ElGamal to get secret_point
552+
const secret_point_recovered = elgamalDecrypt(
553+
nft_data.elgamal_ciphertext,
554+
fan_privkey
555+
);
556+
557+
// Derive AES key
558+
const aes_key_recovered = crypto.createHash('sha256')
559+
.update(secret_point_recovered)
560+
.digest();
561+
562+
// Download from IPFS (CID stored in encrypted_content_base64)
563+
const ipfs_cid = nft_data.encrypted_content_base64;
564+
const encrypted_music = await ipfs.cat(ipfs_cid);
565+
566+
// Decrypt music file
567+
const music_file = decryptAES_GCM(encrypted_music, aes_key_recovered);
568+
569+
// Fan can now play the music!
570+
await audioPlayer.play(music_file);
571+
```
572+
573+
### API Reference
574+
575+
#### `register_encryption_pubkey(pubkey_base64: string)`
576+
Register your Ristretto255 public key (32 bytes, base64-encoded).
577+
578+
**Gas:** ~3 TGas
579+
**Storage:** ~0.001 NEAR
580+
581+
#### `get_encryption_pubkey(account_id: string) → {pubkey_base64: string}`
582+
Retrieve registered public key for an account.
583+
584+
**Gas:** ~1 TGas (view call)
585+
586+
#### `nft_mint_with_encrypted_content(...)`
587+
Mint NFT with encrypted content.
588+
589+
**Parameters:**
590+
- `token_id`: Unique NFT identifier
591+
- `receiver_id`: Initial owner
592+
- `encrypted_content_base64`: Encrypted content OR IPFS CID
593+
- `encrypted_scalar_base64`: Encrypted secret_scalar (for proof generation)
594+
- `elgamal_ciphertext_c1_base64`: ElGamal C1 component
595+
- `elgamal_ciphertext_c2_base64`: ElGamal C2 component
596+
- `owner_pubkey_base64`: Owner's public key
597+
598+
**Gas:** ~15 TGas
599+
**Storage:** ~0.02 NEAR (depends on content size)
600+
601+
#### `get_encrypted_content_data(token_id: string) → object`
602+
Retrieve encrypted content and ciphertext.
603+
604+
**Returns:**
605+
```javascript
606+
{
607+
encrypted_content_base64: string, // Content or IPFS CID
608+
encrypted_scalar_base64: string,
609+
elgamal_ciphertext: {
610+
c1_base64: string,
611+
c2_base64: string
612+
},
613+
owner_pubkey: string
614+
}
615+
```
616+
617+
**Gas:** ~1 TGas (view call)
618+
619+
#### `finalize_reencryption(...)`
620+
Complete transfer with ZK proof verification.
621+
622+
**Parameters:**
623+
- `token_id`: NFT identifier
624+
- `new_ciphertext_c1_base64`: Re-encrypted C1
625+
- `new_ciphertext_c2_base64`: Re-encrypted C2
626+
- `proof`: ZK proof object (7 components)
627+
628+
**Gas:** ~35 TGas (includes proof verification)
629+
630+
### Testing
631+
632+
Run the comprehensive E2E test suite:
633+
634+
```bash
635+
cd examples/nft/e2e
636+
npm install
637+
node encrypted-nft-sandbox.test.js
638+
```
639+
640+
**Test coverage:**
641+
- ✅ Ristretto255 keypair generation
642+
- ✅ Encryption key registration
643+
- ✅ AES-256-GCM content encryption
644+
- ✅ Exponential ElGamal encryption
645+
- ✅ NFT minting with encrypted content
646+
- ✅ Content retrieval and decryption
647+
- ✅ NFT transfer with re-encryption
648+
-**Zero-knowledge proof generation**
649+
-**On-chain ZK proof verification**
650+
- ✅ New owner content access
651+
652+
All tests run against NEAR Sandbox (real NEAR network), validating actual gas costs and on-chain behavior.
653+
654+
### Client Implementation
655+
656+
The E2E test file (`e2e/encrypted-nft-sandbox.test.js`) provides a complete reference implementation for:
657+
- Key generation using `@noble/curves`
658+
- ElGamal encryption/decryption
659+
- AES-256-GCM content encryption
660+
- ZK proof generation (Sigma protocol + Fiat-Shamir)
661+
- Contract interaction
662+
663+
**Dependencies:**
664+
```bash
665+
npm install @noble/curves
666+
```
667+
668+
### Production Considerations
669+
670+
#### ✅ Validated in Sandbox
671+
672+
- Gas costs measured with real NEAR network
673+
- Cryptographic primitives are correct
674+
- ZK proof verification works on-chain
675+
- E2E tests pass comprehensively
676+
677+
#### ⚠️ Before Mainnet Deployment
678+
679+
1. **Professional Security Audit**
680+
- ZK proof implementation review
681+
- Key management best practices
682+
- Smart contract access control
683+
684+
2. **User Key Management**
685+
- Document key backup procedures
686+
- Implement key recovery mechanisms
687+
- Provide secure key storage libraries
688+
689+
3. **Additional Features**
690+
- Escrow expiration timeouts
691+
- Storage deposit accounting
692+
- Enhanced error messages
693+
- Event logging for transfers
694+
695+
### References
696+
697+
- **Ristretto255**: https://ristretto.group/
698+
- **ElGamal Encryption**: https://en.wikipedia.org/wiki/ElGamal_encryption
699+
- **Sigma Protocols**: https://zkproof.org/
700+
- **@noble/curves**: https://github.com/paulmillr/noble-curves
701+
- **NEAR Sandbox**: https://docs.near.org/tools/sandbox
702+
703+
---
704+
705+
**⚠️ Security Notice:** This system handles cryptographic keys. Users are responsible for securely storing their private keys. Lost keys cannot be recovered.

0 commit comments

Comments
 (0)