First piece of PREMIUM_DESIGNS_FEATURE.md §6. Adds the static
client-side decryption layer for premium designs: HKDF-SHA256 over
an obfuscated per-epoch pepper + a per-design public salt, then
AES-256-GCM with the event coordinate as additional-authenticated-
data so ciphertext can't be copy-pasted to a different event.
noble bundle
- Add @noble/hashes/hkdf to the bundle entry; expose as
window.ArtstrNoble.hkdf and .hmac. Bundle size 14.0 KB → 16.5 KB.
Premium IIFE (new section in index.html)
- _softGatePepperParts — per-epoch lookup of 4x8-byte fragments
("palette seed I..IV"). For epoch 2026-05. Sub-strings don't
look like a "premium key"; reconstruction concats at use time.
Comment explicitly calls this obfuscation, not security, per
PREMIUM_DESIGNS_FEATURE.md §2 / §6.3.
- SUPPORTED_EPOCHS / CURRENT_EPOCH — adding a future epoch is
one entry in the parts table.
- pepperBytesForEpoch(epoch) — reconstructs the 32-byte pepper.
- deriveKey({ epoch, creatorPubkey, premiumAddress, schemaVersion,
publicDesignSalt, puzzleNonce }) -> 32 bytes via HKDF-SHA256
with the pepper as IKM, salt = publicDesignSalt, info =
"artstr-softgate-v1|<epoch>|<creator>|<addr>|v<schema>" (and an
optional "|p<nonce>" for the §6.7 CPU-puzzle hook).
- encodeAad(creatorPubkey, dTag) -> UTF-8 of
"30078:<creator>:<dTag>" so AES-GCM authenticates the
coordinate.
- encryptPayload(payloadObj, coord, { epoch? }) — random
32-byte salt + 12-byte IV; returns the spec §5.2 envelope
(base64 fields, softgate metadata, etc.).
- decryptPayload(envelope, coord) — reverses the above.
- Self-test runs on every load: round-trip a known payload AND
verify AAD/creator/schema mismatches all reject the decrypt
(event-coordinate binding). Single console.warn on failure.
Tested via CDP — all green:
- Round-trip with random keypair: ok
- AAD mismatch (different dTag) -> AES-GCM rejects: ok
- Creator pubkey mismatch -> KDF info changes -> rejects: ok
- Schema version mismatch -> KDF info changes -> rejects: ok
- KDF deterministic with identical inputs: ok
- Output length = 32 bytes (AES-256): ok
- Pepper reconstruction = 32 bytes: ok
- Envelope size for tiny payload = 577 bytes; ~63 KB headroom
under typical relay limits, plenty for an embedded preview
JPEG in step 2.
- Regression: NWC crypto self-test still passes; existing editor
still loads and renders layers.
Code only; nothing in the publish/feed flow uses this yet. Next
step (PREMIUM_DESIGNS_FEATURE.md §19 item 2) is the watermarked
preview pipeline.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Bundle @noble/secp256k1 v3.1.0 + @noble/hashes v2.2.0 as an IIFE
(noble-bundle.min.js, 15.8 KB) and load it before the main script.
The bundle exposes window.ArtstrNoble with schnorr, getPublicKey,
getSharedSecret, sha256, hex/randomBytes utils, plus secpUtils.
(noble v3 keeps hash functions external; the bundle wires
hashes.sha256 + hashes.hmacSha256 so schnorr.sign / verify work.)
On top of that, add an `NWC` IIFE in the main script with:
- sharedSecret(privHex, peerPubHex) — NIP-04 32-byte ECDH x-coord
(handles x-only 32-byte and full 33-byte pubkey hex).
- encrypt04 / decrypt04 — AES-256-CBC via crypto.subtle, 16-byte
random IV, `<base64ct>?iv=<base64iv>` wire format.
- getEventHash + signEventWithKey — NIP-01 event hashing + schnorr
signing using a *passed-in* hex privkey (the NWC connection's
secret, not the user's identity).
- verifyEvent — for kind-9735 receipt verification later.
- getPubkeyHex — schnorr x-only pubkey from a hex privkey.
Phase-0 self-test runs once at load: BIP-340 test vector #1 derives
the expected x-only pubkey AND verifies the published signature
through window.ArtstrNoble.schnorr; a NIP-04 round-trip encrypts
+ decrypts a known plaintext between two random keypairs; a
synthetic Nostr event is signed and verified through our helpers.
All checks pass silently; any failure logs a single console.warn
so the user sees "Crypto self-test failed — NWC features will not
work" if a bad bundle ever ships.
Verified via CDP:
- derivedPub matches BIP-340 vector 1: dff1d77f...502ba659
- sigVerify on the vector signature: true
- nip04Roundtrip: true
- eventSigned + eventVerify: true
- No console errors loading the app; existing custom-art editor
smoke test (add a color layer, render) still works.
Co-Authored-By: Claude Opus 4.7 <[email protected]>