diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 056b862..83b7492 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ permissions: jobs: pytest: - # Windows, because that is what PassMan ships for: the paths, the + # Windows, because that is what SecretSpace ships for: the paths, the # installer marker and the tkinter build all match what users run. runs-on: windows-latest strategy: diff --git a/.gitignore b/.gitignore index 0f559b9..43c2a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,14 +24,17 @@ data/ *.vault.enc.base config.json -# What the PassMan Builder writes here: the installer it packs from -# installer\PassMan.iss, and the builder itself (py builder\build.py +# What the SecretSpace Builder writes here: the installer it packs from +# installer\SecretSpace.iss, and the builder itself (py builder\build.py # --builder). Publish them as release assets rather than committing them. # PassMan.exe is the portable exe of 2.2 and earlier, no longer built; still # ignored in case an old copy is around. +SecretSpace.exe PassMan.exe PassMan-Client-Installer.exe PassMan-Builder.exe +SecretSpace-Client-Installer.exe +SecretSpace-Builder.exe # Working notes, kept locally and out of the public repository. TODO.txt diff --git a/README.md b/README.md index d34f77a..6801a73 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,37 @@ -# PassMan for Windows +# SecretSpace for Windows A local desktop password manager for Windows. 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. -PassMan lives in four repositories: +**Formerly PassMan.** The project was renamed to SecretSpace in November +2026, because another password manager already uses the name PassMan. +Releases up to 2.11.0 and 3.0.0-alpha.1 still carry the old name: their +installer is `PassMan-Client-Installer.exe`, the program `PassMan.exe`, and +the data folder `%APPDATA%\PassMan`. Upgrading from them is an ordinary +upgrade: the installer replaces the PassMan program and shortcuts with +SecretSpace's, and your vaults stay in `%APPDATA%\PassMan`, which +SecretSpace keeps using (nothing is moved or renamed). The vault files are +the same. + +SecretSpace lives in four repositories: | Repository | What it holds | |---|---| -| **PassMan-Client-Windows** (this one) | the app for Windows, and its installer | -| [PassMan-Client-Linux](https://github.com/usermaneGermane/PassMan-Client-Linux) | the app for Linux — from source or a .deb package | -| [PassMan-Server-Windows](https://github.com/usermaneGermane/PassMan-Server-Windows) | the sync server for Windows, and its installer | -| [PassMan-Server-Linux](https://github.com/usermaneGermane/PassMan-Server-Linux) | the sync server for Linux (systemd) | +| **SecretSpace-Client-Windows** (this one) | the app for Windows, and its installer | +| [SecretSpace-Client-Linux](https://github.com/usermaneGermane/SecretSpace-Client-Linux) | the app for Linux — from source or a .deb package | +| [SecretSpace-Server-Windows](https://github.com/usermaneGermane/SecretSpace-Server-Windows) | the sync server for Windows, and its installer | +| [SecretSpace-Server-Linux](https://github.com/usermaneGermane/SecretSpace-Server-Linux) | the sync server for Linux (systemd) | -![PassMan](sourceFiles/PassMan-logo.png) +![SecretSpace](sourceFiles/SecretSpace-logo.png) -## PassMan 3 — a test version, not for real passwords yet +## SecretSpace 3 — a test version, not for real passwords yet -`main` is now **3.0.0-alpha.1**, the first step of PassMan 3. **For your real -passwords keep using [2.11.0](https://github.com/usermaneGermane/PassMan-Client-Windows/releases/tag/v2.11.0)**, +`main` is now **3.0.0-alpha.2**, a test step of SecretSpace 3. **For your real +passwords keep using [2.11.0](https://github.com/usermaneGermane/SecretSpace-Client-Windows/releases/tag/v2.11.0)**, which the rest of this README describes. Do not install the test version -over it: it opens PassMan 3 accounts only, so your 2.x vaults would not +over it: it opens SecretSpace 3 accounts only, so your 2.x vaults would not show up (they are not changed or deleted — 2.11.0 opens them again). What the test version has: @@ -46,7 +56,7 @@ What the test version has: Not in this test version yet: **sync** (the next one adds server accounts), recovering an account with the 12-word phrase (the phrase is made and checked now; the screen that uses it comes later), and sharing a vault with -other people. PassMan for Linux has the same test version, 1.0.0-alpha.1. +other people. SecretSpace for Linux has the same test version, 1.0.0-alpha.1. Coming next, in this order: sync with accounts on the server, several vaults per account, sharing a vault with family or colleagues, and recovering an account with the 12-word phrase. @@ -54,30 +64,31 @@ recovering an account with the 12-word phrase. ## Running it The repository does not contain the app itself. Download -`PassMan-Client-Installer.exe` from the release marked **Latest** on the -[Releases](https://github.com/usermaneGermane/PassMan-Client-Windows/releases) page (a +`SecretSpace-Client-Installer.exe` from the release marked **Latest** on the +[Releases](https://github.com/usermaneGermane/SecretSpace-Client-Windows/releases) page (a *Pre-release* is a test version and has the same file name), or build it -yourself with the [PassMan Builder](#building-it-yourself-windows). The +yourself with the [SecretSpace Builder](#building-it-yourself-windows). The installer is all you need: it brings Python and every library with it, so installing needs no source code and no Python. -**`PassMan-Client-Installer.exe`** installs it. Run it and choose a folder -(by default `%LOCALAPPDATA%\Programs\PassMan`) and whether you want a +**`SecretSpace-Client-Installer.exe`** installs it. Run it and choose a folder +(by default `%LOCALAPPDATA%\Programs\SecretSpace`) and whether you want a desktop shortcut. It needs Windows 10 or 11 (64-bit) and installs for your Windows user only, so it needs no admin rights. You get a Start Menu entry and an uninstaller under Settings → Apps; the program it installs is -`PassMan.exe`. The vault lives in **`%APPDATA%\PassMan`**, not in the -program folder, so upgrading or uninstalling never touches it. +`SecretSpace.exe`. The vault lives in **`%APPDATA%\SecretSpace`**, not in the +program folder, so upgrading or uninstalling never touches it. A copy first +installed as PassMan keeps `%APPDATA%\PassMan` instead. If you used the portable `PassMan.exe` of 2.2 or earlier, the installer can move your vaults across. They are moved, not copied, and a vault already in -`%APPDATA%\PassMan` is never overwritten. The portable single-file exe is +`%APPDATA%\SecretSpace` is never overwritten. The portable single-file exe is no longer made: it unpacked itself to a temporary folder on every launch, which made it slow to start. The optional sync server is a separate download from its own repository: -**`PassMan-Server-Installer.exe`** from [PassMan-Server-Windows](https://github.com/usermaneGermane/PassMan-Server-Windows), or a Python script and a -systemd unit from [PassMan-Server-Linux](https://github.com/usermaneGermane/PassMan-Server-Linux). The app never needs it, and it never needs the +**`SecretSpace-Server-Installer.exe`** from [SecretSpace-Server-Windows](https://github.com/usermaneGermane/SecretSpace-Server-Windows), or a Python script and a +systemd unit from [SecretSpace-Server-Linux](https://github.com/usermaneGermane/SecretSpace-Server-Linux). The app never needs it, and it never needs the app. To run from source: @@ -90,15 +101,15 @@ python sourceFiles/gui.py ## Building it yourself (Windows) Building needs a checkout of this repository: the builder looks for the -source next to itself and builds from it. Run **`PassMan-Builder.exe`** +source next to itself and builds from it. Run **`SecretSpace-Builder.exe`** from the project folder. It is not on the Releases page (it is only useful inside a checkout); make it once with `py builder\build.py --builder`, see below. It shows a checklist of what the build needs: -- the PassMan source files +- the SecretSpace source files - Python 3 and its `tkinter` -- PassMan's libraries (`requirements.txt`) +- SecretSpace's libraries (`requirements.txt`) - PyInstaller - that the app imports cleanly - Inno Setup 6, for the installer @@ -114,14 +125,14 @@ it is reported missing, re-run the Python installer, choose **Modify**, and tick **tcl/tk and IDLE**. When every line is ✓, press **Build**. About a minute later the project -folder holds the installer, `PassMan-Client-Installer.exe`. The version +folder holds the installer, `SecretSpace-Client-Installer.exe`. The version comes from `sourceFiles/version.py`. The builder does not compile anything itself. It drives the Python installed on the PC, which is why Python is on the checklist. It prefers the `py` launcher's newest Python 3 over a bare `python` on `PATH`, which is often a virtualenv or the Microsoft Store placeholder. It never touches -an installed or running PassMan: everything it writes is in the project +an installed or running SecretSpace: everything it writes is in the project folder. The same steps run in a terminal, without the window: @@ -129,7 +140,7 @@ The same steps run in a terminal, without the window: ``` py builder\build.py check, then build py builder\build.py --install-missing install what is missing first -py builder\build.py --builder build PassMan-Builder.exe itself +py builder\build.py --builder build SecretSpace-Builder.exe itself ``` The last line is how the builder is made in the first place, so a fresh @@ -143,7 +154,7 @@ Copy the **whole project folder** to the other PC. The build needs: ``` requirements.txt sourceFiles\ (all of it) -installer\ (for PassMan-Client-Installer.exe) +installer\ (for SecretSpace-Client-Installer.exe) ``` `README.md` is not needed to build, and neither are the `tests` folders. @@ -157,32 +168,32 @@ that while the error is still readable. The installer is self-contained: what it installs bundles Python and every library, so the target PC does not need Python installed to *run* -PassMan. Python is only needed to *build* it. +SecretSpace. Python is only needed to *build* it. The exe is not code-signed, so Windows SmartScreen will likely warn the first time — "More info" then "Run anyway". Your passwords do **not** travel with the installer. The vault lives in -`%APPDATA%\PassMan`, so a fresh install on a new PC starts empty. To bring +`%APPDATA%\SecretSpace`, so a fresh install on a new PC starts empty. To bring your logins across, use *Restore from server* if you sync, or copy -`vault\.vault.enc` into `%APPDATA%\PassMan\vault` on the new PC, or +`vault\.vault.enc` into `%APPDATA%\SecretSpace\vault` on the new PC, or use Settings → *Encrypted backup* → *Save backup…* and copy that file over. The backup is still protected by your master password. ## A lab copy for testing -The installed copy is your production PassMan: its vaults are in -`%APPDATA%\PassMan`. Only one copy can be installed — a second run of the +The installed copy is your production SecretSpace: its vaults are in +`%APPDATA%\SecretSpace`. Only one copy can be installed — a second run of the installer upgrades the first — so a copy for trying things out runs *without* the installer, and keeps its vaults and `config.json` in its own folder instead: -1. Build (`py builder\build.py`, or PassMan Builder). -2. Copy the folder `build\installer\PassMan` somewhere of its own, e.g. - `D:\PassMan-Lab`, and run `PassMan.exe` from there. +1. Build (`py builder\build.py`, or SecretSpace Builder). +2. Copy the folder `build\installer\SecretSpace` somewhere of its own, e.g. + `D:\SecretSpace-Lab`, and run `SecretSpace.exe` from there. That folder has no `installed` file, so it never touches -`%APPDATA%\PassMan`. Its window title says +`%APPDATA%\SecretSpace`. Its window title says `[LAB - ]`; the installed copy's title never does. Do not copy the `installed` file into it. @@ -190,7 +201,7 @@ To update the lab copy after a new build, close it and copy the new program files over the old ones, leaving its `vault` folder and `config.json` alone: ``` -robocopy build\installer\PassMan D:\PassMan-Lab /E /XD vault /XF config.json +robocopy build\installer\SecretSpace D:\SecretSpace-Lab /E /XD vault /XF config.json ``` (`/E` adds and replaces files but deletes nothing.) @@ -201,12 +212,12 @@ your real vaults do not use** (e.g. `test`, `lab-alice`): a lab vault with the same name as a real one would sync into it. **Moving test vaults out of production.** Uninstalling keeps the vaults, so -`%APPDATA%\PassMan\vault` may still hold vaults you made while testing. With -PassMan closed, move each `.vault.enc` together with its `.bak` and +`%APPDATA%\SecretSpace\vault` may still hold vaults you made while testing. With +SecretSpace closed, move each `.vault.enc` together with its `.bak` and `.base` into the lab copy's `vault` folder (and `config.json` too, if it names a test vault as the last user). -### PassMan 3 (test version) +### SecretSpace 3 (test version) ``` keyset.pmk: base64( "PMK3" || ver(1) || kdf(1) || memory_kib(4) || passes(1) || lanes(1) || salt(16) || nonce(12) || ciphertext ) @@ -222,10 +233,10 @@ cannot be swapped in for another's. ## Project layout ``` -PassMan-Client-Windows/ -├── builder/ PassMan Builder: build.py, its window, its spec -├── PassMan-Builder.exe the builder (built, not committed) -├── PassMan-Client-Installer.exe installs the app (built, not committed) +SecretSpace-Client-Windows/ +├── builder/ SecretSpace Builder: build.py, its window, its spec +├── SecretSpace-Builder.exe the builder (built, not committed) +├── SecretSpace-Client-Installer.exe installs the app (built, not committed) ├── vault/ vaults when running from source - never committed ├── installer/ the Inno Setup script for the installer └── sourceFiles/ the program itself @@ -248,7 +259,7 @@ PassMan-Client-Windows/ ├── lockout.py rate limiting of master-password attempts ├── password_generator.py ├── version.py the version shown in the UI - ├── PassMan-installer.spec the PyInstaller build the installer ships + ├── SecretSpace-installer.spec the PyInstaller build the installer ships ├── build_version.py the exe's version resource, read from version.py └── tests/ └── sync_server/ a copy of the server, for the end-to-end sync tests @@ -262,7 +273,7 @@ backup* if it has one. The upgrade is one-way. This version reads vaults written by older builds and transparently re-encrypts them at the stronger settings on first unlock — -but once that has happened, **an older PassMan.exe can no longer open the +but once that has happened, **an older SecretSpace.exe can no longer open the vault**. It reports "Decryption failed. Wrong password or corrupted data.", which looks alarmingly like a lost password even though the data is fine. @@ -279,11 +290,11 @@ So once any PC runs 2.0: - update **every** PC to 2.0 before using it, and keep no 1.x executable pointing at that vault folder; -- update the sync server too (see "Updating" in [PassMan-Server-Windows](https://github.com/usermaneGermane/PassMan-Server-Windows) or [PassMan-Server-Linux](https://github.com/usermaneGermane/PassMan-Server-Linux)). +- update the sync server too (see "Updating" in [SecretSpace-Server-Windows](https://github.com/usermaneGermane/SecretSpace-Server-Windows) or [SecretSpace-Server-Linux](https://github.com/usermaneGermane/SecretSpace-Server-Linux)). ## Syncing two PCs -Point both PCs at a server you run — [PassMan-Server-Windows](https://github.com/usermaneGermane/PassMan-Server-Windows) or [PassMan-Server-Linux](https://github.com/usermaneGermane/PassMan-Server-Linux) says how to set one +Point both PCs at a server you run — [SecretSpace-Server-Windows](https://github.com/usermaneGermane/SecretSpace-Server-Windows) or [SecretSpace-Server-Linux](https://github.com/usermaneGermane/SecretSpace-Server-Linux) says how to set one up — then press **Sync**. The vault is encrypted before it leaves your machine, so the server stores ciphertext it has no key for and never sees your master password. @@ -299,7 +310,7 @@ your master password. down. After that, Sync on either machine. **Sync when I unlock.** Sync is manual unless you switch this on, in -Settings → Sync once sync is set up (off by default). With it on, PassMan +Settings → Sync once sync is set up (off by default). With it on, SecretSpace syncs right after you unlock; a conflict still waits for your choice, and a server that cannot be reached only shows a warning — the vault opens either way. The switch is per vault and per PC: a merge keeps this PC's choice, @@ -312,7 +323,7 @@ side by side, and **whichever side you do not keep is saved in that entry's history** — every outcome is reversible with one click on *Revert*. Cancelling it changes nothing on either side. -**More than one vault.** Each PassMan user syncs as its own, independent vault +**More than one vault.** Each SecretSpace user syncs as its own, independent vault on the server, matched across PCs by username (exactly, including case). Set up sync once per vault, while that vault is unlocked; Sync only ever syncs the vault that is open. @@ -407,7 +418,7 @@ History syncs, and never causes a conflict: each PC ends up with the combined history from both sides. When you settle a sync conflict, the version you do not keep becomes the entry's newest revision. -**Update every PC** before relying on it: an older PassMan does not know the +**Update every PC** before relying on it: an older SecretSpace does not know the field, drops it when it saves, and that loss would then sync. The server needs no update for this. @@ -453,7 +464,8 @@ drive or in cloud storage. ## Where your data lives -The files below are in `%APPDATA%\PassMan`. An installed copy is +The files below are in `%APPDATA%\SecretSpace` (in `%APPDATA%\PassMan` if +that folder was there first, from before the rename). An installed copy is recognised by the `installed` file that the installer puts next to its exe; without it — running from source, or a portable exe from 2.2 or earlier — they are next to the program instead. @@ -484,13 +496,13 @@ is encrypted with the same master password as the vault. Deleting it is safe master password deletes it automatically, since it cannot be re-encrypted without the old one. -This folder used to be called `data`. If you already have one, PassMan keeps +This folder used to be called `data`. If you already have one, SecretSpace keeps using it as-is and creates nothing new — rename it to `vault` yourself when convenient, while the app is closed. Vault writes go to a temporary file and are then atomically renamed into place, so an interrupted save (full disk, power cut) cannot corrupt the vault. -If the main file is ever damaged, PassMan falls back to the `.bak` copy. +If the main file is ever damaged, SecretSpace falls back to the `.bak` copy. ## Vault format @@ -517,7 +529,7 @@ Worth knowing about, in the interest of not overselling this: - **The unlocked vault lives in memory in the clear**, and Python strings cannot be reliably wiped. Auto-lock limits the window; it does not close it. - **Locking does not clear a copied password.** It stays pasteable until - the 30-second auto-clear, so you can lock and then paste. Closing PassMan + the 30-second auto-clear, so you can lock and then paste. Closing SecretSpace still clears it at once, since nothing would be left to clear it later. - **Clipboard auto-clear only clears the live clipboard.** Windows clipboard history (Win+V) and cloud clipboard sync keep copies no application can @@ -528,7 +540,7 @@ Worth knowing about, in the interest of not overselling this: codes are made on this PC (RFC 6238); scanning a QR code is not supported yet, so paste the secret shown under it. - **Card details, custom fields and attachments** in an export are not - imported; PassMan warns when it sees them. + imported; SecretSpace warns when it sees them. - **The unlock delay is a speed bump, not a boundary.** It slows guessing at the login screen and survives a restart, but anyone who can copy the vault file can attack it offline with no limit at all. Deleting `config.json` @@ -591,12 +603,12 @@ every pull request and every push to `main` ## License -PassMan (this client) is free software under the **GNU General Public +SecretSpace (this client) 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 8acfcab..3498884 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # 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 @@ -23,5 +23,5 @@ Please give a reasonable 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 installer and the -sync client. Server issues go to the PassMan-Server-Windows or -PassMan-Server-Linux repository, the same way. +sync client. Server issues go to the SecretSpace-Server-Windows or +SecretSpace-Server-Linux repository, the same way. diff --git a/builder/PassMan-Builder.spec b/builder/SecretSpace-Builder.spec similarity index 70% rename from builder/PassMan-Builder.spec rename to builder/SecretSpace-Builder.spec index 9d9f80c..9c9919d 100644 --- a/builder/PassMan-Builder.spec +++ b/builder/SecretSpace-Builder.spec @@ -1,9 +1,9 @@ # -*- mode: python ; coding: utf-8 -*- # -# PassMan-Builder.exe: build.py + builder_gui.py frozen into one file that -# checks the prerequisites and builds PassMan. Built with: +# SecretSpace-Builder.exe: build.py + builder_gui.py frozen into one file that +# checks the prerequisites and builds SecretSpace. Built with: # py builder\build.py --builder -# It borrows PassMan's theme and widgets from sourceFiles/ for its look. +# It borrows SecretSpace's theme and widgets from sourceFiles/ for its look. import os import sys @@ -14,8 +14,8 @@ SRC = os.path.join(os.path.dirname(HERE), 'sourceFiles') sys.path.insert(0, SRC) from build_version import version_info # Properties > Details -datas = [(os.path.join(SRC, 'PassMan-logo.png'), '.'), - (os.path.join(SRC, 'PassMan-logo.ico'), '.')] +datas = [(os.path.join(SRC, 'SecretSpace-logo.png'), '.'), + (os.path.join(SRC, 'SecretSpace-logo.ico'), '.')] binaries = [] hiddenimports = [] tmp_ret = collect_all('customtkinter') @@ -43,7 +43,7 @@ exe = EXE( a.binaries, a.datas, [], - name='PassMan-Builder', + name='SecretSpace-Builder', debug=False, bootloader_ignore_signals=False, strip=False, @@ -55,6 +55,6 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, - icon=[os.path.join(SRC, 'PassMan-logo.ico')], - version=version_info("PassMan Builder", "PassMan-Builder.exe"), + icon=[os.path.join(SRC, 'SecretSpace-logo.ico')], + version=version_info("SecretSpace Builder", "SecretSpace-Builder.exe"), ) diff --git a/builder/build.py b/builder/build.py index e4bcb9f..bd2963a 100644 --- a/builder/build.py +++ b/builder/build.py @@ -1,14 +1,14 @@ """ -Build PassMan-Client-Installer.exe from the source, after checking every +Build SecretSpace-Client-Installer.exe from the source, after checking every prerequisite. This replaces build.bat. The sync server is built from its own -repository (PassMan-Server-Windows). +repository (SecretSpace-Server-Windows). Two ways to run it: - PassMan-Builder.exe double-click; a window walks through it + SecretSpace-Builder.exe double-click; a window walks through it py builder\\build.py the same steps in a terminal -PassMan-Builder.exe is this module frozen with builder_gui.py. It does not +SecretSpace-Builder.exe is this module frozen with builder_gui.py. It does not compile anything itself -- a frozen exe cannot run PyInstaller on the source -- it drives a real Python installed on the PC, the way build.bat did. Everything here is plain functions over an injectable command runner, @@ -34,10 +34,10 @@ "requirements.txt", os.path.join("sourceFiles", "gui.py"), os.path.join("sourceFiles", "version.py"), - os.path.join("sourceFiles", "PassMan-installer.spec"), - os.path.join("sourceFiles", "PassMan-logo.png"), - os.path.join("sourceFiles", "PassMan-logo.ico"), - os.path.join("installer", "PassMan.iss"), + os.path.join("sourceFiles", "SecretSpace-installer.spec"), + os.path.join("sourceFiles", "SecretSpace-logo.png"), + os.path.join("sourceFiles", "SecretSpace-logo.ico"), + os.path.join("installer", "SecretSpace.iss"), os.path.join("installer", "installed"), ) @@ -83,7 +83,7 @@ def run(cmd, cwd=None, env=None, on_line=None) -> Result: # --------------------------------------------------------------------------- def project_dir() -> str: """ - The PassMan folder: the one holding PassMan-Builder.exe when frozen, + The SecretSpace folder: the one holding SecretSpace-Builder.exe when frozen, else the parent of builder/ where this file lives. """ if getattr(sys, "frozen", False): @@ -115,7 +115,7 @@ def file_version(version: str) -> str: m = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:-(?:alpha|beta|rc)\.(\d+))?", version) if not m: - raise ValueError(f"Not a PassMan version: {version!r}") + raise ValueError(f"Not a SecretSpace version: {version!r}") return ".".join(g or "0" for g in m.groups()) @@ -183,7 +183,7 @@ def check_all(project: str, runner=run, find_iscc=find_iscc) -> list: checks = [] gone = missing_files(project) checks.append(Check( - "PassMan source files", not gone, + "SecretSpace source files", not gone, "all present" if not gone else "missing: " + ", ".join(gone) + ". Copy the whole project folder.", None if not gone else "manual")) @@ -191,7 +191,7 @@ def check_all(project: str, runner=run, find_iscc=find_iscc) -> list: py = find_python(runner) if py is None: checks.append(Check("Python 3", False, "not found", "winget-python")) - for name in ("tkinter", "PassMan's libraries", "PyInstaller", + for name in ("tkinter", "SecretSpace's libraries", "PyInstaller", "The app imports cleanly"): checks.append(Check(name, False, "needs Python first")) else: @@ -208,7 +208,7 @@ def check_all(project: str, runner=run, find_iscc=find_iscc) -> list: missing = [pip for mod, pip in APP_PACKAGES if runner(py.cmd + ["-c", f"import {mod}"]).code != 0] checks.append(Check( - "PassMan's libraries", not missing, + "SecretSpace's libraries", not missing, "ok" if not missing else "missing: " + ", ".join(missing), None if not missing else "pip", missing)) @@ -257,10 +257,10 @@ def fix_commands(checks, project: str, py: "Python" = None) -> list: elif c.fix == "winget-inno": out.append(("Install Inno Setup 6", winget + [WINGET_INNO])) elif c.fix == "pip" and py is not None: - if c.name == "PassMan's libraries": + if c.name == "SecretSpace's libraries": cmd = py.cmd + ["-m", "pip", "install", "-r", os.path.join(project, "requirements.txt")] - out.append(("Install PassMan's libraries", cmd)) + out.append(("Install SecretSpace's libraries", cmd)) else: out.append((f"Install {', '.join(c.packages)}", py.cmd + ["-m", "pip", "install"] + c.packages)) @@ -280,14 +280,17 @@ def remove_previous(project: str) -> None: successful one because last time's files are still there.""" import shutil # PassMan.exe here is the portable exe of 2.2 and earlier, no longer - # built: removed so it cannot pass for a current build. - for name in ("PassMan.exe", "PassMan-Client-Installer.exe"): + # built, and the PassMan-* names are those from before the rename to + # SecretSpace: removed so none of them can pass for a current build. + for name in ("SecretSpace.exe", "SecretSpace-Client-Installer.exe", + "PassMan.exe", "PassMan-Client-Installer.exe"): path = os.path.join(project, name) if os.path.exists(path): os.remove(path) - folder = os.path.join(project, "build", "installer", "PassMan") - if os.path.isdir(folder): - shutil.rmtree(folder) + for folder in (os.path.join(project, "build", "installer", "SecretSpace"), + os.path.join(project, "build", "installer", "PassMan")): + if os.path.isdir(folder): + shutil.rmtree(folder) def build_steps(project: str, py: "Python", iscc: str, version: str) -> list: @@ -300,14 +303,14 @@ def build_steps(project: str, py: "Python", iscc: str, version: str) -> list: "--distpath", os.path.join(project, "build", "installer"), "--workpath", os.path.join(project, "build", "installer-work"), - os.path.join(src, "PassMan-installer.spec")], - os.path.join(project, "build", "installer", "PassMan", - "PassMan.exe")), - ("Packing PassMan-Client-Installer.exe", + os.path.join(src, "SecretSpace-installer.spec")], + os.path.join(project, "build", "installer", "SecretSpace", + "SecretSpace.exe")), + ("Packing SecretSpace-Client-Installer.exe", [iscc, "/Q", f"/DAppVersion={version}", f"/DFileVersion={file_version(version)}", - os.path.join(project, "installer", "PassMan.iss")], - os.path.join(project, "PassMan-Client-Installer.exe")), + os.path.join(project, "installer", "SecretSpace.iss")], + os.path.join(project, "SecretSpace-Client-Installer.exe")), ] @@ -315,7 +318,7 @@ def build(project: str, py: "Python", iscc: str, log=print, runner=run, exists=os.path.isfile) -> bool: """Run the build, logging as it goes. True when every output exists.""" version = app_version(project) - log(f"PassMan {version}") + log(f"SecretSpace {version}") remove_previous(project) for label, cmd, product in build_steps(project, py, iscc, version): log("") @@ -327,31 +330,31 @@ def build(project: str, py: "Python", iscc: str, log=print, runner=run, "The messages above give the reason.") return False log("") - log(f"DONE. Version {version}: PassMan-Client-Installer.exe") + log(f"DONE. Version {version}: SecretSpace-Client-Installer.exe") return True def build_builder(project: str, py: "Python", log=print, runner=run) -> bool: - """Freeze this module and builder_gui.py into PassMan-Builder.exe.""" - product = os.path.join(project, "PassMan-Builder.exe") + """Freeze this module and builder_gui.py into SecretSpace-Builder.exe.""" + product = os.path.join(project, "SecretSpace-Builder.exe") if os.path.exists(product): os.remove(product) res = runner(py.cmd + ["-m", "PyInstaller", "--noconfirm", "--distpath", project, "--workpath", os.path.join(project, "build"), os.path.join(project, "builder", - "PassMan-Builder.spec")], + "SecretSpace-Builder.spec")], cwd=project, on_line=log) ok = res.code == 0 and os.path.isfile(product) - log("DONE: PassMan-Builder.exe" if ok else - f"FAILED to build PassMan-Builder.exe (exit code {res.code})") + log("DONE: SecretSpace-Builder.exe" if ok else + f"FAILED to build SecretSpace-Builder.exe (exit code {res.code})") return ok # --------------------------------------------------------------------------- # Terminal use: py builder\build.py [--install-missing] [--builder] # --install-missing install what is missing, then build -# --builder build PassMan-Builder.exe instead of the app +# --builder build SecretSpace-Builder.exe instead of the app # --------------------------------------------------------------------------- def main(argv=None) -> int: argv = sys.argv[1:] if argv is None else argv diff --git a/builder/builder_gui.py b/builder/builder_gui.py index 11e0889..8ac3817 100644 --- a/builder/builder_gui.py +++ b/builder/builder_gui.py @@ -1,9 +1,9 @@ """ -PassMan Builder: the window around build.py. +SecretSpace Builder: the window around build.py. It shows every prerequisite as a checklist, offers to install what is missing (asking before each install), then builds -PassMan-Client-Installer.exe with a live log. All the work happens in build.py, on a background thread, so the +SecretSpace-Client-Installer.exe with a live log. All the work happens in build.py, on a background thread, so the window never freezes during a long build. """ @@ -13,7 +13,7 @@ import threading from tkinter import filedialog, messagebox -# Reuse PassMan's own look. When frozen, PyInstaller has bundled theme.py. +# Reuse SecretSpace's own look. When frozen, PyInstaller has bundled theme.py. if not getattr(sys, "frozen", False): sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname( os.path.abspath(__file__))), "sourceFiles")) @@ -37,10 +37,10 @@ def __init__(self, root, project): self.checks = [] self.python = None - root.title("PassMan Builder") + root.title("SecretSpace Builder") root.geometry("760x640") root.minsize(640, 520) - for path in asset_candidates("PassMan-logo.ico"): + for path in asset_candidates("SecretSpace-logo.ico"): if os.path.exists(path): try: root.iconbitmap(path) @@ -53,7 +53,7 @@ def __init__(self, root, project): head = ctk.CTkFrame(root, fg_color=HEADER_BG, corner_radius=0) head.grid(row=0, column=0, sticky="ew") - ctk.CTkLabel(head, text="PassMan Builder", font=F(20, "bold"), + ctk.CTkLabel(head, text="SecretSpace Builder", font=F(20, "bold"), text_color=GREEN).pack(anchor="w", padx=20, pady=(14, 0)) self.where = ctk.CTkLabel(head, text="", font=F(12), text_color=MUTED, anchor="w") @@ -236,7 +236,7 @@ def done(ok): def choose_folder(self): folder = filedialog.askdirectory( - parent=self.root, title="Choose the PassMan project folder", + parent=self.root, title="Choose the SecretSpace project folder", initialdir=self.project) if folder: self.project = os.path.normpath(folder) diff --git a/builder/tests/test_build.py b/builder/tests/test_build.py index a6b9dd8..f2e8043 100644 --- a/builder/tests/test_build.py +++ b/builder/tests/test_build.py @@ -72,7 +72,7 @@ def test_missing_libraries_are_installed_from_requirements(): runner = healthy(**{"import customtkinter": build.Result(1, ""), "import PIL": build.Result(1, "")}) checks = build.check_all(PROJECT, runner, find_iscc=has_iscc) - libs = checks_by_name(checks)["PassMan's libraries"] + libs = checks_by_name(checks)["SecretSpace's libraries"] assert not libs.ok and libs.fix == "pip" assert libs.detail == "missing: customtkinter, Pillow" [(label, cmd)] = build.fix_commands(checks, PROJECT, @@ -125,7 +125,7 @@ def test_a_broken_source_file_blocks_the_build(): def test_an_incomplete_folder_is_reported(tmp_path): checks = build.check_all(str(tmp_path), healthy(), find_iscc=has_iscc) - files = checks_by_name(checks)["PassMan source files"] + files = checks_by_name(checks)["SecretSpace source files"] assert not files.ok and "Copy the whole project folder" in files.detail @@ -167,7 +167,7 @@ def project(tmp_path): (tmp_path / "sourceFiles").mkdir() (tmp_path / "sourceFiles" / "version.py").write_text( '__version__ = "9.8.7"\n') - (tmp_path / "PassMan.exe").write_text("last time's build") + (tmp_path / "SecretSpace.exe").write_text("last time's build") return str(tmp_path) @@ -177,8 +177,8 @@ def test_build_runs_the_steps_in_order(project): assert build.build(project, py, r"C:\Inno\ISCC.exe", log=lambda s: None, runner=runner, exists=lambda p: True) cmds = [" ".join(c) for c, _ in runner.calls] - assert "PassMan-installer.spec" in cmds[0] - assert "/DAppVersion=9.8.7" in cmds[1] and "PassMan.iss" in cmds[1] + assert "SecretSpace-installer.spec" in cmds[0] + assert "/DAppVersion=9.8.7" in cmds[1] and "SecretSpace.iss" in cmds[1] assert "/DFileVersion=9.8.7.0" in cmds[1] assert len(cmds) == 2 @@ -188,7 +188,7 @@ def test_the_portable_exe_is_no_longer_built(project): build.build(project, build.Python(["py"], "3", "p"), "iscc", log=lambda s: None, runner=runner, exists=lambda p: True) cmds = [" ".join(c) for c, _ in runner.calls] - assert not any(c.endswith("PassMan.spec") for c in cmds) + assert not any(c.endswith("SecretSpace.spec") for c in cmds) assert not any("--distpath " + project + " " in c for c in cmds) @@ -196,7 +196,7 @@ def test_only_the_app_is_built(): """The server has its own repository and its own build.""" steps = build.build_steps("P", build.Python(["py"], "3", "p"), "iscc", "1.0.0") products = [os.path.basename(product) for _, _, product in steps] - assert products == ["PassMan.exe", "PassMan-Client-Installer.exe"] + assert products == ["SecretSpace.exe", "SecretSpace-Client-Installer.exe"] def test_old_outputs_are_removed_before_building(project): @@ -204,11 +204,11 @@ def test_old_outputs_are_removed_before_building(project): log=lambda s: None, runner=FakeRunner(), exists=lambda p: True) # A portable exe left by 2.2 or earlier must not pass for a new build. - assert not os.path.exists(os.path.join(project, "PassMan.exe")) + assert not os.path.exists(os.path.join(project, "SecretSpace.exe")) def test_an_old_installer_is_removed_too(project): - old = os.path.join(project, "PassMan-Client-Installer.exe") + old = os.path.join(project, "SecretSpace-Client-Installer.exe") with open(old, "w") as f: f.write("last time's build") build.remove_previous(project) @@ -216,7 +216,7 @@ def test_an_old_installer_is_removed_too(project): def test_build_stops_at_the_first_failure(project): - runner = FakeRunner({"PassMan-installer.spec": build.Result(1, "boom")}) + runner = FakeRunner({"SecretSpace-installer.spec": build.Result(1, "boom")}) lines = [] ok = build.build(project, build.Python(["py"], "3", "p"), "iscc", log=lines.append, runner=runner, exists=lambda p: True) diff --git a/installer/PassMan.iss b/installer/SecretSpace.iss similarity index 75% rename from installer/PassMan.iss rename to installer/SecretSpace.iss index 02ffc74..d482e37 100644 --- a/installer/PassMan.iss +++ b/installer/SecretSpace.iss @@ -1,18 +1,19 @@ -; PassMan installer - compiled by the PassMan Builder (builder\build.py) +; SecretSpace installer - compiled by the SecretSpace Builder (builder\build.py) ; with Inno Setup 6: -; ISCC.exe /DAppVersion=2.1.0 installer\PassMan.iss +; ISCC.exe /DAppVersion=2.1.0 installer\SecretSpace.iss ; ; Installs for the current user only (no admin prompt), by default into -; %LOCALAPPDATA%\Programs\PassMan, with a Start Menu entry, an optional +; %LOCALAPPDATA%\Programs\SecretSpace, with a Start Menu entry, an optional ; desktop shortcut and an uninstaller in Settings > Apps. ; ; The vaults are NOT in the program folder. The "installed" marker file makes -; PassMan keep them in %APPDATA%\PassMan (see vault.get_home_dir), so an -; upgrade or an uninstall never touches them. +; SecretSpace keep them in %APPDATA%\SecretSpace (see vault.get_home_dir), so an +; upgrade or an uninstall never touches them. A copy installed before the +; rename (as PassMan) keeps %APPDATA%\PassMan: see DataHome below. ; ; Silent install, e.g. for testing: -; PassMan-Client-Installer.exe /VERYSILENT /DIR="C:\somewhere" /TASKS="desktopicon" -; /MOVEVAULTS="E:\old\PassMan" +; SecretSpace-Client-Installer.exe /VERYSILENT /DIR="C:\somewhere" /TASKS="desktopicon" +; /MOVEVAULTS="E:\old\SecretSpace" #ifndef AppVersion #define AppVersion "0.0.0" @@ -26,11 +27,11 @@ [Setup] ; Never change AppId: it is how an upgrade finds the copy it replaces. AppId={{2142690B-93AA-41F8-9056-BE62EE0426B4} -AppName=PassMan +AppName=SecretSpace AppVersion={#AppVersion} -AppVerName=PassMan {#AppVersion} +AppVerName=SecretSpace {#AppVersion} VersionInfoVersion={#FileVersion} -DefaultDirName={autopf}\PassMan +DefaultDirName={autopf}\SecretSpace DisableProgramGroupPage=yes PrivilegesRequired=lowest ; Windows 10 and 11 only (11 reports itself as 10.0.22000+). Older Windows @@ -39,14 +40,14 @@ MinVersion=10.0 ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible OutputDir=.. -OutputBaseFilename=PassMan-Client-Installer -SetupIconFile=..\sourceFiles\PassMan-logo.ico -UninstallDisplayIcon={app}\PassMan.exe -UninstallDisplayName=PassMan +OutputBaseFilename=SecretSpace-Client-Installer +SetupIconFile=..\sourceFiles\SecretSpace-logo.ico +UninstallDisplayIcon={app}\SecretSpace.exe +UninstallDisplayName=SecretSpace WizardStyle=modern Compression=lzma2 SolidCompression=yes -; Close a running PassMan before replacing its files. +; Close a running SecretSpace before replacing its files. CloseApplications=yes ; The vault move below writes to the user's own profile on purpose. UsedUserAreasWarning=no @@ -57,26 +58,41 @@ Name: "desktopicon"; Description: "Create a &desktop shortcut"; GroupDescription [InstallDelete] ; An upgrade must not leave the previous version's libraries behind. Type: filesandordirs; Name: "{app}\_internal" +; Nor the program and shortcuts of the name before SecretSpace. An upgrade +; keeps the folder it was installed in, even if that folder says PassMan. +Type: files; Name: "{app}\PassMan.exe" +Type: files; Name: "{autoprograms}\PassMan.lnk" +Type: files; Name: "{autodesktop}\PassMan.lnk" [Files] -Source: "..\build\installer\PassMan\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "..\build\installer\SecretSpace\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "installed"; DestDir: "{app}"; Flags: ignoreversion [Icons] -Name: "{autoprograms}\PassMan"; Filename: "{app}\PassMan.exe" -Name: "{autodesktop}\PassMan"; Filename: "{app}\PassMan.exe"; Tasks: desktopicon +Name: "{autoprograms}\SecretSpace"; Filename: "{app}\SecretSpace.exe" +Name: "{autodesktop}\SecretSpace"; Filename: "{app}\SecretSpace.exe"; Tasks: desktopicon [Run] -Filename: "{app}\PassMan.exe"; Description: "Start PassMan now"; Flags: nowait postinstall skipifsilent +Filename: "{app}\SecretSpace.exe"; Description: "Start SecretSpace now"; Flags: nowait postinstall skipifsilent [Code] var MovePage: TInputOptionWizardPage; FromPage: TInputDirWizardPage; +{ Where the installed app keeps its data (vault.appdata_home makes the same + choice): SecretSpace, unless only the folder of the old name PassMan exists, + which an upgrade then keeps using. Nothing is moved or renamed. } function DataHome: String; +var + NewHome, OldHome: String; begin - Result := ExpandConstant('{userappdata}\PassMan'); + NewHome := ExpandConstant('{userappdata}\SecretSpace'); + OldHome := ExpandConstant('{userappdata}\PassMan'); + if (not DirExists(NewHome)) and DirExists(OldHome) then + Result := OldHome + else + Result := NewHome; end; function HasVaults(Dir: String): Boolean; @@ -107,7 +123,7 @@ procedure InitializeWizard; begin MovePage := CreateInputOptionPage(wpSelectTasks, 'Existing vaults', 'Have you used the portable PassMan.exe before?', - 'This copy of PassMan keeps its vaults in your user profile:' + #13#10 + + 'This copy of SecretSpace keeps its vaults in your user profile:' + #13#10 + ' ' + DataHome + #13#10 + #13#10 + 'If you have vaults next to an earlier PassMan.exe, Setup can move ' + 'them there now. They are moved, not copied, so you still have one ' + diff --git a/installer/installed b/installer/installed index 7149ec8..c2befd1 100644 --- a/installer/installed +++ b/installer/installed @@ -1,4 +1,5 @@ -This file was put here by PassMan-Setup.exe. While it is next to -PassMan.exe, PassMan keeps your vaults and settings in -%APPDATA%\PassMan instead of in this folder, so upgrading or +This file was put here by SecretSpace-Setup.exe. While it is next to +SecretSpace.exe, SecretSpace keeps your vaults and settings in +%APPDATA%\SecretSpace (or %APPDATA%\PassMan, if that is where they +already are) instead of in this folder, so upgrading or uninstalling the program never touches them. diff --git a/requirements.txt b/requirements.txt index 2b0b2c8..908adcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -# PassMan dependencies +# SecretSpace dependencies cryptography>=44 # AES-GCM + PBKDF2 vaults; 44+ for Argon2id (3.0 accounts) pyperclip>=1.8 # copy username/password to clipboard customtkinter>=5.2 # modern UI toolkit diff --git a/sourceFiles/PassMan-installer.spec b/sourceFiles/SecretSpace-installer.spec similarity index 85% rename from sourceFiles/PassMan-installer.spec rename to sourceFiles/SecretSpace-installer.spec index bd94e15..242c786 100644 --- a/sourceFiles/PassMan-installer.spec +++ b/sourceFiles/SecretSpace-installer.spec @@ -1,6 +1,6 @@ # -*- mode: python ; coding: utf-8 -*- # -# The build the installer ships: a folder (PassMan.exe + _internal\), not a +# The build the installer ships: a folder (SecretSpace.exe + _internal\), not a # single self-extracting file. A single-file exe unpacks ~45 MB to %TEMP% on # every launch - about a second of start-up, and a leftover _MEI folder # whenever it is killed - which is why the portable one was retired. UPX is off for the same reason: every DLL would otherwise be @@ -14,8 +14,8 @@ SRC = SPECPATH sys.path.insert(0, SRC) from build_version import version_info # Properties > Details -datas = [(os.path.join(SRC, 'PassMan-logo.png'), '.'), - (os.path.join(SRC, 'PassMan-logo.ico'), '.')] +datas = [(os.path.join(SRC, 'SecretSpace-logo.png'), '.'), + (os.path.join(SRC, 'SecretSpace-logo.ico'), '.')] binaries = [] # PIL.ImageTk pulls in PIL._tkinter_finder at runtime, and PyInstaller's # Pillow hook does not always detect it. Without it the frozen app crashes @@ -46,7 +46,7 @@ exe = EXE( a.scripts, [], exclude_binaries=True, - name='PassMan', + name='SecretSpace', debug=False, bootloader_ignore_signals=False, strip=False, @@ -57,7 +57,7 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, - icon=[os.path.join(SRC, 'PassMan-logo.ico')], + icon=[os.path.join(SRC, 'SecretSpace-logo.ico')], version=version_info(), ) @@ -68,5 +68,5 @@ coll = COLLECT( strip=False, upx=False, upx_exclude=[], - name='PassMan', + name='SecretSpace', ) diff --git a/sourceFiles/PassMan-logo.ico b/sourceFiles/SecretSpace-logo.ico similarity index 100% rename from sourceFiles/PassMan-logo.ico rename to sourceFiles/SecretSpace-logo.ico 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/build_version.py b/sourceFiles/build_version.py index 248f553..f462945 100644 --- a/sourceFiles/build_version.py +++ b/sourceFiles/build_version.py @@ -1,5 +1,5 @@ """ -The Windows version resource for PassMan.exe -- what Explorer shows under +The Windows version resource for SecretSpace.exe -- what Explorer shows under Properties > Details. Used by both .spec files at build time only; the app never imports it, so PyInstaller does not bundle it. @@ -14,8 +14,8 @@ from version import __version__, file_version -def version_info(description="PassMan password manager", - filename="PassMan.exe") -> VSVersionInfo: +def version_info(description="SecretSpace password manager", + filename="SecretSpace.exe") -> VSVersionInfo: numbers = file_version() return VSVersionInfo( ffi=FixedFileInfo(filevers=numbers, prodvers=numbers), @@ -25,7 +25,7 @@ def version_info(description="PassMan password manager", StringStruct("FileVersion", __version__), StringStruct("InternalName", filename[:-4]), StringStruct("OriginalFilename", filename), - StringStruct("ProductName", "PassMan"), + StringStruct("ProductName", "SecretSpace"), StringStruct("ProductVersion", __version__), ])]), VarFileInfo([VarStruct("Translation", [0x0409, 1200])]), 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 6bb6b2d..5df50bb 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 @@ -143,7 +143,7 @@ def __init__(self, root): # Branding (window icon + logo) - all optional / best effort # ------------------------------------------------------------------ def _load_branding(self): - for p in asset_candidates("PassMan-logo.ico"): + for p in asset_candidates("SecretSpace-logo.ico"): if os.path.exists(p): try: self.root.iconbitmap(p) @@ -153,7 +153,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 @@ -454,7 +454,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( @@ -668,7 +668,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) @@ -1155,7 +1155,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 b8da2ba..bbca769 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="C:\\PassMan\\server.crt") + placeholder="C:\\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 ac77b3c..6f5e1fd 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 %APPDATA%\PassMan, not the program folder ----- +# --- installed copy: data in %APPDATA%\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_appdata(tmp_path, monkeypatch): import appconfig monkeypatch.delenv("PASSMAN_DATA_DIR", raising=False) - app = tmp_path / "Programs" / "PassMan" + app = tmp_path / "Programs" / "SecretSpace" app.mkdir(parents=True) (app / vault.INSTALLED_MARKER).write_text("installed by setup") monkeypatch.setattr(vault, "get_app_dir", lambda: str(app)) monkeypatch.setenv("APPDATA", str(tmp_path / "Roaming")) - home = tmp_path / "Roaming" / "PassMan" + home = tmp_path / "Roaming" / "SecretSpace" assert vault.is_installed() assert vault.get_data_dir() == str(home / "vault") assert appconfig.config_file() == str(home / "config.json") @@ -780,6 +780,40 @@ def test_installed_copy_keeps_data_in_appdata(tmp_path, monkeypatch): assert sorted(p.name for p in app.iterdir()) == [vault.INSTALLED_MARKER] +def _installed(tmp_path, monkeypatch): + monkeypatch.delenv("PASSMAN_DATA_DIR", raising=False) + app = tmp_path / "Programs" / "SecretSpace" + app.mkdir(parents=True) + (app / vault.INSTALLED_MARKER).write_text("installed by setup") + monkeypatch.setattr(vault, "get_app_dir", lambda: str(app)) + monkeypatch.setenv("APPDATA", str(tmp_path / "Roaming")) + return tmp_path / "Roaming" + + +def test_an_upgrade_from_passman_keeps_the_old_data_folder(tmp_path, monkeypatch): + """Installed as PassMan before the rename: the vaults stay where they are, + nothing is moved, and no empty SecretSpace folder appears beside them.""" + roaming = _installed(tmp_path, monkeypatch) + old = roaming / "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 (roaming / "SecretSpace").exists() + + +def test_the_new_folder_wins_once_it_exists(tmp_path, monkeypatch): + roaming = _installed(tmp_path, monkeypatch) + (roaming / "PassMan").mkdir(parents=True) + (roaming / "SecretSpace").mkdir(parents=True) + assert vault.get_data_dir() == str(roaming / "SecretSpace" / "vault") + + +def test_a_new_install_uses_the_new_name(tmp_path, monkeypatch): + roaming = _installed(tmp_path, monkeypatch) + assert vault.get_data_dir() == str(roaming / "SecretSpace" / "vault") + + def test_env_override_beats_an_installed_copy(tmp_path, monkeypatch): (tmp_path / vault.INSTALLED_MARKER).write_text("") monkeypatch.setattr(vault, "get_app_dir", lambda: str(tmp_path)) diff --git a/sourceFiles/tests/test_version.py b/sourceFiles/tests/test_version.py index 331173a..f0ad440 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 39b0057..aef2c81 100644 --- a/sourceFiles/vault.py +++ b/sourceFiles/vault.py @@ -82,7 +82,7 @@ def legacy_id(site: str, username: str) -> str: def get_app_dir() -> str: """ - The project root: the folder holding PassMan.exe when frozen, else the + The project root: the folder holding SecretSpace.exe when frozen, else the parent of ``sourceFiles`` where this module lives. Anchored to the executable, NOT the working directory, so shortcuts with @@ -92,11 +92,11 @@ 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__))) -#: The installer puts a file of this name next to PassMan.exe. Its presence +#: The installer puts a file of this name next to SecretSpace.exe. Its presence #: means "installed copy": the program folder is replaced on upgrade and #: removed on uninstall, so the user's data must live somewhere else. INSTALLED_MARKER = "installed" @@ -111,18 +111,37 @@ def get_home_dir() -> str: Where config.json and the vault folder live. Portable (no installer marker): the app folder, so the app and its data - travel together on a USB stick. Installed: %APPDATA%\\PassMan, which + travel together on a USB stick. Installed: %APPDATA%\\SecretSpace, which survives upgrades and uninstalling and does not need the program folder - to be writable. + to be writable -- or %APPDATA%\\PassMan, see ``appdata_home``. """ if is_installed(): base = os.environ.get("APPDATA") or os.path.join( os.path.expanduser("~"), "AppData", "Roaming") - return os.path.join(base, "PassMan") + return appdata_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 appdata_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 finds the + vaults where they are. Nothing is moved or renamed; the installer's + DataHome makes the same choice. + """ + 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: @@ -143,7 +162,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 %APPDATA%\\PassMan + elsewhere. An installed copy keeps its one copy in %APPDATA%\\SecretSpace instead of next to the exe; that is a different home, not a mirror. If a folder from before the rename (``data``) is present and ``vault`` is @@ -286,7 +305,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 80c53f2..289d4a7 100644 --- a/sourceFiles/version.py +++ b/sourceFiles/version.py @@ -12,7 +12,7 @@ import re -__version__ = "3.0.0-alpha.1" +__version__ = "3.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: """(3, 0, 0, 1) for '3.0.0-alpha.1', (3, 0, 0, 0) for '3.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,5 +36,5 @@ def version_string() -> str: def long_version_string() -> str: - """e.g. 'PassMan v3.0.0 · vault format PMV3' — for the Settings footer.""" - return f"PassMan {version_string()} · vault format {VAULT_FORMAT}" + """e.g. 'SecretSpace v3.0.0 · vault format PMV3' — for the Settings footer.""" + return f"SecretSpace {version_string()} · vault format {VAULT_FORMAT}"