/api/system/login

POST

Set the global file-encryption password. Coin files written after a successful login are encrypted with a key derived from this password; encrypted files already on disk become readable.

POST http://localhost:8080/api/system/login

Description

The /api/system/login endpoint accepts a password in the POST body and uses it to derive the global file-encryption key. While the key is set:

  • New .bin coin files written through the standard helpers (coin_file_write / coin_file_write_atomic) are stored as AES-256-CTR encrypted on disk.
  • .bin files that were already encrypted under the same password become readable.
  • Already-plaintext .bin files (e.g. created before login) keep working but log a one-time PLAINTEXT-DETECTED warning. Use /api/system/encrypt_existing_files to convert them.

The password itself is never written to disk and never sent back over the wire. Internally the server derives SHA-256(password_bytes) as the AES-256 key. A separate salted SHA-256 of the password is stored in Client_Data/auth/verifier.bin (see "Verifier File" below) so subsequent logins can validate the password even when no encrypted coin files exist yet.

⚠️ Security Notes
  • POST body only. Sending password= in the URL query string is rejected with 400 because query strings are written to access logs.
  • Use HTTPS in any deployment that is not loopback-only.
  • Lost password = lost coins. Once files are encrypted, there is no recovery if the password is forgotten. Take a wallet backup before encrypting for the first time.
  • UTF-8 byte exactness. The hash is computed over the raw bytes the client sends. Make sure your client encodes non-ASCII characters as UTF-8 (form-encode them with --data-urlencode in cURL or use a fetch FormData body in JS) so two clients agree on the same hash.

Client Flow — what to call, in what order

A first-time login does not require any prerequisite calls. However, well-behaved clients should call /api/system/encryption-status first so the UI can decide whether to prompt for a password at all.

  1. Call GET /api/system/encryption-status.
    • If login_required: true, the wallet has encrypted files and the user must enter their password before any coin operation will work.
    • If login_required: false and encrypted_files_exist: false, login is optional. The user can either skip login (coins stay plaintext) or set a password to start encrypting future writes.
    • If key_set: true, the user is already logged in for this server session — no further prompt needed.
  2. Prompt the user for the password.
  3. POST /api/system/login with the password in the request body.
    • 200 + password_verified: true = correct password (matched the verifier or an existing encrypted file).
    • 200 + verifier_created: true = first-time login on a wallet with no encrypted files. The password the user just typed is now bound as the account password. Make sure the user did not typo.
    • 401 = incorrect password. The key is cleared and the user can retry.
  4. Optionally POST /api/system/encrypt_existing_files to convert any pre-existing plaintext .bin files to encrypted on disk.
Recovery vs. encryption status

The /api/recovery/status endpoint reports a different concern — whether the program has finished its boot-time coin health check and any interrupted deposit/upgrade tasks. Recovery and encryption are independent. A client that wants both signals should poll /api/recovery/status at startup AND call /api/system/encryption-status before its first wallet operation.

Parameters

Parameter Location Type Required Description
password POST body (form-encoded) string Required The user's password as raw UTF-8 bytes. Any characters are accepted. Sending in the query string is rejected.

How the password becomes a key

Suppose the password is ¥CheeseCake£ (a 12-byte UTF-8 string: C2 A5 43 68 65 65 73 65 43 61 6B 65 C2 A3). The server runs:

key = SHA-256(¥CheeseCake£)
    = 3bb491006bdd9a53379915b5d8068fcf66d2ebcc03ad1cbbdf8bcca247d5ec88

That 32-byte key is the AES-256 key used for every .bin file written while the user is logged in. The client never sends or stores this hex value — it only sends the password. It is shown here so a developer can verify their client's UTF-8 encoding by computing the same hash locally:

printf '%s' '¥CheeseCake£' | sha256sum
# 3bb491006bdd9a53379915b5d8068fcf66d2ebcc03ad1cbbdf8bcca247d5ec88

If your client sends the password as Latin-1 bytes (A5 43 68 ... A3) instead of UTF-8, the server computes a different hash and the verifier check will fail with 401. Either consistently encode as UTF-8 everywhere or explicitly set charset=utf-8 on the request Content-Type.

Response

Success — 200 OK

First-time login (verifier just created)
{
  "command": "login",
  "success": true,
  "message": "Encryption key set (no prior verifier or encrypted files — password is now the bound login password)",
  "key_set": true,
  "password_verified": false,
  "verifier_created": true
}
Subsequent login (verified against verifier.bin)
{
  "command": "login",
  "success": true,
  "message": "Password verified, encryption key set",
  "key_set": true,
  "password_verified": true,
  "verifier_created": false
}

Response Fields

FieldTypeDescription
key_setboolAlways true on a 200 response. The server now holds the derived key in memory.
password_verifiedbooltrue if the password was matched against either the persistent verifier or a real encrypted .bin file. false on a fresh-wallet first login (no prior reference to compare against — the password is being established, not verified).
verifier_createdbooltrue if this call wrote Client_Data/auth/verifier.bin for the first time. After this, future logins go through the verifier path and password_verified is authoritative.

Error Responses

400 — Password missing or in query string

{
  "error": true,
  "message": "Missing required parameter: password (use POST body)",
  "code": 400
}
{
  "error": true,
  "message": "Password must be sent via POST body, not query string (query strings are logged)",
  "code": 400
}

401 — Incorrect password

The supplied password did not match the verifier or any decryptable file. The server has cleared the key from memory; the user can retry.

{
  "error": true,
  "message": "Incorrect password",
  "code": 401
}

500 — Internal failure

Returned when the password could not be hashed or the key could not be installed. Should not happen in normal operation.

Verifier File

On a successful first-time login, the server writes Client_Data/auth/verifier.bin:

OffsetSizeField
04Magic "PVF1"
41Hash type (1 = SHA-256(password || salt))
53Reserved (zero)
816Random per-account salt
2432SHA-256(password_bytes || salt)

This file is the password's persistent reference. It survives logout. It does NOT contain the encryption key itself — the key is still derived live from the password on every login. The verifier exists so the server can return 401 immediately on a wrong password instead of letting the user encrypt their wallet to a typo.

The verifier is written to Client_Data/auth/, so it is also why /api/wallets/backup rejects destinations inside Client_Data.

Example Usage

curl -X POST \
  -H "Content-Type: application/x-www-form-urlencoded; charset=utf-8" \
  --data-urlencode "password=¥CheeseCake£" \
  "http://localhost:8080/api/system/login"
const password = '¥CheeseCake£';

const body = new URLSearchParams();
body.set('password', password);

const res = await fetch('http://localhost:8080/api/system/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8' },
  body: body
});
const data = await res.json();
if (res.status === 401) {
  // Wrong password — prompt the user again.
} else if (data.verifier_created) {
  // First-time login on a fresh wallet. The user just bound this password.
} else {
  // password_verified === true; logged in.
}
import requests

url = 'http://localhost:8080/api/system/login'
# Pass the password as raw UTF-8 — requests handles form encoding.
resp = requests.post(url, data={'password': '¥CheeseCake£'})

if resp.status_code == 401:
    raise SystemExit('wrong password')
data = resp.json()
print(data)
# {'command': 'login', 'success': True, 'key_set': True,
#  'password_verified': True, 'verifier_created': False, ...}

Related Endpoints