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).
| Function | Does |
|---|---|
secrets.put(service, account, data) -> void or SecretError | Keeps data under the pair, replacing what was there. At most secrets.MAX_BYTES (2560) bytes: past that TooLarge |
secrets.get(service, account) -> ?Secret or SecretError | What the pair holds, or null when nothing is kept there |
secrets.remove(service, account) -> bool or SecretError | Removes 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.
| Method | Is |
|---|---|
s.bytes() -> []const u8 | The secret |
s.len() -> i64 | How 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:
| Variant | When |
|---|---|
Unsupported | No 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 |
Denied | The 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) |
TooLarge | More than MAX_BYTES |
Failed | The store failed otherwise |
OutOfMemory | Out 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 areDeniedinstead of waiting. - Linux: the Secret Service (GNOME Keyring, KWallet, KeePassXC) on the
session bus, through
dbus’s shared connection: items with the attributesserviceandaccountin the default collection (whatsecret-tool lookup service S account Afinds), 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 isDenied, and so is a locked collection with no display to ask on ($DISPLAYand$WAYLAND_DISPLAYunset) or withallow_prompts(false).Unsupportedwithout 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: