The .ikm protected model

An .ikm file is a runtime model for Ikimotion’s own player, encrypted and licensed per buyer. It uses the container described in The Ikimotion file container (magic \x89IKM\r\n\x1a\n). How to use it in the app: Protected models.

Contents

The files of the runtime package the viewer plays: the .moc3, the .model3.json and every file it references (physics, motions, expressions, pose, display info, user data, sounds) as deflate entries under their package paths, and the textures as rgba8 entries (straight-alpha pixels, type image/x-rgba8, meta.source: "image/png") under their PNG paths. The player resolves the package exactly as it resolves an unprotected one; there is no second engine.

Manifest meta:

{ "format": "ikimotion-model", "version": 1, "name": "Ibuki", "role": "license",
  "license": { "id": "0123456789abcdef", "masterId": "…", "licensee": "<account id>",
               "licenseeName": "…", "seller": "<account id>", "issuedAt": 1760000000000 } }

Masters and license copies

  1. Master (written by the seller’s editor, Export › Ikimotion protected model (.ikm)…): a random content key encrypts everything; one recipient key slot seals the key to the license service’s X25519 key (GET /api/keys/service); the seller’s key signs the file. role is master.
  2. Upload (POST /api/licenses/masters): the service stores the file in R2 and checks it completely while streaming it back (every chunk decrypted and verified, the seller’s signature valid), then records its SHA-256.
  3. License (POST /api/licenses/masters/<id>/licenses, by account id or by the buyer’s verified email): a random 64-bit license id.
  4. License copy (GET /api/licenses/<id>/model, the licensee only): streamed from the master chunk by chunk, never stored. Each chunk is decrypted, every texture row band gets the license’s watermark, and the chunk is re-encrypted with the license key HKDF-SHA256(service root, salt = master SHA-256, "license-key-v1" || master id || license id). The copy’s only key slot is a license slot (service, license id, master id; no key material), its manifest carries the license, and the license service signs it.
  5. Key release (POST /api/licenses/<id>/key { publicKey }): the player makes an ephemeral X25519 key pair inside WebAssembly and sends the public half with the buyer’s access token. The service checks that the license exists, belongs to the caller and is not revoked, applies rate limits (20 per license and 120 per user per hour), counts the release, and answers the license key sealed to that ephemeral key (X25519 + HKDF-SHA256 + XChaCha20-Poly1305, context "ikm-license-key-v1\0" || license id). The player opens the copy in memory; the key never leaves the WebAssembly module.
  6. Revocation (POST /api/licenses/<id>/revoke, the seller): no further copies or keys.

Watermark

The license id is in the manifest (removable by anyone who decrypts the file) and, invisibly, in the pixels of every texture:

  • Payload: the 64-bit license id and a CRC-16 (80 bits).
  • A per-master secret key (derived from the service’s root secret) drives a ChaCha8 keystream per pixel row. Each visible pixel (alpha of 8 or more) is assigned one payload bit and a +1/-1 chip; its R, G and B move by 2 levels (of 255) in the direction chip x bit.
  • Reading needs no original: a Laplacian high-pass removes flat colour and gradients, clipped residuals are correlated with each bit’s chips, and the CRC rejects noise. The seller reads it with Identify a leaked texture… (POST /api/licenses/masters/<id>/identify).

Limits, plainly:

  • It survives lossless re-encoding exactly (PNG, lossless WebP), uniform brightness or contrast changes, mild noise and high-quality JPEG.
  • It does not survive resizing, rotation, cropping or any shift (the pattern is tied to pixel positions), heavy compression, repainting, or averaging differently licensed copies.
  • Very small or mostly transparent textures carry fewer pixels per bit and may not read back confidently.

Threat model

Client-side protection raises the cost of ripping a model but cannot make it impossible: to draw the model, the player must hold the decrypted moc3, motions and texture pixels in memory, and a determined person with a debugger can copy them. The design therefore aims at:

  • No casual copying: the file on disk is useless without the buyer’s signed-in account; the player never writes decrypted bytes to disk, IndexedDB or caches.
  • Per-buyer accountability: every copy differs, and a leaked texture names the license it came from. The watermark, not the encryption, is the deterrent.
  • Control: the seller revokes a license; rate limits and release counts show abuse.

Not covered: a buyer extracting and redistributing their own decrypted copy (the watermark names them), screen capture, or a compromised license service (it holds the root secret that derives every license and watermark key).