secrets

tape
@profile(t1);
import secrets from "secrets";
import io from "io";

pub fn main() -> i32 {
    secrets.put("com.example.notes", "db-key", "correct horse battery staple") or {
        io.println("can't keep it: {err}");
        return 1;
    };
    let key: ?secrets.Secret = secrets.get("com.example.notes", "db-key") or {
        io.println("can't read it: {err}");
        return 1;
    };
    if (key == null) {
        io.println("nothing kept");
        return 1;
    }
    io.println("{key!.len()} bytes");            // the bytes are zeroed when key goes
    secrets.remove("com.example.notes", "db-key") or { return 1; };
    return 0;
}

A secret is a few bytes the system stores encrypted under the user’s login and gives back to this user’s programs, so a program needn’t keep a key in a file of its own. It is named by a service (your program’s reverse-DNS name) and an account (what the secret is for).

FunctionDoes
secrets.put(service, account, data) -> void or SecretErrorKeeps data under the pair, replacing what was there. At most secrets.MAX_BYTES (2560) bytes: past that TooLarge
secrets.get(service, account) -> ?Secret or SecretErrorWhat the pair holds, or null when nothing is kept there
secrets.remove(service, account) -> bool or SecretErrorRemoves it: true when something was there, false when not
secrets.allow_prompts(on)Whether the store may show the user a dialog (unlock the keychain, allow this program). On by default, where the session can show one; off, a store that would ask is Denied at once. Turn it off in a service or a test that must never wait on a person

An empty service or account is a programming error and traps.

Secret

What get gives back. Its bytes are zeroed before their memory is freed: when the Secret goes out of scope, or at free(). A copy is a second copy of the secret, zeroed in its turn.

MethodIs
s.bytes() -> []const u8The secret
s.len() -> i64How many bytes
s.free()Zeroes and frees them now; twice does nothing
tape
fn open_db(key: []const u8) -> bool { return key.len == 32; }

fn unlock() -> bool {
    let k: ?secrets.Secret = secrets.get("com.example.notes", "db-key") or { return false; };
    if (k == null) { return false; }
    return open_db(k!.bytes());                 // k zeroed and freed on return
}

Errors

SecretError:

VariantWhen
UnsupportedNo store for this program: Linux without a Secret Service, a Windows account without a profile, a sandboxed Mac program without the keychain entitlement. Ask the user for a passphrase instead
DeniedThe user refused, or the store is locked and can’t ask (prompts off with allow_prompts(false), or a session that can’t show a dialog: no graphic access on macOS, no display on Linux)
TooLargeMore than MAX_BYTES
FailedThe store failed otherwise
OutOfMemoryOut of memory

Platforms

  • Windows: the Credential Manager (DPAPI underneath), as generic credentials named service/account, kept on this machine and not roaming. You can see them in Control Panel, Credential Manager, Windows Credentials.
  • macOS: generic passwords in the login Keychain. A locked keychain asks the user to unlock it, and reading what an earlier build of an unsigned program wrote asks the user to allow it (a rebuilt unsigned program is a new program to the Keychain). Where no dialog can show (ssh, a daemon) or with allow_prompts(false), both are Denied instead of waiting.
  • Linux: the Secret Service (GNOME Keyring, KWallet, KeePassXC) on the session bus, through dbus’s shared connection: items with the attributes service and account in the default collection (what secret-tool lookup service S account A finds), the secret encrypted on the bus by a Diffie-Hellman session. A locked collection asks the user to unlock it, and the call waits; refusing is Denied, and so is a locked collection with no display to ask on ($DISPLAY and $WAYLAND_DISPLAY unset) or with allow_prompts(false). Unsupported without a session bus or a Secret Service.

The library is shell100 (.dll, .dylib, .so), next to tape; tape build copies it beside your program.

Last modified: