diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 734c0f7..6d631eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,31 +75,49 @@ jobs: python3-pip python3-pytest xvfb imagemagick desktop-file-utils - name: Build the package run: python3 deb/build_deb.py - - name: Install it with apt - run: sudo apt-get install -y ./build/passman_*_all.deb - - name: Check what was installed + - name: Install the last passman package (from before the rename) + env: + GH_TOKEN: ${{ github.token }} run: | + gh release download v0.9.0 -R "$GITHUB_REPOSITORY" -p 'passman_*_all.deb' -D old + sudo apt-get install -y ./old/passman_0.9.0_all.deb dpkg -s passman - dpkg -L passman - test -f /usr/lib/passman/installed - test -x /usr/bin/passman - desktop-file-validate /usr/share/applications/passman.desktop - ls /usr/lib/passman/sourceFiles/__pycache__ # compiled by postinst + mkdir -p ~/.local/share/passman/vault + echo kept > ~/.local/share/passman/vault/alice.vault.enc + - name: Install it with apt - an upgrade that replaces passman + run: sudo apt-get install -y ./build/secretspace_*_all.deb + - name: The passman package is gone, its user's vaults are not + run: | + if dpkg-query -W -f='${Status}' passman 2>/dev/null | grep -q "install ok installed"; then + echo "passman is still installed"; exit 1; fi + test ! -e /usr/lib/passman + test ! -e /usr/bin/passman + grep -qx kept ~/.local/share/passman/vault/alice.vault.enc + /usr/bin/python3 -I -c "import sys; sys.path.insert(0, '/usr/lib/secretspace/sourceFiles'); sys.path.insert(0, '/usr/lib/secretspace/lib'); import vault, os; h = vault.get_home_dir(); print(h); assert h == os.path.expanduser('~/.local/share/passman'), h" + rm -r ~/.local/share/passman # the steps below check a fresh install + - name: Check what was installed + run: | + dpkg -s secretspace + dpkg -L secretspace + test -f /usr/lib/secretspace/installed + test -x /usr/bin/secretspace + desktop-file-validate /usr/share/applications/secretspace.desktop + ls /usr/lib/secretspace/sourceFiles/__pycache__ # compiled by postinst - name: The tests, on the system's Python, Tk, cryptography and Pillow - run: PYTHONPATH=/usr/lib/passman/lib xvfb-run -a /usr/bin/python3 -m pytest -q + run: PYTHONPATH=/usr/lib/secretspace/lib xvfb-run -a /usr/bin/python3 -m pytest -q - name: Run the installed app run: xvfb-run -a -s "-screen 0 1280x800x24" /usr/bin/python3 -I tools/smoke_gui.py smoke-out --installed - name: Start it the way the menu does run: | set +e - timeout 15 xvfb-run -a passman + timeout 15 xvfb-run -a secretspace code=$? - [ "$code" -eq 124 ] || { echo "passman exited after less than 15 s (code $code)"; exit 1; } + [ "$code" -eq 124 ] || { echo "secretspace exited after less than 15 s (code $code)"; exit 1; } - name: Remove it run: | - sudo apt-get remove -y passman - if [ -e /usr/lib/passman ]; then find /usr/lib/passman; exit 1; fi - test ! -e /usr/bin/passman + sudo apt-get remove -y secretspace + if [ -e /usr/lib/secretspace ]; then find /usr/lib/secretspace; exit 1; fi + test ! -e /usr/bin/secretspace - name: Keep the screenshots if: always() uses: actions/upload-artifact@v4 @@ -110,5 +128,5 @@ jobs: if: matrix.os == 'ubuntu-24.04' uses: actions/upload-artifact@v4 with: - name: passman-deb + name: secretspace-deb path: build/*.deb diff --git a/README.md b/README.md index 1577f88..f706090 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,46 @@ -# PassMan for Linux +# SecretSpace for Linux A local desktop password manager. Your vault is a single AES-256-GCM encrypted file. There is no online account and no service behind it: it works entirely offline, and if you want two PCs in step you run the sync server yourself, on your own hardware. -**`main` is 1.0.0-alpha.1, a PassMan 3 test version — not for real passwords yet.** -For real use keep the [0.9.0 release](https://github.com/usermaneGermane/PassMan-Client-Linux/releases/tag/v0.9.0), +**Formerly PassMan.** The project was renamed to SecretSpace in November +2026, because another password manager already uses the name PassMan. +Releases up to 0.9.0 and 1.0.0-alpha.1 are the `passman` package (command +`passman`, data in `~/.local/share/passman`). Installing the `secretspace` +package replaces it in one step (`Conflicts`/`Replaces: passman`), and your +vaults stay in `~/.local/share/passman`, which SecretSpace keeps using — +nothing is moved or renamed. The vault files are the same. + +**`main` is 1.0.0-alpha.2, a SecretSpace 3 test version — not for real passwords yet.** +For real use keep the [0.9.0 release](https://github.com/usermaneGermane/SecretSpace-Client-Linux/releases/tag/v0.9.0), which the rest of this README describes. It installs from a `.deb` on Ubuntu 22.04 or newer, or runs from source. It has so far been tested in CI only, on a virtual display, not yet on a real desktop. -![PassMan](sourceFiles/PassMan-logo.png) +![SecretSpace](sourceFiles/SecretSpace-logo.png) | Repository | What it holds | |---|---| -| [PassMan-Client-Windows](https://github.com/usermaneGermane/PassMan-Client-Windows) | the app for Windows | -| **PassMan-Client-Linux** (this one) | the app for Linux | -| [PassMan-Server-Windows](https://github.com/usermaneGermane/PassMan-Server-Windows) | the sync server for Windows, with an installer | -| [PassMan-Server-Linux](https://github.com/usermaneGermane/PassMan-Server-Linux) | the sync server for Linux, run by systemd | +| [SecretSpace-Client-Windows](https://github.com/usermaneGermane/SecretSpace-Client-Windows) | the app for Windows | +| **SecretSpace-Client-Linux** (this one) | the app for Linux | +| [SecretSpace-Server-Windows](https://github.com/usermaneGermane/SecretSpace-Server-Windows) | the sync server for Windows, with an installer | +| [SecretSpace-Server-Linux](https://github.com/usermaneGermane/SecretSpace-Server-Linux) | the sync server for Linux, run by systemd | **Same vaults as on Windows.** The vault file, the sync protocol and the -features match PassMan for Windows (see the version table under *What is +features match SecretSpace for Windows (see the version table under *What is different from Windows*): 0.9.0 = Windows 2.11.0, and this test version = -Windows 3.0.0-alpha.1. +Windows 3.0.0-alpha.2. A vault moves between a Windows PC and a Linux PC unchanged, and both sync with either server. -## PassMan 3 — the test version +## SecretSpace 3 — the test version -1.0.0-alpha.1 is the same as [PassMan for Windows -3.0.0-alpha.1](https://github.com/usermaneGermane/PassMan-Client-Windows#passman-3--a-test-version-not-for-real-passwords-yet): +1.0.0-alpha.2 is the same as [SecretSpace for Windows +3.0.0-alpha.2](https://github.com/usermaneGermane/SecretSpace-Client-Windows#secretspace-3--a-test-version-not-for-real-passwords-yet): accounts with a Secret Key and a 12-word recovery phrase, Argon2id, and -importing 2.x vaults with their history. It opens PassMan 3 accounts only, +importing 2.x vaults with their history. It opens SecretSpace 3 accounts only, so do not install it over 0.9.0 — your vaults would not show up (they are not changed). No sync yet. On Linux: @@ -44,7 +52,7 @@ not changed). No sync yet. On Linux: - The account folders are `700` and their files `600`, like the vault folder; a saved Emergency Kit is `600` too. - *Settings → Backup → Open folder* opens the folder with `xdg-open`. -- The package spells the version `1.0.0~alpha.1`, which apt sorts after +- The package spells the version `1.0.0~alpha.2`, which apt sorts after 0.9.0 and before 1.0.0. | File | What it is | @@ -58,36 +66,36 @@ They sit beside the `vault` folder below, which the test version only reads. ## Installing it -The package is `passman__all.deb`. Download it from -[Releases](https://github.com/usermaneGermane/PassMan-Client-Linux/releases) +The package is `secretspace__all.deb`. Download it from +[Releases](https://github.com/usermaneGermane/SecretSpace-Client-Linux/releases) (the release marked **Latest**; checksums in `SHA256SUMS.txt`; a *Pre-release* is a test version), or build it yourself on Ubuntu or Debian. -CI also keeps a `passman-deb` artifact of each run on `main`, but `main` is -the PassMan 3 test version now, so that one is for testing only. +CI also keeps a `secretspace-deb` artifact of each run on `main`, but `main` is +the SecretSpace 3 test version now, so that one is for testing only. ```bash sudo apt install git python3-pip -git clone https://github.com/usermaneGermane/PassMan-Client-Linux.git -cd PassMan-Client-Linux -python3 deb/build_deb.py # writes build/passman__all.deb +git clone https://github.com/usermaneGermane/SecretSpace-Client-Linux.git +cd SecretSpace-Client-Linux +python3 deb/build_deb.py # writes build/secretspace__all.deb ``` Then install it — apt pulls in Tk, cryptography, Argon2 (`python3-argon2`, in the *universe* section, on by default on Ubuntu desktops) and Pillow from Ubuntu: ```bash -sudo apt install ./build/passman_*_all.deb +sudo apt install ./build/secretspace_*_all.deb ``` -PassMan is then in the application menu, and `passman` starts it from a -terminal. `sudo apt remove passman` removes the program and leaves your +SecretSpace is then in the application menu, and `secretspace` starts it from a +terminal. `sudo apt remove secretspace` removes the program and leaves your vaults alone. | Installed file | What it is | | --- | --- | -| `/usr/bin/passman` | starts the app | -| `/usr/lib/passman/` | the program and the libraries it bundles (customtkinter, darkdetect, packaging — pinned by hash in `deb/vendor.txt`) | -| `/usr/share/applications/passman.desktop` | the menu entry | +| `/usr/bin/secretspace` | starts the app | +| `/usr/lib/secretspace/` | the program and the libraries it bundles (customtkinter, darkdetect, packaging — pinned by hash in `deb/vendor.txt`) | +| `/usr/share/applications/secretspace.desktop` | the menu entry | ## Running from source @@ -96,8 +104,8 @@ same way): ```bash sudo apt install python3-tk python3-venv git -git clone https://github.com/usermaneGermane/PassMan-Client-Linux.git ~/PassMan-Client-Linux -cd ~/PassMan-Client-Linux +git clone https://github.com/usermaneGermane/SecretSpace-Client-Linux.git ~/SecretSpace-Client-Linux +cd ~/SecretSpace-Client-Linux python3 -m venv .venv .venv/bin/pip install -r requirements.txt .venv/bin/python sourceFiles/gui.py @@ -109,12 +117,13 @@ After that, starting it is the last line, from that folder. Run from a checkout, as above, the files are in the checkout itself, next to `sourceFiles/` — the same rule as a portable copy on Windows. The -package keeps them in **`~/.local/share/passman`** -(or `$XDG_DATA_HOME/passman`) instead, so that upgrading or removing it never -touches them. `PASSMAN_DATA_DIR` points the app at any other vault folder. +package keeps them in **`~/.local/share/secretspace`** +(or `$XDG_DATA_HOME/secretspace`) instead, so that upgrading or removing it never +touches them — or in `~/.local/share/passman`, if that folder was there +first, from the `passman` package before the rename. `PASSMAN_DATA_DIR` points the app at any other vault folder. **Lab and production.** The package is the production copy. A checkout is a -handy lab copy beside it: it never touches `~/.local/share/passman`, and its +handy lab copy beside it: it never touches `~/.local/share/secretspace`, and its window title says `[LAB - ]` (the package's never does). If both sync to the same server, give lab vaults names your real vaults do not use — the server keeps one vault per username, so a lab vault with a @@ -128,19 +137,19 @@ real vault's name would sync into it. | `config.json` | non-secret preferences (last username, auto-lock delay, sort order) | The vault files are readable by your user only (`600`), and so is the -`vault` folder (`700`) when PassMan creates it. A plaintext CSV export is +`vault` folder (`700`) when SecretSpace creates it. A plaintext CSV export is created `600` too — it holds every password in the clear, so delete it once it has done its job. -**Moving a vault from Windows:** copy `%APPDATA%\PassMan\vault\.vault.enc` +**Moving a vault from Windows:** copy `%APPDATA%\SecretSpace\vault\.vault.enc` into the `vault` folder here, or use *Restore from server* on the login screen if you sync. ## What is different from Windows -- **Clipboard.** PassMan uses Tk's own clipboard, so nothing like `xclip` +- **Clipboard.** SecretSpace uses Tk's own clipboard, so nothing like `xclip` needs installing. It works on X11 and, through XWayland, on Wayland. On - X11 what PassMan copied disappears when PassMan closes. As on Windows, + X11 what SecretSpace copied disappears when SecretSpace closes. As on Windows, locking leaves a copied password pasteable, closing clears it at once, and copied passwords are cleared after 30 seconds — but a clipboard manager (GNOME or KDE clipboard history, CopyQ, …) may keep its own copy, which @@ -151,12 +160,12 @@ screen if you sync. installer does: a menu entry, data in your home folder, removal that keeps the vaults. Unlike the Windows installer it does not offer to move a vault from a copy run from source — copy the `vault` folder into - `~/.local/share/passman` yourself. + `~/.local/share/secretspace` yourself. Everything else — syncing (and sync on unlock), the archive, revision history, 2FA codes, importing from other password managers, and the security notes and limits — works and -reads exactly as in [PassMan for Windows's -README](https://github.com/usermaneGermane/PassMan-Client-Windows#syncing-two-pcs). +reads exactly as in [SecretSpace for Windows's +README](https://github.com/usermaneGermane/SecretSpace-Client-Windows#syncing-two-pcs). Where that README names a Windows version, read the Linux one beside it: | Windows | Linux | What arrived | @@ -171,6 +180,7 @@ Where that README names a Windows version, read the Linux one beside it: | 2.10.0 | 0.8.0 | 2FA codes, behind the master password | | 2.11.0 | 0.9.0 | sync on unlock, a setting that is off by default | | 3.0.0-alpha.1 | 1.0.0-alpha.1 | PassMan 3 test: accounts, Emergency Kit, recovery phrase, 2.x import | +| 3.0.0-alpha.2 | 1.0.0-alpha.2 | renamed to SecretSpace (package `secretspace`, replaces `passman`) | (0.2.0 added the `.deb` package, which has no Windows counterpart.) @@ -184,8 +194,8 @@ your master password. ## Project layout ``` -PassMan-Client-Linux/ -├── sourceFiles/ the program — ported from PassMan-Client-Windows +SecretSpace-Client-Linux/ +├── sourceFiles/ the program — ported from SecretSpace-Client-Windows │ ├── gui.py app shell: login, account list, lifecycle │ ├── clipboard.py the clipboard, through Tk (Linux only) │ ├── ... the same modules as on Windows @@ -193,7 +203,7 @@ PassMan-Client-Linux/ │ └── sync_server/ a copy of the server, for the end-to-end sync tests ├── deb/ │ ├── build_deb.py builds the .deb -│ ├── passman.py the launcher installed in /usr/lib/passman +│ ├── secretspace.py the launcher installed in /usr/lib/secretspace │ └── vendor.txt the libraries bundled into the package, pinned by hash └── tools/smoke_gui.py starts the real app on a display and takes screenshots ``` @@ -219,12 +229,12 @@ the login and main windows as the run's artifact. The `deb` job, on Ubuntu 22.04 and 24.04, builds the package, installs it with apt, runs the tests on the system's own Python, Tk, cryptography and Pillow, runs the smoke test against the installed copy -(`tools/smoke_gui.py --installed`), starts `passman` as the menu would, and +(`tools/smoke_gui.py --installed`), starts `secretspace` as the menu would, and removes the package again. The 24.04 run keeps the `.deb` as the -`passman-deb` artifact. +`secretspace-deb` artifact. **Staying in step with Windows.** The modules here began as copies of -PassMan-Client-Windows's `sourceFiles/`; only `vault.py` (where data lives, +SecretSpace-Client-Windows's `sourceFiles/`; only `vault.py` (where data lives, file modes), `gui.py` (clipboard, window icon), `theme.py` (fonts), `version.py`, `password_generator.py` and the new `clipboard.py` differ. A fix to the shared logic — the vault, sync, importers — belongs in both @@ -232,12 +242,12 @@ repositories. ## License -PassMan for Linux is free software under the **GNU General Public License +SecretSpace for Linux is free software under the **GNU General Public License v3.0 only** (GPL-3.0-only) — see [LICENSE](LICENSE). You may use, study, change and share it; if you distribute a changed version, it must stay under the same licence, with its source. -The other PassMan repositories (the Windows and Linux clients and sync servers) +The other SecretSpace repositories (the Windows and Linux clients and sync servers) are free software under the same licence. The 12-word recovery phrase uses the BIP39 English word list from diff --git a/SECURITY.md b/SECURITY.md index ec2c6fa..0b3d2ec 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,13 +1,13 @@ # Security policy -PassMan keeps passwords, so security reports are welcome and taken seriously. +SecretSpace keeps passwords, so security reports are welcome and taken seriously. ## Reporting a vulnerability Please **do not open a public issue**. Report it privately through GitHub: the repository's **Security** tab -> **Report a vulnerability**. -Include what you found, the version (Settings -> About, or `dpkg -s passman`), and the steps to +Include what you found, the version (Settings -> About, or `dpkg -s secretspace`), and the steps to reproduce it. You will get an answer within a week. Please give a reasonable time for a fix before you publish anything. @@ -23,4 +23,4 @@ time for a fix before you publish anything. The client in this repository: vault encryption, the master password, Secret Key and recovery phrase handling, the clipboard, the .deb package and -the sync client. Server issues go to PassMan-Server-Linux, the same way. +the sync client. Server issues go to SecretSpace-Server-Linux, the same way. diff --git a/deb/build_deb.py b/deb/build_deb.py index 96de24c..7672b67 100644 --- a/deb/build_deb.py +++ b/deb/build_deb.py @@ -1,11 +1,11 @@ """ -Build the PassMan .deb on Debian or Ubuntu: +Build the SecretSpace .deb on Debian or Ubuntu: python3 deb/build_deb.py -writes build/passman__all.deb. Install it with +writes build/secretspace__all.deb. Install it with - sudo apt install ./build/passman__all.deb + sudo apt install ./build/secretspace__all.deb Needs dpkg-deb (every Debian and Ubuntu has it) and pip, which fetches the libraries listed in deb/vendor.txt. Nothing is installed on the build @@ -13,14 +13,14 @@ What the package holds: - /usr/bin/passman starts the app (python3 -I) - /usr/lib/passman/passman.py the launcher behind it - /usr/lib/passman/sourceFiles/ the program, without its tests - /usr/lib/passman/lib/ customtkinter, darkdetect, packaging - /usr/lib/passman/installed the marker: data lives in - ~/.local/share/passman, not here - /usr/share/applications/passman.desktop - /usr/share/icons/hicolor/512x512/apps/passman.png + /usr/bin/secretspace starts the app (python3 -I) + /usr/lib/secretspace/secretspace.py the launcher behind it + /usr/lib/secretspace/sourceFiles/ the program, without its tests + /usr/lib/secretspace/lib/ customtkinter, darkdetect, packaging + /usr/lib/secretspace/installed the marker: data lives in + ~/.local/share/secretspace, not here + /usr/share/applications/secretspace.desktop + /usr/share/icons/hicolor/512x512/apps/secretspace.png cryptography, Pillow and Tk come from Ubuntu (DEPENDS), so the package is Architecture: all and keeps working when the system's python3 is upgraded. @@ -41,10 +41,12 @@ SOURCE_DIR = os.path.join(ROOT, "sourceFiles") VENDOR_FILE = os.path.join(HERE, "vendor.txt") -PACKAGE = "passman" -APP_DIR = "/usr/lib/passman" +PACKAGE = "secretspace" +#: The package name before the rename (0.9.0 and 1.0.0~alpha.1). +OLD_PACKAGE = "passman" +APP_DIR = "/usr/lib/secretspace" MAINTAINER = "usermaneGermane " -HOMEPAGE = "https://github.com/usermaneGermane/PassMan-Client-Linux" +HOMEPAGE = "https://github.com/usermaneGermane/SecretSpace-Client-Linux" #: Ubuntu 22.04 ships Python 3.10, the oldest the app supports. #: python3-pil.imagetk is separate from python3-pil on Debian and @@ -67,20 +69,20 @@ LAUNCHER = f"""#!/bin/sh # -I: the user's PYTHONPATH and ~/.local packages must not replace the # libraries in {APP_DIR}/lib. -exec /usr/bin/python3 -I {APP_DIR}/passman.py "$@" +exec /usr/bin/python3 -I {APP_DIR}/secretspace.py "$@" """ DESKTOP_ENTRY = """[Desktop Entry] Type=Application -Name=PassMan +Name=SecretSpace GenericName=Password Manager Comment=A local, encrypted password manager -Exec=passman -Icon=passman +Exec=secretspace +Icon=secretspace Terminal=false Categories=Utility; Keywords=password;vault;login; -StartupWMClass=passman +StartupWMClass=secretspace """ # The program folder is not writable by the user, so the app cannot leave @@ -102,8 +104,8 @@ """ MARKER_TEXT = """\ -Installed by the passman package. Because this file exists, PassMan keeps -its vaults and settings in ~/.local/share/passman (or $XDG_DATA_HOME/passman) +Installed by the secretspace package. Because this file exists, SecretSpace keeps +its vaults and settings in ~/.local/share/secretspace (or $XDG_DATA_HOME/secretspace) and never in this folder. Removing the package leaves them in place. """ @@ -145,14 +147,20 @@ def control(version, installed_size_kb): f"Maintainer: {MAINTAINER}\n" f"Installed-Size: {installed_size_kb}\n" f"Depends: {', '.join(DEPENDS)}\n" + # The package was called passman before the rename to SecretSpace. + # Installing this one removes that one in the same step; the vaults + # in the user's home are not part of either package and stay put. + f"Conflicts: {OLD_PACKAGE}\n" + f"Replaces: {OLD_PACKAGE}\n" + f"Provides: {OLD_PACKAGE}\n" "Section: utils\n" "Priority: optional\n" f"Homepage: {HOMEPAGE}\n" "Description: local, encrypted password manager\n" - " PassMan keeps your logins in a single AES-256-GCM encrypted vault\n" + " SecretSpace keeps your logins in a single AES-256-GCM encrypted vault\n" " file. It works offline, with no account; two PCs can stay in step\n" " through a sync server you run yourself. Vaults move unchanged\n" - " between PassMan for Windows and for Linux.\n" + " between SecretSpace for Windows and for Linux.\n" ) @@ -160,7 +168,7 @@ def copyright_text(pins): bundled = "\n".join(f" {name} {version}" for name, version in pins) return f"""\ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: PassMan +Upstream-Name: SecretSpace Source: {HOMEPAGE} Files: * @@ -168,7 +176,7 @@ def copyright_text(pins): License: none No licence has been granted yet. -Files: usr/lib/passman/lib/* +Files: usr/lib/secretspace/lib/* Comment: bundled Python libraries; each one's licence is in its *.dist-info folder next to it: {bundled} @@ -216,16 +224,16 @@ def stage(tree, version, wheels, source_dir=SOURCE_DIR, pins=()): shutil.copyfile(path, os.path.join(src_out, name)) unpack_wheels(wheels, os.path.join(app, "lib")) - shutil.copyfile(os.path.join(HERE, "passman.py"), os.path.join(app, "passman.py")) + shutil.copyfile(os.path.join(HERE, "secretspace.py"), os.path.join(app, "secretspace.py")) write(os.path.join(app, "installed"), MARKER_TEXT) - write(os.path.join(tree, "usr", "bin", "passman"), LAUNCHER, 0o755) - write(os.path.join(tree, "usr", "share", "applications", "passman.desktop"), + write(os.path.join(tree, "usr", "bin", "secretspace"), LAUNCHER, 0o755) + write(os.path.join(tree, "usr", "share", "applications", "secretspace.desktop"), DESKTOP_ENTRY) icon = os.path.join(tree, "usr", "share", "icons", "hicolor", "512x512", - "apps", "passman.png") + "apps", "secretspace.png") os.makedirs(os.path.dirname(icon)) - shutil.copyfile(os.path.join(source_dir, "PassMan-logo.png"), icon) + shutil.copyfile(os.path.join(source_dir, "SecretSpace-logo.png"), icon) write(os.path.join(tree, "usr", "share", "doc", PACKAGE, "copyright"), copyright_text(pins)) @@ -235,7 +243,7 @@ def stage(tree, version, wheels, source_dir=SOURCE_DIR, pins=()): os.chmod(dirpath, 0o755) for name in filenames: path = os.path.join(dirpath, name) - os.chmod(path, 0o755 if path.endswith(os.path.join("bin", "passman")) else 0o644) + os.chmod(path, 0o755 if path.endswith(os.path.join("bin", "secretspace")) else 0o644) size = sum(os.path.getsize(os.path.join(d, n)) for d, _, files in os.walk(tree) for n in files) @@ -248,7 +256,7 @@ def stage(tree, version, wheels, source_dir=SOURCE_DIR, pins=()): def main(argv=None): - parser = argparse.ArgumentParser(description="Build the PassMan .deb.") + parser = argparse.ArgumentParser(description="Build the SecretSpace .deb.") parser.add_argument("--out", default=os.path.join(ROOT, "build"), help="folder for the .deb (default: build/)") args = parser.parse_args(argv) diff --git a/deb/passman.py b/deb/secretspace.py similarity index 61% rename from deb/passman.py rename to deb/secretspace.py index cd67864..7d099c1 100644 --- a/deb/passman.py +++ b/deb/secretspace.py @@ -1,6 +1,6 @@ """ -Start an installed PassMan. The package puts this at /usr/lib/passman and -/usr/bin/passman runs it with ``python3 -I``, so nothing from the user's own +Start an installed SecretSpace. The package puts this at /usr/lib/secretspace and +/usr/bin/secretspace runs it with ``python3 -I``, so nothing from the user's own Python setup (PYTHONPATH, ~/.local site-packages) can stand in for the libraries the package ships. """ @@ -15,8 +15,8 @@ # helpers); cryptography and Pillow come from the system's python3. sys.path[:0] = [os.path.join(APP_DIR, "sourceFiles"), os.path.join(APP_DIR, "lib")] -# Tk names the window after argv[0]; "passman" ties it to passman.desktop, -# so the dock shows PassMan's icon. -sys.argv[0] = "passman" +# Tk names the window after argv[0]; "secretspace" ties it to secretspace.desktop, +# so the dock shows SecretSpace's icon. +sys.argv[0] = "secretspace" runpy.run_path(os.path.join(APP_DIR, "sourceFiles", "gui.py"), run_name="__main__") diff --git a/deb/tests/test_build_deb.py b/deb/tests/test_build_deb.py index 44c49e1..f6d1e8a 100644 --- a/deb/tests/test_build_deb.py +++ b/deb/tests/test_build_deb.py @@ -63,43 +63,46 @@ def test_vendored_pins_are_exact(): def test_program_without_its_tests(tree): - src = os.path.join(tree, "usr", "lib", "passman", "sourceFiles") + src = os.path.join(tree, "usr", "lib", "secretspace", "sourceFiles") names = set(os.listdir(src)) - assert {"gui.py", "vault.py", "version.py", "PassMan-logo.png"} <= names + assert {"gui.py", "vault.py", "version.py", "SecretSpace-logo.png"} <= names assert "tests" not in names and "__pycache__" not in names def test_installed_marker_moves_data_to_home(tree): # vault.is_installed() looks for this file next to sourceFiles/. - assert os.path.isfile(os.path.join(tree, "usr", "lib", "passman", "installed")) + assert os.path.isfile(os.path.join(tree, "usr", "lib", "secretspace", "installed")) def test_wheels_unpacked_into_lib(tree): - lib = os.path.join(tree, "usr", "lib", "passman", "lib") + lib = os.path.join(tree, "usr", "lib", "secretspace", "lib") assert os.path.isfile(os.path.join(lib, "fakelib", "__init__.py")) assert os.path.isfile(os.path.join(lib, "fakelib-1.0.dist-info", "LICENSE")) def test_launcher_runs_isolated_python(tree): - text = read(tree, "usr", "bin", "passman") + text = read(tree, "usr", "bin", "secretspace") assert text.startswith("#!/bin/sh\n") - assert 'exec /usr/bin/python3 -I /usr/lib/passman/passman.py "$@"' in text - assert os.path.isfile(os.path.join(tree, "usr", "lib", "passman", "passman.py")) + assert 'exec /usr/bin/python3 -I /usr/lib/secretspace/secretspace.py "$@"' in text + assert os.path.isfile(os.path.join(tree, "usr", "lib", "secretspace", "secretspace.py")) def test_desktop_entry_and_icon(tree): - entry = read(tree, "usr", "share", "applications", "passman.desktop") - assert "Exec=passman\n" in entry and "Icon=passman\n" in entry - assert "StartupWMClass=passman\n" in entry + entry = read(tree, "usr", "share", "applications", "secretspace.desktop") + assert "Exec=secretspace\n" in entry and "Icon=secretspace\n" in entry + assert "StartupWMClass=secretspace\n" in entry assert os.path.isfile(os.path.join(tree, "usr", "share", "icons", "hicolor", - "512x512", "apps", "passman.png")) + "512x512", "apps", "secretspace.png")) def test_control_file(tree): text = read(tree, "DEBIAN", "control") fields = dict(line.split(": ", 1) for line in text.splitlines() if line and not line.startswith(" ")) - assert fields["Package"] == "passman" + assert fields["Package"] == "secretspace" + # Upgrading from the passman package (before the rename) replaces it. + for field in ("Conflicts", "Replaces", "Provides"): + assert fields[field] == "passman" assert fields["Version"] == "9.8.7" assert fields["Architecture"] == "all" assert int(fields["Installed-Size"]) > 0 @@ -110,16 +113,16 @@ def test_control_file(tree): def test_maintainer_scripts_compile_and_clean(tree): - assert "py3compile -p passman /usr/lib/passman" in read(tree, "DEBIAN", "postinst") - assert "py3clean -p passman" in read(tree, "DEBIAN", "prerm") + assert "py3compile -p secretspace /usr/lib/secretspace" in read(tree, "DEBIAN", "postinst") + assert "py3clean -p secretspace" in read(tree, "DEBIAN", "prerm") def test_copyright_lists_bundled_libraries(tree): - assert "fakelib 1.0" in read(tree, "usr", "share", "doc", "passman", "copyright") + assert "fakelib 1.0" in read(tree, "usr", "share", "doc", "secretspace", "copyright") def test_generated_files_use_lf(tree): - for parts in (("usr", "bin", "passman"), ("DEBIAN", "control"), ("DEBIAN", "prerm")): + for parts in (("usr", "bin", "secretspace"), ("DEBIAN", "control"), ("DEBIAN", "prerm")): with open(os.path.join(tree, *parts), "rb") as f: assert b"\r" not in f.read() @@ -129,10 +132,10 @@ def test_file_modes(tree): def mode(*parts): return os.stat(os.path.join(tree, *parts)).st_mode & 0o777 - assert mode("usr", "bin", "passman") == 0o755 + assert mode("usr", "bin", "secretspace") == 0o755 assert mode("DEBIAN", "postinst") == 0o755 - assert mode("usr", "lib", "passman", "sourceFiles", "gui.py") == 0o644 - assert mode("usr", "lib", "passman") == 0o755 + assert mode("usr", "lib", "secretspace", "sourceFiles", "gui.py") == 0o644 + assert mode("usr", "lib", "secretspace") == 0o755 def test_refuses_compiled_wheels(tmp_path, monkeypatch): diff --git a/deb/vendor.txt b/deb/vendor.txt index 22be80a..e6c85c5 100644 --- a/deb/vendor.txt +++ b/deb/vendor.txt @@ -1,4 +1,4 @@ -# Python libraries bundled into the .deb, in /usr/lib/passman/lib. +# Python libraries bundled into the .deb, in /usr/lib/secretspace/lib. # # Only pure-Python wheels go here, so the package stays Architecture: all # and works with whatever python3 the system has. cryptography and Pillow diff --git a/requirements.txt b/requirements.txt index ab1553c..f53d5e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -# PassMan dependencies. Also needs Tk from the system: sudo apt install python3-tk +# SecretSpace dependencies. Also needs Tk from the system: sudo apt install python3-tk cryptography>=42 # AES-GCM + PBKDF2 vault encryption argon2-cffi>=21.1 # Argon2id for 3.0 accounts (Ubuntu: python3-argon2; or cryptography>=44) customtkinter>=5.2 # modern UI toolkit diff --git a/sourceFiles/PassMan-logo.png b/sourceFiles/SecretSpace-logo.png similarity index 100% rename from sourceFiles/PassMan-logo.png rename to sourceFiles/SecretSpace-logo.png diff --git a/sourceFiles/account_crypto.py b/sourceFiles/account_crypto.py index c073596..a2189a8 100644 --- a/sourceFiles/account_crypto.py +++ b/sourceFiles/account_crypto.py @@ -1,5 +1,5 @@ """ -The cryptography of PassMan 3.0 accounts (design: E:\\CLAUDE\\PassMan\\DESIGN-3.0.md). +The cryptography of SecretSpace 3.0 accounts (design: E:\\CLAUDE\\SecretSpace\\DESIGN-3.0.md). Nothing in the 2.x app calls this yet: phase 1 builds the local account on it. Every function is pure -- bytes and dicts in, bytes and dicts out, no files -- @@ -128,7 +128,7 @@ def _check_cost(passes, memory_kib, lanes): and 8 * lanes <= memory_kib <= ARGON2_MAX_MEMORY_KIB and 1 <= lanes <= ARGON2_MAX_LANES): raise CryptoError("The account file asks for an unreasonable key " - "derivation cost; it is damaged or not PassMan's.") + "derivation cost; it is damaged or not SecretSpace's.") # =========================================================================== @@ -252,13 +252,13 @@ def keyset_header(blob: str) -> dict: the password is stretched, so the stored cost is the one used.""" raw = _unb64(blob, "account file") if len(raw) < _KEYSET_HEADER.size + NONCE_LEN + 16: - raise CryptoError("The account file is too short to be PassMan's.") + raise CryptoError("The account file is too short to be SecretSpace's.") magic, version, kdf, memory_kib, passes, lanes, salt = \ _KEYSET_HEADER.unpack(raw[:_KEYSET_HEADER.size]) if magic != KEYSET_MAGIC: - raise CryptoError("That is not a PassMan 3 account file.") + raise CryptoError("That is not a SecretSpace 3 account file.") if version != KEYSET_VERSION: - raise CryptoError("The account file was written by a newer PassMan; " + raise CryptoError("The account file was written by a newer SecretSpace; " "update this one.") if kdf not in (KDF_ARGON2ID, KDF_NONE): raise CryptoError("The account file uses an unknown key derivation.") @@ -329,7 +329,7 @@ def wrap_key(recipient_public: bytes, vault_key: bytes, vault_id: bytes, def unwrap_key(keyset: dict, wrapped: str, vault_id: bytes, member: str) -> bytes: raw = _unb64(wrapped, "shared vault key") if len(raw) != 1 + 32 + NONCE_LEN + KEY_LEN + 16 or raw[0] != WRAP_VERSION: - raise CryptoError("A shared vault key is damaged or from a newer PassMan.") + raise CryptoError("A shared vault key is damaged or from a newer SecretSpace.") eph_pub, nonce, ct = raw[1:33], raw[33:33 + NONCE_LEN], raw[33 + NONCE_LEN:] private = X25519PrivateKey.from_private_bytes( _unb64(keyset["x25519_private"], "account key")) @@ -373,13 +373,13 @@ def encrypt_vault(vault_key: bytes, vault_id: bytes, key_generation: int, def vault_header(blob: str) -> dict: raw = _unb64(blob, "vault") if len(raw) < _VAULT_HEADER.size + NONCE_LEN + 16: - raise CryptoError("The vault file is too short to be a PassMan 3 vault.") + raise CryptoError("The vault file is too short to be a SecretSpace 3 vault.") magic, version, vault_id, generation = _VAULT_HEADER.unpack( raw[:_VAULT_HEADER.size]) if magic != VAULT_MAGIC: - raise CryptoError("That is not a PassMan 3 vault.") + raise CryptoError("That is not a SecretSpace 3 vault.") if version != VAULT_VERSION: - raise CryptoError("The vault was written by a newer PassMan; update " + raise CryptoError("The vault was written by a newer SecretSpace; update " "this one.") return {"vault_id": vault_id, "key_generation": generation} diff --git a/sourceFiles/accounts.py b/sourceFiles/accounts.py index 05b66f3..52bd0bc 100644 --- a/sourceFiles/accounts.py +++ b/sourceFiles/accounts.py @@ -1,5 +1,5 @@ """ -PassMan 3.0 accounts on this device -- phase 1: local only, no server. +SecretSpace 3.0 accounts on this device -- phase 1: local only, no server. An account replaces the 2.x "one vault file per user". It is a folder:: @@ -12,7 +12,7 @@ vaults/.pmv3 the personal vault (+ .bak, the previous save) ```` is the folder that holds the 2.x ``vault`` folder (beside the app -for a lab copy, %APPDATA%\\PassMan when installed). 2.x vault files are only +for a lab copy, %APPDATA%\\SecretSpace when installed). 2.x vault files are only ever *read*, by the importer; nothing here writes to them. The cryptography is account_crypto; this module is files and lifecycle. @@ -104,7 +104,7 @@ def _read_info(username: str) -> dict: raise ValueError(f"The account file of “{username}” is damaged.") from None if not isinstance(info, dict) or info.get("format") != ACCOUNT_FORMAT: raise ValueError(f"The account “{username}” was made by another " - "PassMan version.") + "SecretSpace version.") return info @@ -335,7 +335,7 @@ def write_emergency_kit(path: str, username: str, secret_key_text: str) -> None: def emergency_kit_text(username: str, secret_key_text: str, created: str = "") -> str: """The Emergency Kit as a text file (Save Emergency Kit…).""" return "\n".join([ - "PassMan Emergency Kit", + "SecretSpace Emergency Kit", "=====================", "", f"Account: {username}", diff --git a/sourceFiles/clipboard.py b/sourceFiles/clipboard.py index e55090f..ecf8481 100644 --- a/sourceFiles/clipboard.py +++ b/sourceFiles/clipboard.py @@ -4,8 +4,8 @@ On Linux pyperclip needs xclip, xsel or wl-clipboard installed and fails without them. Tk's own clipboard needs nothing: it works on X11 and, through XWayland, on Wayland desktops. It also suits a password manager: on X11 the -clipboard belongs to the program that copied, so what PassMan copied is gone -once PassMan closes -- unless a clipboard manager kept its own copy. +clipboard belongs to the program that copied, so what SecretSpace copied is gone +once SecretSpace closes -- unless a clipboard manager kept its own copy. """ import tkinter as tk diff --git a/sourceFiles/crypto_utils.py b/sourceFiles/crypto_utils.py index 7d451f8..626dde8 100644 --- a/sourceFiles/crypto_utils.py +++ b/sourceFiles/crypto_utils.py @@ -1,5 +1,5 @@ """ -Vault encryption for PassMan. +Vault encryption for SecretSpace. Container format (all vaults written by this version):: @@ -99,7 +99,7 @@ def _decrypt_v2(master_password: str, raw: bytes) -> dict: kdf_id = header[len(MAGIC)] if kdf_id != KDF_PBKDF2_SHA256: raise ValueError(f"Unsupported key-derivation function id {kdf_id}. " - "This vault needs a newer version of PassMan.") + "This vault needs a newer version of SecretSpace.") iterations = int.from_bytes(header[len(MAGIC) + 1:], "big") if not 1 <= iterations <= MAX_ITERATIONS: diff --git a/sourceFiles/dialogs.py b/sourceFiles/dialogs.py index d01d6a0..179d91e 100644 --- a/sourceFiles/dialogs.py +++ b/sourceFiles/dialogs.py @@ -45,7 +45,7 @@ def _site_tile(site): def create_user_dialog(app): """ - A new PassMan 3 account, in three steps: name and master password, the + A new SecretSpace 3 account, in three steps: name and master password, the Emergency Kit (Secret Key), the 12-word recovery phrase. Nothing is written until the last step, so closing a window half-way leaves no account behind. @@ -113,7 +113,7 @@ def _save_kit_file(app, username, secret_key_text): path = filedialog.asksaveasfilename( parent=app.root, title="Save Emergency Kit", defaultextension=".txt", - initialfile=f"PassMan Emergency Kit - {username}.txt", + initialfile=f"SecretSpace Emergency Kit - {username}.txt", filetypes=[("Text file", "*.txt"), ("All files", "*.*")]) if not path: return False @@ -139,7 +139,7 @@ def _emergency_kit_dialog(app, new, password): shell = dialog_shell(top, "Your Emergency Kit", "Step 2 of 3 · With your master password, this " "Secret Key opens your account on another PC. " - "PassMan keeps a copy on this PC only.") + "SecretSpace keeps a copy on this PC only.") wrap = shell.body field_label(wrap, "Account") @@ -201,7 +201,7 @@ def _phrase_dialog(app, new, password): note(wrap, "Anyone with these words can open everything in this account. " "Keep them like a passport: not in a file or a photo, and not with " - "the Emergency Kit. PassMan does not show them again.", + "the Emergency Kit. SecretSpace does not show them again.", shell.text_width, pady=(14, 0)) def next_step(): @@ -344,7 +344,7 @@ def go(action): def _import_2x_file(app): path = filedialog.askopenfilename( parent=app.root, title="Import a PassMan 2.x vault", - filetypes=[("PassMan vault", "*.enc *.vault *.bak"), + filetypes=[("SecretSpace vault", "*.enc *.vault *.bak"), ("All files", "*.*")]) if path: _import_2x(app, path, os.path.basename(path)) @@ -387,7 +387,7 @@ def unlock(): except OSError as e: err.configure(text=f"Could not read the file: {e}"); return except ValueError: - err.configure(text="Wrong password, or not a PassMan vault file.") + err.configure(text="Wrong password, or not a SecretSpace vault file.") return import copy trial = copy.deepcopy(app.vault) @@ -452,7 +452,7 @@ def _finish_import(app, parsed, path): # Say so up front: silently losing data is how people get locked # out of their accounts after a migration. msg += ("\n\nThis file also contains " + ", ".join(parsed.dropped) + - ", which PassMan cannot store. Keep the original file " + ", which SecretSpace cannot store. Keep the original file " "until you have checked you still have them.") msg += "\n\nAdd them to your vault?" if not app._confirm("Import accounts", msg, danger=False): @@ -700,12 +700,12 @@ def set_autolock(choice): lock_menu.set(label_for(appconfig.autolock_minutes())) # ---- sync ---- - # PassMan 3 accounts sync from phase 2 (server accounts) on. + # SecretSpace 3 accounts sync from phase 2 (server accounts) on. syn = section(wrap, "Sync") # The control slot gets a label: an empty CTkFrame is 200 px tall. ctk.CTkLabel(setting_row(syn, "Not in this test version", "Accounts sync from the next 3.0 test version " - "on. Your 2.x vaults keep syncing in PassMan " + "on. Your 2.x vaults keep syncing in SecretSpace " "2.x.", hint, first=True), text="Next version", font=F(12, "bold"), text_color=MUTED).pack() diff --git a/sourceFiles/gui.py b/sourceFiles/gui.py index e43099f..8190a9d 100644 --- a/sourceFiles/gui.py +++ b/sourceFiles/gui.py @@ -1,5 +1,5 @@ """ -PassMan - a modern desktop password manager. +SecretSpace - a modern desktop password manager. Only the presentation layer lives here, and only the application shell: the login screen, the account list with its details pane and the session @@ -145,7 +145,7 @@ def __init__(self, root): def _load_branding(self): # Linux takes the window icon as an image, not an .ico. With # default=True every dialog gets it too, so icon_path stays None. - for p in asset_candidates("PassMan-logo.png"): + for p in asset_candidates("SecretSpace-logo.png"): if os.path.exists(p): try: self._icon_photo = tk.PhotoImage(file=p) @@ -155,7 +155,7 @@ def _load_branding(self): pass self.logo_image = self.logo_small = None - for p in asset_candidates("PassMan-logo.png"): + for p in asset_candidates("SecretSpace-logo.png"): if os.path.exists(p): try: from PIL import Image, ImageChops @@ -456,7 +456,7 @@ def _draw_login_tiles(self, users): self._login_tiles.grid_columnconfigure(c, weight=1, uniform="tile") if not users: ctk.CTkLabel(self._login_tiles, - text="There is no PassMan 3 account on this PC yet. " + text="There is no SecretSpace 3 account on this PC yet. " "Create one, then import your 2.x vaults into " "it.", font=F(13), text_color=MUTED, wraplength=360).grid( @@ -670,7 +670,7 @@ def show_main_window(self): brand = ctk.CTkFrame(bar, fg_color="transparent") brand.grid(row=0, column=0, padx=(24, 14)) self._logo(brand, small=True).pack(side="left") - ctk.CTkLabel(brand, text="PassMan", font=F(17, "bold"), + ctk.CTkLabel(brand, text="SecretSpace", font=F(17, "bold"), text_color=TEXT).pack(side="left", padx=(10, 0)) bg, ink, _ = tile_colors(self.current_user) @@ -1157,7 +1157,7 @@ def toggle(): if not dialogs.ask_master_password( self, "Show 2FA code", f"Enter your master password to see the code for " - f"“{_clip(acc['site'], 40)}”. PassMan asks every time.", + f"“{_clip(acc['site'], 40)}”. SecretSpace asks every time.", action="Show code"): return if not shown.winfo_exists(): diff --git a/sourceFiles/importers.py b/sourceFiles/importers.py index 429a96d..3c38530 100644 --- a/sourceFiles/importers.py +++ b/sourceFiles/importers.py @@ -30,7 +30,7 @@ TOTP_KEYS = ("totp", "login_totp", "otp", "otpauth", "authenticator", "two_factor", "2fa") -#: Columns whose contents PassMan has nowhere to put. Reported to the user +#: Columns whose contents SecretSpace has nowhere to put. Reported to the user #: rather than discarded quietly -- losing a TOTP seed without being told is #: how people get locked out of their accounts after a migration. LOSSY_KEYS = ( diff --git a/sourceFiles/secret_store.py b/sourceFiles/secret_store.py index bef8705..b26a892 100644 --- a/sourceFiles/secret_store.py +++ b/sourceFiles/secret_store.py @@ -152,7 +152,7 @@ def unprotect(record, username: str, password: str) -> bytes: except Exception: raise CryptoError("Wrong master password or Secret Key.") from None else: - raise Unavailable("The stored Secret Key is in a form this PassMan " + raise Unavailable("The stored Secret Key is in a form this SecretSpace " "does not know.") if len(raw) != account_crypto.SECRET_KEY_LEN: raise Unavailable("The stored Secret Key is damaged.") diff --git a/sourceFiles/sync_ui.py b/sourceFiles/sync_ui.py index c07a98f..374192f 100644 --- a/sourceFiles/sync_ui.py +++ b/sourceFiles/sync_ui.py @@ -445,7 +445,7 @@ def password_mismatch_dialog(app, adopt, wrong=False): body = ("You changed this vault's master password while the server " "could not be reached, so its copy is still under your " "previous password.\n\n" - "Enter the previous password once. PassMan reads the server's " + "Enter the previous password once. SecretSpace reads the server's " "copy with it, merges, and uploads everything again under " "your new password.") label = "Your previous master password" @@ -575,7 +575,7 @@ def purge(): "Delete permanently", f"Permanently delete the saved login for " f"“{entry.get('site', '')}”?\n\n" - "This cannot be undone, and it is the only action in PassMan " + "This cannot be undone, and it is the only action in SecretSpace " "that really destroys a password."): return vault.purge_account(app.vault, entry["id"]) @@ -643,7 +643,7 @@ def sync_settings_dialog(app): token_entry.insert(0, settings.get("token", "")) ca_entry = field(wrap, "Certificate file (only for https, optional)", - placeholder="/home/you/passman/server.crt") + placeholder="/home/you/secretspace/server.crt") ca_entry.insert(0, settings.get("ca_file", "")) note(wrap, "Plain http is reasonable over a VPN you trust. For https with " diff --git a/sourceFiles/tests/sync_server/README.md b/sourceFiles/tests/sync_server/README.md index 3ae7660..6b4307f 100644 --- a/sourceFiles/tests/sync_server/README.md +++ b/sourceFiles/tests/sync_server/README.md @@ -1,7 +1,7 @@ # A copy of the sync server, for the tests `passman_server.py` here is an unchanged copy of `server/passman_server.py` -from the server repositories (PassMan-Server-Windows and PassMan-Server-Linux +from the server repositories (SecretSpace-Server-Windows and SecretSpace-Server-Linux hold the same file). `test_sync_server.py` runs it on a free port and syncs two simulated PCs through it over a real socket, so the client is tested against the real server rather than a mock. diff --git a/sourceFiles/tests/sync_server/passman_server.py b/sourceFiles/tests/sync_server/passman_server.py index 4b2c557..47e7d98 100644 --- a/sourceFiles/tests/sync_server/passman_server.py +++ b/sourceFiles/tests/sync_server/passman_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -PassMan sync server -- a store for encrypted vault blobs. +SecretSpace sync server -- a store for encrypted vault blobs. What it is NOT: it cannot read your passwords. The client encrypts the vault with your master password before it ever reaches the network, so this process @@ -524,7 +524,7 @@ def main(argv=None): # flush: under systemd stdout is a pipe and fully buffered, so without it # these lines never reach the journal while the server is running -- and # a silent journal after `systemctl start` reads as a failure. - print(f"PassMan sync on {scheme}://{args.host}:{args.port}") + print(f"SecretSpace sync on {scheme}://{args.host}:{args.port}") print(f"Vault store: {httpd.store.root}") if scheme == "http" and args.host not in ("127.0.0.1", "localhost"): print("NOTE: plain HTTP on a network address. Vaults are encrypted " diff --git a/sourceFiles/tests/test_account_crypto.py b/sourceFiles/tests/test_account_crypto.py index ba1b924..ec47bf5 100644 --- a/sourceFiles/tests/test_account_crypto.py +++ b/sourceFiles/tests/test_account_crypto.py @@ -1,4 +1,4 @@ -"""PassMan 3.0 account cryptography: published vectors first, then the format.""" +"""SecretSpace 3.0 account cryptography: published vectors first, then the format.""" import base64 import hashlib @@ -178,8 +178,8 @@ def test_an_absurd_cost_is_refused_before_any_work(cost): @pytest.mark.parametrize("mangle,words", [ - (lambda r: b"PMV2" + r[4:], "not a PassMan 3 account"), - (lambda r: r[:4] + b"\x09" + r[5:], "newer PassMan"), + (lambda r: b"PMV2" + r[4:], "not a SecretSpace 3 account"), + (lambda r: r[:4] + b"\x09" + r[5:], "newer SecretSpace"), (lambda r: r[:30], "too short"), ]) def test_a_wrong_file_says_so(mangle, words): @@ -254,7 +254,7 @@ def test_a_2x_vault_is_not_mistaken_for_pmv3(): import crypto_utils blob = crypto_utils.encrypt("pw", DATA, iterations=1000) blob = blob.decode() if isinstance(blob, bytes) else blob - with pytest.raises(ac.CryptoError, match="not a PassMan 3 vault"): + with pytest.raises(ac.CryptoError, match="not a SecretSpace 3 vault"): ac.vault_header(blob) diff --git a/sourceFiles/tests/test_accounts.py b/sourceFiles/tests/test_accounts.py index e0751dd..49647c8 100644 --- a/sourceFiles/tests/test_accounts.py +++ b/sourceFiles/tests/test_accounts.py @@ -1,4 +1,4 @@ -"""PassMan 3.0 local accounts: files, unlock, Secret Key per device, 2.x import.""" +"""SecretSpace 3.0 local accounts: files, unlock, Secret Key per device, 2.x import.""" import hashlib import json @@ -227,7 +227,7 @@ def test_newer_account_format_refused(): info = json.load(open(path, encoding="utf-8")) info["format"] = 99 json.dump(info, open(path, "w", encoding="utf-8")) - with pytest.raises(ValueError, match="another PassMan version"): + with pytest.raises(ValueError, match="another SecretSpace version"): accounts.unlock("alice", PW) diff --git a/sourceFiles/tests/test_crypto.py b/sourceFiles/tests/test_crypto.py index 3908c54..cd9680f 100644 --- a/sourceFiles/tests/test_crypto.py +++ b/sourceFiles/tests/test_crypto.py @@ -13,7 +13,7 @@ def legacy_blob(password, data): - """A vault exactly as PassMan wrote them before the PMV2 header existed.""" + """A vault exactly as SecretSpace wrote them before the PMV2 header existed.""" salt, nonce = os.urandom(16), os.urandom(12) key = C.derive_key(password, salt, C.LEGACY_ITERATIONS) ct = AESGCM(key).encrypt(nonce, json.dumps(data).encode(), None) diff --git a/sourceFiles/tests/test_sync_server.py b/sourceFiles/tests/test_sync_server.py index b174752..fc51150 100644 --- a/sourceFiles/tests/test_sync_server.py +++ b/sourceFiles/tests/test_sync_server.py @@ -17,7 +17,7 @@ import pytest -# A copy of the server, from PassMan-Server-Windows / PassMan-Server-Linux +# A copy of the server, from SecretSpace-Server-Windows / SecretSpace-Server-Linux # (sync_server/README.md says how to refresh it). sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "sync_server")) diff --git a/sourceFiles/tests/test_vault.py b/sourceFiles/tests/test_vault.py index 4302bda..23c8068 100644 --- a/sourceFiles/tests/test_vault.py +++ b/sourceFiles/tests/test_vault.py @@ -753,7 +753,7 @@ def test_csv_export_leaves_history_out_json_keeps_it(tmp_path): assert exported["accounts"][0]["history"][0]["password"] == "v1" -# --- installed copy: data in ~/.local/share/passman, not the program folder +# --- installed copy: data in ~/.local/share/secretspace, not the program folder def test_portable_copy_keeps_data_next_to_the_app(tmp_path, monkeypatch): monkeypatch.delenv("PASSMAN_DATA_DIR", raising=False) monkeypatch.setattr(vault, "get_app_dir", lambda: str(tmp_path / "app")) @@ -765,12 +765,12 @@ def test_portable_copy_keeps_data_next_to_the_app(tmp_path, monkeypatch): def test_installed_copy_keeps_data_in_xdg_data_home(tmp_path, monkeypatch): import appconfig monkeypatch.delenv("PASSMAN_DATA_DIR", raising=False) - app = tmp_path / "usr" / "lib" / "passman" + app = tmp_path / "usr" / "lib" / "secretspace" app.mkdir(parents=True) (app / vault.INSTALLED_MARKER).write_text("installed by the package") monkeypatch.setattr(vault, "get_app_dir", lambda: str(app)) monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "share")) - home = tmp_path / "share" / "passman" + home = tmp_path / "share" / "secretspace" assert vault.is_installed() assert vault.get_data_dir() == str(home / "vault") assert appconfig.config_file() == str(home / "config.json") @@ -789,7 +789,36 @@ def test_installed_without_xdg_data_home_uses_local_share(tmp_path, monkeypatch.setattr(vault.os.path, "expanduser", lambda path: path.replace("~", home, 1)) assert vault.get_home_dir() == str(tmp_path / "home" / ".local" / - "share" / "passman") + "share" / "secretspace") + + +def _installed(tmp_path, monkeypatch): + monkeypatch.delenv("PASSMAN_DATA_DIR", raising=False) + app = tmp_path / "usr" / "lib" / "secretspace" + app.mkdir(parents=True) + (app / vault.INSTALLED_MARKER).write_text("installed by the package") + monkeypatch.setattr(vault, "get_app_dir", lambda: str(app)) + monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "share")) + return tmp_path / "share" + + +def test_an_upgrade_from_passman_keeps_the_old_data_folder(tmp_path, monkeypatch): + """Installed as the passman package before the rename: the vaults stay + where they are, nothing is moved, no empty secretspace folder appears.""" + share = _installed(tmp_path, monkeypatch) + old = share / "passman" + (old / "vault").mkdir(parents=True) + (old / "vault" / "bob.vault.enc").write_bytes(b"x") + assert vault.get_data_dir() == str(old / "vault") + assert "bob" in vault.list_users() + assert not (share / "secretspace").exists() + + +def test_the_new_folder_wins_once_it_exists(tmp_path, monkeypatch): + share = _installed(tmp_path, monkeypatch) + (share / "passman").mkdir(parents=True) + (share / "secretspace").mkdir(parents=True) + assert vault.get_data_dir() == str(share / "secretspace" / "vault") @pytest.mark.skipif(os.name != "posix", reason="POSIX file modes") diff --git a/sourceFiles/tests/test_version.py b/sourceFiles/tests/test_version.py index 87dcb03..fb71afb 100644 --- a/sourceFiles/tests/test_version.py +++ b/sourceFiles/tests/test_version.py @@ -37,7 +37,7 @@ def test_version_string_is_prefixed(): def test_long_version_mentions_app_and_format(): text = version.long_version_string() - assert "PassMan" in text + assert "SecretSpace" in text assert version.version_string() in text assert version.VAULT_FORMAT in text diff --git a/sourceFiles/vault.py b/sourceFiles/vault.py index fc27ebf..3dbb2e3 100644 --- a/sourceFiles/vault.py +++ b/sourceFiles/vault.py @@ -92,7 +92,7 @@ def get_app_dir() -> str: """ if getattr(sys, "frozen", False): return os.path.dirname(os.path.abspath(sys.executable)) - # .../PassMan/sourceFiles/vault.py -> .../PassMan + # .../SecretSpace/sourceFiles/vault.py -> .../SecretSpace return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -112,18 +112,37 @@ def get_home_dir() -> str: Run from a checkout (no installed marker): the app folder, so the app and its data travel together, as on Windows. Installed: - $XDG_DATA_HOME/passman, by default ~/.local/share/passman, which survives - upgrades and removing the package and needs no write access to the - program folder. + $XDG_DATA_HOME/secretspace, by default ~/.local/share/secretspace, which + survives upgrades and removing the package and needs no write access to + the program folder -- or .../passman, see ``data_home``. """ if is_installed(): base = os.environ.get("XDG_DATA_HOME") or os.path.join( os.path.expanduser("~"), ".local", "share") - return os.path.join(base, "passman") + return data_home(base) return get_app_dir() -APP_TITLE = "PassMan - Password Manager" +#: The folder name before the rename to SecretSpace (2026-11). +OLD_HOME_NAME = "passman" +HOME_NAME = "secretspace" + + +def data_home(base: str) -> str: + """ + The data folder under ``base``: secretspace, unless only the folder of + the old name, passman, exists -- then that one, so an upgrade from the + passman package finds the vaults where they are. Nothing is moved or + renamed. The same rule as appdata_home in SecretSpace for Windows. + """ + new = os.path.join(base, HOME_NAME) + old = os.path.join(base, OLD_HOME_NAME) + if not os.path.isdir(new) and os.path.isdir(old): + return old + return new + + +APP_TITLE = "SecretSpace - Password Manager" def window_title() -> str: @@ -144,7 +163,7 @@ def get_data_dir() -> str: deliberately the ONLY place vaults are read from or written to — the user requires a single copy they control (no AppData mirrors, no automatic migration copies). Do not re-add code that copies vault files - elsewhere. An installed copy keeps its one copy in ~/.local/share/passman + elsewhere. An installed copy keeps its one copy in ~/.local/share/secretspace instead of next to the program; that is a different home, not a mirror. If a folder from before the rename (``data``) is present and ``vault`` is @@ -290,7 +309,7 @@ def validate_vault(data) -> dict: bare KeyError after an apparently successful unlock. """ if not isinstance(data, dict): - raise ValueError("This file is not a PassMan vault.") + raise ValueError("This file is not a SecretSpace vault.") accounts = data.get("accounts") if accounts is None: raise ValueError("This vault has no 'accounts' section; it may have " diff --git a/sourceFiles/version.py b/sourceFiles/version.py index 30c16d9..5c4ea63 100644 --- a/sourceFiles/version.py +++ b/sourceFiles/version.py @@ -6,13 +6,13 @@ A test version carries a pre-release tag, ``1.0.0-alpha.1``; the .deb spells it 1.0.0~alpha.1 (deb/build_deb.deb_version) so it sorts before -1.0.0. :func:`file_version` is the same as in PassMan for Windows, where it +1.0.0. :func:`file_version` is the same as in SecretSpace for Windows, where it gives the exe four plain numbers. """ import re -__version__ = "1.0.0-alpha.1" +__version__ = "1.0.0-alpha.2" #: The container a 3.0 account's vaults are written in (account_crypto). VAULT_FORMAT = "PMV3" @@ -26,7 +26,7 @@ def file_version(text: str = __version__) -> tuple: """(1, 0, 0, 1) for '1.0.0-alpha.1', (1, 0, 0, 0) for '1.0.0'.""" m = _VERSION.fullmatch(text) if not m: - raise ValueError(f"Not a PassMan version: {text!r}") + raise ValueError(f"Not a SecretSpace version: {text!r}") return tuple(int(g or 0) for g in m.groups()) @@ -36,6 +36,6 @@ def version_string() -> str: def long_version_string() -> str: - """e.g. 'PassMan for Linux v1.0.0 · vault format PMV3' — for the + """e.g. 'SecretSpace for Linux v1.0.0 · vault format PMV3' — for the Settings footer.""" - return f"PassMan for Linux {version_string()} · vault format {VAULT_FORMAT}" + return f"SecretSpace for Linux {version_string()} · vault format {VAULT_FORMAT}" diff --git a/tools/smoke_gui.py b/tools/smoke_gui.py index 1ff0240..1ae073e 100644 --- a/tools/smoke_gui.py +++ b/tools/smoke_gui.py @@ -1,5 +1,5 @@ """ -Start the real app on a display, unlock a throwaway PassMan 3 account, copy +Start the real app on a display, unlock a throwaway SecretSpace 3 account, copy a password, and save screenshots -- the check that the unit tests cannot make. @@ -13,9 +13,9 @@ python3 -I tools/smoke_gui.py OUT_DIR --installed -runs the copy the .deb installed in /usr/lib/passman, with its bundled +runs the copy the .deb installed in /usr/lib/secretspace, with its bundled libraries, instead of the checkout, and also checks that it knows it is -installed (data in ~/.local/share/passman). +installed (data in ~/.local/share/secretspace). """ import argparse @@ -25,7 +25,7 @@ import traceback HERE = os.path.dirname(os.path.abspath(__file__)) -INSTALLED_DIR = "/usr/lib/passman" +INSTALLED_DIR = "/usr/lib/secretspace" _parser = argparse.ArgumentParser() _parser.add_argument("out_dir", nargs="?", default="smoke-out") @@ -173,8 +173,8 @@ def check_installed(): ctk.__file__.startswith(os.path.join(INSTALLED_DIR, "lib") + os.sep)) check("knows it is installed", vault.is_installed()) base = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share") - check("data in ~/.local/share/passman", - vault.get_home_dir() == os.path.join(base, "passman")) + check("data in ~/.local/share/secretspace", + vault.get_home_dir() == os.path.join(base, "secretspace")) if __name__ == "__main__":