Skip to the content.

Alt Vault Protocol (AVP) — Specification

Version: 0.2 (draft) Status: stable wire contract; see §12 for open items.

AVP lets independent Minecraft clients share alt accounts through a zero-knowledge server. This document is the normative contract. An implementation is conformant if it satisfies every MUST here and passes the vectors in vectors/.

1. Overview

A repository (“repo”) is a shared, end-to-end-encrypted collection of alt accounts with a set of members. A member is an Ed25519 keypair. The repo has a single symmetric data key that encrypts the alt payload; the data key is wrapped to each member’s X25519 public key. A server stores the encrypted payload, the per-member wrapped keys, the members’ public keys, and version/epoch counters — and nothing it can decrypt. All cryptography happens on the client.

Repositories are federated: each is addressed avp://host/repoId, and a member’s keypair authenticates against any conformant server, so a member can join and sync a repository hosted anywhere.

2. Conventions

3. Identity and authentication

Members are identified by an Ed25519 keypair, not an account. Authentication is a challenge→token flow that yields a bearer token scoping the caller to the repositories it is a member of. The token is minted by an identity provider (IdP) and verified by the vault server.

  1. challenge { ed25519PublicKey } → { nonce } — the server returns a single-use random nonce (at least 32 bytes, base64) with a short TTL (RECOMMENDED ≤ 2 minutes).
  2. token { ed25519PublicKey, nonce, signature } → { token, expiresAt } — the client signs the raw nonce bytes (the bytes obtained by base64-decoding nonce) with its Ed25519 private key. The IdP MUST verify the signature against ed25519PublicKey, MUST reject a reused or expired nonce, and then mints a token whose subject is the member id.

The reference token is a JWT with claims { "sub": "key:" + ed25519PublicKey, "kind": "keypair" } and no account/email/role claims, verifiable via the IdP’s published key set (e.g. JWKS). A vault server authorizes each operation by matching the token subject (with the key: prefix stripped) against repo membership; it MUST NOT require any account lookup.

A token is server-local: it is minted by, and valid only at, the server that issued it. A client MUST cache tokens keyed by host and MUST NOT present a token issued by one host to another (see §8).

4. Cryptographic envelope

Field names are part of the contract. All values are base64 strings unless typed otherwise.

The data key is per-repo and symmetric. The payload AEAD is AES-256-GCM (12-byte IV, 128-bit tag) or an equivalent AEAD named by schemeId. The additional authenticated data (AAD) bound into every payload ciphertext is the tuple (repoId, payloadVersion, keyEpoch). Because the AAD is part of interoperable ciphertext, its byte layout is fixed:

AAD = UTF8(repoId) || 0x1F || int64BE(payloadVersion) || int64BE(keyEpoch)

that is, the UTF-8 bytes of repoId, a single 0x1F separator byte, then the big-endian 8-byte two’s-complement encodings of payloadVersion and keyEpoch. A conformant encoder MUST bind all three this way, so that an envelope replayed under a different repo, version, or epoch fails authentication. See vectors/aad.json.

Default wrap scheme — X25519-HKDF-SHA256-AESGCM-v1

This is the default schemeId. Keys are raw encodings: X25519 keys are the raw 32-byte little-endian form (RFC 7748), and the 32-byte data key is the AES-256 key. To wrap a data key to a recipient whose X25519 public key is recipientPub:

  1. Generate a fresh ephemeral X25519 key pair (ephemeralPriv, ephemeralPub).
  2. sharedSecret = X25519(ephemeralPriv, recipientPub) — the raw 32-byte ECDH output (not hashed).
  3. KEK = HKDF-SHA256(ikm = sharedSecret, salt = ephemeralPubRaw, info = UTF8("avp/rdk-wrap/v1"), L = 32) where ephemeralPubRaw is the raw 32-byte ephemeral public key and KEK is a 32-byte key. (HKDF is RFC 5869; the extract step uses 32 zero bytes when the salt is empty, but here the salt is never empty.)
  4. Pick a fresh 12-byte iv. ciphertext = AES-256-GCM(key = KEK, iv = iv, aad = UTF8("avp/rdk-wrap/v1"), plaintext = dataKey), with the 128-bit tag appended to the ciphertext.
  5. WrappedKey = { schemeId, ephemeralPublicKey: base64(ephemeralPubRaw), iv: base64(iv), ciphertext: base64(ciphertext) }.

To unwrap, recompute sharedSecret = X25519(recipientPriv, ephemeralPub) and the same KEK, then AES-256-GCM-decrypt with the same aad. The info string (avp/rdk-wrap/v1) is bound as both the HKDF info and the GCM AAD; it is the vendor-neutral scheme label, identical for every implementation.

The conformance vectors in vectors/ pin each primitive and this composition byte-for-byte.

5. Payload and provenance

The plaintext inside EncryptedEnvelope.ciphertext is a JSON object:

{ "alts": [ <AltAccount>, ... ], "payloadVersion": <int64> }

payloadVersion here is a redundant stamp; the authoritative version is the envelope header (bound into the AAD). An AltAccount is:

{
  "uuid": "<player uuid>",
  "username": "<last known name>",
  "accessToken": "<credential>",
  "type": "MICROSOFT | COOKIE | SESSION | OFFLINE",
  "lastUsed": <int64>,
  "lastUsedBy": "<member id or null>",
  "ban": { "banned": <bool>, "observedAt": <int64>, "source": "...", "detail": "...", "observedBy": "<member id or null>" } | null,
  "sourceClient": "<client name or null>",
  "sourceUser": "<user within that client or null>"
}

Provenance. sourceClient / sourceUser identify which client an alt was added from and the user within that client (for example a client id and a user handle). They are plain, opaque, implementer-defined strings; this specification defines only the field names, never their values. They let a cross-client repository attribute each alt. Because they live inside the encrypted payload, the server never sees them — cross-client attribution does not weaken the zero-knowledge guarantee. Implementations SHOULD set them when adding an alt and MUST tolerate their absence (older payloads, or clients that do not attribute).

lastUsedBy / ban.observedBy are member ids (base64 Ed25519 keys) or null; they let members coordinate (who used an alt last, who observed it banned) so a teammate is not handed a banned account.

6. Transport surface

The vault operations below carry a bearer token (§3). Authentication (challenge/token) is typically HTTP/JSON to the IdP but MAY be offered over gRPC; the data operations are the vault service.

Operation Request → Response Authorization
createRepo CreateRepoRequest { manifest, initialEnvelope }VaultManifest manifest.members MUST contain exactly one member whose key equals the caller
pull PullRequest { repoId, knownPayloadVersion }PullResponse { manifest, envelope?, unchanged } caller is a member
push PushRequest { repoId, envelope, expectedPayloadVersion, rotatedMembers? }PushResponse { accepted, payloadVersion, keyEpoch, conflict } caller is a member
addMember MemberAddRequest { repoId, member }VaultManifest caller is a member (v1 policy: any member may invite)
removeMember MemberRemoveRequest { repoId, removedMemberId, rotatedEnvelope, rewrappedMembers, newKeyEpoch }VaultManifest caller is a member
fetchMemberKey { repoId, memberId }MemberEntry caller is a member

Semantics:

Profiles

7. (reserved)

8. Federation

AVP federates by portable identity + addressing, not server-to-server replication.

8.1 Join handshake

Because adding a member is member-initiated (an existing member wraps the data key to the joiner’s X25519 key), the joiner publishes its keys first. Two base64url-encoded JSON tokens:

  1. Invite request (joiner → inviter): { "v": 1, "ed25519PublicKey", "x25519PublicKey" } — the joiner’s public keys. The inviter calls addMember with them.
  2. Repo locator (inviter → joiner): { "v": 1, "host", "repoId", "schemeId", "keyEpoch", "issuerJwksUrl"? } — where the repository lives, plus the IdP whose key bindings to trust (§9). schemeId/keyEpoch are hints; the authoritative values come from the pulled manifest. issuerJwksUrl MAY be absent when the deployment publishes no key binding.

Tokens are base64url (RFC 4648 §5, no padding) over the compact JSON above. They carry only public data and are safe to relay over any channel.

8.2 Discovery (optional)

A server MAY expose an unauthenticated GET /.well-known/avp returning at least { "profiles": ["grpc"|"http-json", ...], "issuerJwksUrl": "..." } so that a client resolving an avp:// address knows which transport profile(s) to use and which IdP to trust. Absent discovery, a client SHOULD default to the HTTP/JSON profile over HTTPS and to the issuer named out of band (e.g. in a repo locator).

8.3 Server-to-server

Reaching a repository through a server that does not host it (replication/relay) is out of scope for this version and is a possible future extension.

9. Anti-MITM key binding

A hostile or compromised server could serve a wrong X25519 public key for a member id on fetchMemberKey, tricking another member into wrapping the data key to an attacker. To defend against this when joining a repository on a server one does not operate:

This is additive and zero-knowledge-safe: a public-key signature, never a secret.

10. Invariants

A conformant server MUST uphold:

11. Conformance

An implementation is conformant if it satisfies every MUST above and reproduces the vectors in vectors/ (canonical AAD/binding-message construction, challenge/sign/token, key wrap/unwrap, and rotation). See vectors/README.md.

12. Security considerations and open items