Skip to content

Profiles

jsem-nerad edited this page Sep 1, 2026 · 2 revisions

Profiles

A profile remembers the two harmless halves of a canteen login — the username and the canteen number — so neither the command line nor a script has to carry them. The password is not one of those halves: it goes to the operating system's credential store, or — on a machine that has none — into the profile file encrypted under a passphrase. It is never written in the clear unless you explicitly ask for that.

Profiles work from the command line and from Python.

Creating one

strava-cz profile add skola --username your.username --canteen 3753 --verify

You are asked for the password on the terminal (it is not echoed), and --verify logs in once to check it before anything is stored.

strava-cz profile list
strava-cz profile show skola      # never prints the password
strava-cz profile default doma    # which one --profile defaults to
strava-cz profile remove doma     # also clears its keyring entry

The first profile you add becomes the default, and a single profile is the default whether or not it is marked as one.

Where the password lives

--secret chooses the store. Nothing here writes the password in the clear unless you name file yourself.

--secret where the password goes remembered?
auto (default) the keyring if there is one, otherwise encrypted yes
keyring the OS credential store yes
encrypted encrypted into the profile file, under a passphrase yes
env nowhere; read from $STRAVA_CZ_PASSWORD each run no
prompt nowhere; asked for each run no
file plaintext in the profile file yes

auto is a strategy, not a stored value. It prints which way it went and writes the resolved source into the profile, so profile show always names the real location.

keyring uses the keyring package, which talks to GNOME Keyring or KWallet on Linux, the Keychain on macOS and the Credential Locker on Windows. Install it with the cli extra:

pip install "strava-cz[cli]"

file writes the password in the clear. The file is chmod 0600, which keeps other users out, but it does not stop anyone who can read your home directory or a backup of it — and the command says so, loudly, every time you use it. There is rarely a reason to choose it over encrypted.

Encrypted profiles

--secret encrypted is the answer for a machine with no credential store. The password is encrypted with AES-256-GCM under a key that scrypt derives from a passphrase you choose:

"encrypted": {
  "cipher": "aes-256-gcm",
  "kdf": {"name": "scrypt", "n": 32768, "r": 8, "p": 1, "dklen": 32, "salt": "…"},
  "nonce": "…",
  "ciphertext": "…"
}
  • A fresh salt and nonce are drawn for every save, so storing the same password twice produces two records that look nothing alike.
  • The KDF parameters live in the record, so raising the work factor in a later release still leaves older profiles readable.
  • GCM authenticates as well as encrypts. A damaged or edited record is rejected with a clear message instead of decrypting to nonsense.
  • Both primitives come from vetted implementations — scrypt from the standard library's hashlib, AES-GCM from cryptography, which the cli extra installs. Nothing here invents a construction of its own.

The passphrase is looked up exactly as the password is: $STRAVA_CZ_PASSPHRASE_<PROFILE>, then $STRAVA_CZ_PASSPHRASE, then a prompt. When you create the profile it is asked for twice, so a typo cannot lock you out of it.

What this does and does not buy you. It protects the profile file at rest — a backup, a Syncthing replica, a copied home directory, a profiles.json that ends up somewhere it should not. Anyone holding only that file has nothing usable.

It does not protect you from someone who can already read your environment or this process's memory while it runs. In cron the passphrase sits in a variable much as the password would; the difference is that the stored file is then safe to back up and sync, which the plaintext one never is.

To change the passphrase, add the profile again under the same name — it is replaced, and re-encrypted under the new one.

The profile file

$XDG_CONFIG_HOME/strava-cz/profiles.json     # ~/.config/... when XDG_CONFIG_HOME is unset

The file is created 0600 inside a 0700 directory and replaced atomically, so an interrupted write cannot leave half a store behind. A keyring-backed profile looks like this — note what is missing:

{
  "default": "skola",
  "profiles": {
    "skola": {
      "canteen_number": "3753",
      "language": "EN",
      "secret": "keyring",
      "username": "your.username"
    }
  }
}

--profile-store PATH points at a different file, which is useful for testing and for keeping a service account separate from your own.

Where the password is looked up

In this order, first hit wins:

  1. $STRAVA_CZ_PASSWORD_<PROFILE> — the profile name upper-cased, with anything that is not a letter or digit turned into _. So skola reads $STRAVA_CZ_PASSWORD_SKOLA.
  2. $STRAVA_CZ_PASSWORD.
  3. Whatever the profile's secret says — the keyring, the encrypted record, the plaintext file, or a prompt.

The environment always wins, including over a keyring profile. That is deliberate: a cron job or a container can drive an existing profile without rewriting it, and one job can drive several accounts by setting the per-profile variable for each.

Headless machines

A server with no desktop session usually has no working keyring — often there is no credential-store daemon installed at all, so there is nothing to unlock. The default --secret auto detects this before storing anything, says so, and encrypts instead:

$ strava-cz profile add skola --username your.username --canteen 3753
no OS keyring on this machine, so the password will be encrypted into the profile file
with a passphrase you choose (--secret keyring to insist on a keyring, --secret env to
store nothing at all)
Password for your.username at canteen 3753:
Passphrase for profile skola:
Passphrase again:
saved profile skola (your.username @ canteen 3753), password in encrypted in the profile file
unlock it without being asked by setting $STRAVA_CZ_PASSPHRASE_SKOLA (or
$STRAVA_CZ_PASSPHRASE) - useful in cron

Nothing is ever silently written somewhere less safe, and --secret keyring still means keyring: asking for it explicitly on such a machine fails rather than quietly becoming something else.

A profile created on a laptop with a keyring also keeps working when the file is copied to such a server, as long as $STRAVA_CZ_PASSWORD is set there — rule 1 above never needs the keyring.

From Python

from strava_cz import StravaCZ

with StravaCZ.from_profile("skola") as strava:   # omit the name for the default
    strava.menu.fetch()
    strava.menu.print()

from_profile() accepts the same keyword arguments as StravaCZ, plus:

argument meaning
store a ProfileStore to read from, instead of the default location
password skip the lookup entirely and use this
allow_prompt set False where nothing can answer a prompt, so it fails instead of hanging

Managing profiles directly:

from strava_cz import Profile, ProfileStore, SecretSource

store = ProfileStore()                       # or ProfileStore("/path/to/profiles.json")
store.save(Profile("skola", "your.username", "3753"), password="...")

store.list()                                 # [Profile(name='skola', ...)]
store.get()                                  # the default profile
store.get("skola")
store.set_default("skola")
store.resolve_password(store.get("skola"))   # follows the order above
store.resolve_passphrase(store.get("skola"))  # for an encrypted profile
store.delete("skola")                        # forgets the keyring entry too

Profile is a frozen dataclass whose repr() never contains the password, so it cannot leak into a traceback or a log line.

Errors

exception when
ProfileNotFoundError no such profile, or no default is set
KeyringUnavailableError no usable keyring; the message lists the alternatives
EncryptionUnavailableError cryptography is not installed, so nothing can be encrypted
ProfileError anything else — a bad name, a corrupt store, no password to be found

All of them derive from StravaError, so except StravaError still catches everything. On the command line they all exit with code 6.

Clone this wiki locally