One call to the public enrollment endpoint mints an account shell and returns the one-time claim. No approval step, no body needed.
var base = "https://secrets.lab.stratus5.net"
res, _ := http.Post(base+"/v1/enroll", "application/json", strings.NewReader("{}"))
// { "account_id": "acct_...", "claim": "acct_...:tok_..." } the claim is one-time
Client-side crypto. Generate a 256-bit Secret Key and keep it as your Emergency Kit. Derive the Account Unlock Key as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Generate an X25519 account keypair, wrap the private half under the AUK, generate a first vault key and wrap it to the account public key. See the 2SKD section. Then post the public and wrapped material to init.
// after deriving verifier/pubkey/enc_privkey/wrapped_vk client-side:
body := `{ "claim":"acct_...:tok_...", "account_id":"acct_...",
"account_pubkey":"<b64>", "enc_account_privkey":"<b64>",
"kdf_salt":"<b64>", "auth_verifier_salt":"<b64>", "auth_verifier":"<b64>",
"first_vault": { "vault_id":"vault_...", "wrapped_vk":"<b64>" } }`
http.Post(base+"/v1/accounts/init", "application/json", strings.NewReader(body))
// → { "account_id":"acct_..." } the account is now active
To unlock a session, run the SRP-6a exchange: POST /v1/auth/srp/start with your account id and ephemeral a, derive the shared key from the returned salt, b and handle, then POST /v1/auth/srp/finish with the handle and m1. The passphrase and Secret Key never leave the client. The response carries a token you send as Authorization: Bearer <token> on later calls.
Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt the value with XChaCha20-Poly1305 under the vault key, with the AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.
req, _ := http.NewRequest("PUT", base+"/v1/vaults/"+vaultID+"/secrets/"+keyRef,
strings.NewReader(`{"ciphertext":"<b64>","nonce":"<b64>","alg":"xchacha20poly1305","protection":"aukwrap"}`))
req.Header.Set("content-type", "application/json")
req.Header.Set("authorization", "Bearer "+token)
http.DefaultClient.Do(req)
GET the same ref, then decrypt the returned blob client-side. A tampered ciphertext or wrong key fails the Poly1305 tag and decryption fails closed.
req, _ := http.NewRequest("GET", base+"/v1/vaults/"+vaultID+"/secrets/"+keyRef, nil)
req.Header.Set("authorization", "Bearer "+token)
got, _ := http.DefaultClient.Do(req)
// { "ciphertext":"<b64>", "nonce":"<b64>", "alg":"xchacha20poly1305", "protection":"aukwrap", "enc_key_name":"<b64>" }
// then: plaintext = xchacha20poly1305.Open(VK, nonce, ciphertext, aad) // client-side
Client-side crypto. Generate a machine key, derive auth_secret = HKDF(MK,"auth") and unwrap_key = HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and compute Argon2id(auth_secret) as the verifier. The machine key is the one secret you give the consumer. See the machine credentials section.
req, _ := http.NewRequest("POST", base+"/v1/vaults/"+vaultID+"/credentials/machine",
strings.NewReader(`{"credential_id":"mach_...","label":"ci-prod","auth_verifier_salt":"<b64>","auth_verifier":"<b64>","wrapped_vk":"<b64>"}`))
req.Header.Set("content-type", "application/json")
req.Header.Set("authorization", "Bearer "+token)
http.DefaultClient.Do(req)
// the consumer then POSTs { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session
One call to the public enrollment endpoint mints an account shell and returns the one-time claim. No approval step, no body needed.
var http = HttpClient.newHttpClient();
var base = "https://secrets.lab.stratus5.net";
var enroll = HttpRequest.newBuilder(URI.create(base + "/v1/enroll"))
.header("content-type", "application/json")
.POST(BodyPublishers.ofString("{}"))
.build();
var res = http.send(enroll, BodyHandlers.ofString());
// { "account_id": "acct_...", "claim": "acct_...:tok_..." } the claim is one-time
Client-side crypto. Generate a 256-bit Secret Key and keep it as your Emergency Kit. Derive the Account Unlock Key as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Generate an X25519 account keypair, wrap the private half under the AUK, generate a first vault key and wrap it to the account public key. See the 2SKD section. Then post the public and wrapped material to init.
// after deriving verifier/pubkey/enc_privkey/wrapped_vk client-side:
var body = """
{ "claim":"acct_...:tok_...", "account_id":"acct_...",
"account_pubkey":"<b64>", "enc_account_privkey":"<b64>",
"kdf_salt":"<b64>", "auth_verifier_salt":"<b64>", "auth_verifier":"<b64>",
"first_vault": { "vault_id":"vault_...", "wrapped_vk":"<b64>" } }""";
http.send(HttpRequest.newBuilder(URI.create(base + "/v1/accounts/init"))
.header("content-type", "application/json")
.POST(BodyPublishers.ofString(body)).build(), BodyHandlers.ofString());
// → { "account_id":"acct_..." } the account is now active
To unlock a session, run the SRP-6a exchange: POST /v1/auth/srp/start with your account id and ephemeral a, derive the shared key from the returned salt, b and handle, then POST /v1/auth/srp/finish with the handle and m1. The passphrase and Secret Key never leave the client. The response carries a token you send as Authorization: Bearer <token> on later calls.
Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt the value with XChaCha20-Poly1305 under the vault key, with the AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.
http.send(HttpRequest.newBuilder(URI.create(base + "/v1/vaults/" + vaultId + "/secrets/" + keyRef))
.header("content-type", "application/json")
.header("authorization", "Bearer " + token)
.PUT(BodyPublishers.ofString(
"{\"ciphertext\":\"<b64>\",\"nonce\":\"<b64>\",\"alg\":\"xchacha20poly1305\",\"protection\":\"aukwrap\"}"))
.build(), BodyHandlers.ofString());
GET the same ref, then decrypt the returned blob client-side. A tampered ciphertext or wrong key fails the Poly1305 tag and decryption fails closed.
var got = http.send(HttpRequest.newBuilder(URI.create(base + "/v1/vaults/" + vaultId + "/secrets/" + keyRef))
.header("authorization", "Bearer " + token)
.GET().build(), BodyHandlers.ofString());
// { "ciphertext":"<b64>", "nonce":"<b64>", "alg":"xchacha20poly1305", "protection":"aukwrap", "enc_key_name":"<b64>" }
// then: plaintext = XChaCha20-Poly1305.open(VK, nonce, ciphertext, aad) // client-side
Client-side crypto. Generate a machine key, derive auth_secret = HKDF(MK,"auth") and unwrap_key = HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and compute Argon2id(auth_secret) as the verifier. The machine key is the one secret you give the consumer. See the machine credentials section.
http.send(HttpRequest.newBuilder(URI.create(base + "/v1/vaults/" + vaultId + "/credentials/machine"))
.header("content-type", "application/json")
.header("authorization", "Bearer " + token)
.POST(BodyPublishers.ofString(
"{\"credential_id\":\"mach_...\",\"label\":\"ci-prod\",\"auth_verifier_salt\":\"<b64>\",\"auth_verifier\":\"<b64>\",\"wrapped_vk\":\"<b64>\"}"))
.build(), BodyHandlers.ofString());
// the consumer then POSTs { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session
One call to the public enrollment endpoint mints the account and returns the one-time claim. No approval, no body.
const base = "https://secrets.lab.stratus5.net";
const r = await fetch(`${base}/v1/enroll`, {
method: "POST",
headers: { "content-type": "application/json" },
body: "{}",
});
const { account_id, claim } = await r.json(); // claim is one-time, consumed by init
Client-side crypto. Generate a 256-bit Secret Key as your Emergency Kit. Derive the AUK as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Build the X25519 account keypair, wrap its private half under the AUK, make a first vault key wrapped to the account public key. See the 2SKD section.
await fetch(`${base}/v1/accounts/init`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
claim, account_id,
account_pubkey: pubB64, enc_account_privkey: encPrivB64,
kdf_salt: kB64, auth_verifier_salt: avsB64, auth_verifier: avB64,
first_vault: { vault_id: vaultId, wrapped_vk: wvkB64 },
}),
});
// → { account_id } the account is now active
For a session, run the SRP-6a exchange: POST /v1/auth/srp/start with a, derive the shared key from the returned salt, b and handle, then POST /v1/auth/srp/finish with the handle and m1. The response carries a token you send as Authorization: Bearer <token>. Neither secret leaves the process.
Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt with XChaCha20-Poly1305 under the vault key, AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.
await fetch(`${base}/v1/vaults/${vaultId}/secrets/${keyRef}`, {
method: "PUT",
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
body: JSON.stringify({ ciphertext: ctB64, nonce: nonceB64, alg: "xchacha20poly1305", protection: "aukwrap" }),
});
GET the ref, then decrypt client-side. Altered ciphertext fails the tag and decryption fails closed.
const g = await fetch(`${base}/v1/vaults/${vaultId}/secrets/${keyRef}`, {
headers: { authorization: `Bearer ${token}` },
});
const blob = await g.json(); // { ciphertext, nonce, alg, protection, enc_key_name }
// plaintext = xchacha20poly1305_open(VK, blob.nonce, blob.ciphertext, aad) // client-side
Client-side crypto. Generate the machine key, derive HKDF(MK,"auth") and HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and store Argon2id(auth_secret) as the verifier. The machine key is the only secret the consumer needs. See the machine credentials section.
await fetch(`${base}/v1/vaults/${vaultId}/credentials/machine`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
body: JSON.stringify({
credential_id: "mach_...", label: "ci-prod",
auth_verifier_salt: mavsB64, auth_verifier: mavB64, wrapped_vk: mwvkB64,
}),
});
// consumer posts { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session
One call mints the account and returns the one-time claim. No approval, no body.
var http = new HttpClient { BaseAddress = new Uri("https://secrets.lab.stratus5.net") };
var res = await http.PostAsync("/v1/enroll",
new StringContent("{}", Encoding.UTF8, "application/json"));
// { "account_id": "acct_...", "claim": "acct_...:tok_..." } claim is one-time
Client-side crypto. Generate a 256-bit Secret Key as your Emergency Kit. Derive the AUK as Argon2id(passphrase, salt) XOR HKDF-SHA256(SK, account_id, "auk"). Build the X25519 account keypair, wrap the private half under the AUK, make a first vault key wrapped to the account public key. See the 2SKD section.
var body = """
{ "claim":"acct_...:tok_...", "account_id":"acct_...",
"account_pubkey":"<b64>", "enc_account_privkey":"<b64>",
"kdf_salt":"<b64>", "auth_verifier_salt":"<b64>", "auth_verifier":"<b64>",
"first_vault": { "vault_id":"vault_...", "wrapped_vk":"<b64>" } }""";
await http.PostAsync("/v1/accounts/init",
new StringContent(body, Encoding.UTF8, "application/json"));
// → { "account_id":"acct_..." } the account is now active
For a session, run the SRP-6a exchange against /v1/auth/srp/start and /v1/auth/srp/finish: send your account id and a, derive the shared key from the returned salt, b and handle, then send the handle and m1. The response carries a token you send as Authorization: Bearer <token>. The passphrase and Secret Key never leave the client.
Client-side crypto. Compute key_ref = HMAC-SHA256(index_key, normalize(name)) and encrypt with XChaCha20-Poly1305 under the vault key, AAD bound to account_id ∥ vault_id ∥ key_ref. Then PUT the ciphertext.
http.DefaultRequestHeaders.Add("authorization", $"Bearer {token}");
await http.PutAsync($"/v1/vaults/{vaultId}/secrets/{keyRef}",
new StringContent(
"{\"ciphertext\":\"<b64>\",\"nonce\":\"<b64>\",\"alg\":\"xchacha20poly1305\",\"protection\":\"aukwrap\"}",
Encoding.UTF8, "application/json"));
GET the ref and decrypt client-side. Tampered ciphertext fails the Poly1305 tag and decryption fails closed.
var g = await http.GetStringAsync($"/v1/vaults/{vaultId}/secrets/{keyRef}");
// { "ciphertext":"<b64>", "nonce":"<b64>", "alg":"xchacha20poly1305", "protection":"aukwrap", "enc_key_name":"<b64>" }
// plaintext = XChaCha20Poly1305.Open(VK, nonce, ciphertext, aad); // client-side
Client-side crypto. Generate the machine key, derive HKDF(MK,"auth") and HKDF(MK,"unwrap"), wrap the vault key under the unwrap key and store Argon2id(auth_secret) as the verifier. Hand the machine key to the consumer. See the machine credentials section.
await http.PostAsync($"/v1/vaults/{vaultId}/credentials/machine",
new StringContent(
"{\"credential_id\":\"mach_...\",\"label\":\"ci-prod\",\"auth_verifier_salt\":\"<b64>\",\"auth_verifier\":\"<b64>\",\"wrapped_vk\":\"<b64>\"}",
Encoding.UTF8, "application/json"));
// consumer posts { credential_id, auth_secret } to /v1/auth/machine for a vault-scoped session
Once you can put and get a secret, prove the guarantee for yourself. The testing section walks a round-trip assert and a tamper assert against a local instance. The full endpoint reference is in the docs.