Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,36 @@ The server replies:
A first push may omit `If-Match`. `If-Match: *` overwrites whatever is there,
which the client never sends — it exists for recovering by hand.

### API v2: SecretSpace 3 accounts (test version)

Server **2.0.0-alpha** adds accounts for the SecretSpace 3 test versions
(Windows 3.0.0-alpha.3, Linux 1.0.0-alpha.3). It sits beside the routes
above, which stay exactly as they are for 2.x clients. The server still never
sees a master password, a Secret Key or a vault key: an account is the
client's **Login Key** (stored as a salted scrypt hash), its public key, and
its keyset and recovery copy, both sealed on the device. A vault is a blob
like a 2.x one, with the same revision, `If-Match` and history rules.

| Route | Method | Who | Purpose |
| --- | --- | --- | --- |
| `/v2/prelogin/<user>` | GET | anyone | the Argon2id salt and cost a device needs to turn the master password into keys. A name with no account gets a made-up, stable answer, so this says nothing about which names exist |
| `/v2/accounts` | POST | the server's **token**, as the sign-up code | make an account (and its personal vault) |
| `/v2/login` | POST | anyone | Login Key → a session, valid 12 hours. After 10 wrong tries in 15 minutes the name is paused for a minute |
| `/v2/logout` | POST | session | end this session |
| `/v2/me` | GET | session | the account: public key, keyset, recovery copy, its vaults |
| `/v2/me/keyset` | PUT | session | a new master password: new keyset and Login Key; every other session of the account ends |
| `/v2/vaults/<id>` | GET / PUT | session, member | the vault blob, as `/vault/<user>` (PUT: owner or editor; it must be a PMV3 file naming this vault) |
| `/v2/vaults/<id>/meta` | GET | session, member | its metadata |

Sessions are sent as `Authorization: Bearer <session>`. The server's token
is not a session and opens no account; a session opens no 2.x vault. A vault
you are not a member of answers 404, the same as one that does not exist.

Everything v2 keeps is in `_v2/` inside the vault store (a 2.x name cannot
start with `_`), so backing up the store backs it up too: `accounts/`,
`sessions/` (stored hashed), `vaults/<id>/` and `server.key` (for the
made-up pre-login answers).

## What it keeps

Inside `C:\ProgramData\SecretSpace Server\store`:
Expand Down
14 changes: 14 additions & 0 deletions builder/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,19 @@ def remove_previous(project: str) -> None:
shutil.rmtree(folder)


def file_version(version: str) -> str:
"""
The four plain numbers Windows wants (Inno's VersionInfoVersion):
'2.0.0-alpha.1' -> '2.0.0.1', '1.2.0' -> '1.2.0.0'. The same rule as
version.file_version, which the exes' own version resource uses.
"""
m = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:-(?:alpha|beta|rc)\.(\d+))?",
version)
if not m:
raise ValueError(f"Not a SecretSpace version: {version!r}")
return ".".join(g or "0" for g in m.groups())


def build_steps(project: str, py: "Python", iscc: str, version: str) -> list:
"""The commands, as (label, cmd, the file it must produce): the server's
folder build, then its installer."""
Expand All @@ -255,6 +268,7 @@ def build_steps(project: str, py: "Python", iscc: str, version: str) -> list:
"PassMan-Server.exe")),
("Packing SecretSpace-Server-Installer.exe",
[iscc, "/Q", f"/DAppVersion={version}",
f"/DFileVersion={file_version(version)}",
os.path.join(project, "installer", "SecretSpace-Server.iss")],
os.path.join(project, "SecretSpace-Server-Installer.exe")),
]
Expand Down
27 changes: 25 additions & 2 deletions builder/tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def test_an_incomplete_folder_is_reported(tmp_path):
# --- versions and paths ----------------------------------------------------
def test_version_is_read_from_version_py():
import re
assert re.fullmatch(r"\d+\.\d+\.\d+", build.app_version(PROJECT))
assert re.fullmatch(r"\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?",
build.app_version(PROJECT))


def test_iscc_found_in_the_per_user_location(monkeypatch):
Expand Down Expand Up @@ -153,7 +154,7 @@ def test_build_runs_the_steps_in_order(project):

def test_only_the_server_is_built():
"""The app has its own repository and its own build."""
steps = build.build_steps("P", build.Python(["py"], "3", "p"), "iscc", "1")
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-Server.exe", "SecretSpace-Server-Installer.exe"]

Expand All @@ -180,3 +181,25 @@ def test_a_missing_product_counts_as_failure(project):
log=lambda s: None, runner=FakeRunner(),
exists=lambda p: False)
assert not ok


@pytest.mark.parametrize("text, numbers", [
("1.2.0", "1.2.0.0"), ("2.0.0-alpha.1", "2.0.0.1"), ("2.0.0", "2.0.0.0"),
("2.0.0-rc.3", "2.0.0.3")])
def test_file_version(text, numbers):
assert build.file_version(text) == numbers


def test_file_version_refuses_junk():
with pytest.raises(ValueError):
build.file_version("2.0.0-dev")


def test_the_installer_gets_a_plain_file_version():
"""Inno's VersionInfoVersion takes four numbers only; a test version's
own name (2.0.0-alpha.1) would stop the installer from compiling."""
steps = build.build_steps("P", build.Python(["py"], "3", "p"), "iscc",
"2.0.0-alpha.1")
cmd = steps[1][1]
assert "/DAppVersion=2.0.0-alpha.1" in cmd and "/DFileVersion=2.0.0.1" in cmd

7 changes: 6 additions & 1 deletion installer/SecretSpace-Server.iss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
#ifndef AppVersion
#define AppVersion "0.0.0"
#endif
; Four plain numbers: a test version's AppVersion (2.0.0-alpha.1) is not
; one, so the builder passes 2.0.0.1 here as well.
#ifndef FileVersion
#define FileVersion "0.0.0.0"
#endif

[Setup]
; Never change AppId: it is how an upgrade finds the copy it replaces. It is
Expand All @@ -33,7 +38,7 @@ AppId={{6C1E2B7A-5F3D-4C8E-9A41-2D7B0E93F615}
AppName=SecretSpace Server
AppVersion={#AppVersion}
AppVerName=SecretSpace Server {#AppVersion}
VersionInfoVersion={#AppVersion}
VersionInfoVersion={#FileVersion}
DefaultDirName={autopf}\SecretSpace Server
DisableProgramGroupPage=yes
PrivilegesRequired=admin
Expand Down
4 changes: 2 additions & 2 deletions server/build_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
FixedFileInfo, StringFileInfo, StringStruct, StringTable, VarFileInfo,
VarStruct, VSVersionInfo)

from version import __version__
from version import __version__, file_version


def version_info(description="SecretSpace sync server",
filename="PassMan-Server.exe") -> VSVersionInfo:
numbers = tuple(int(part) for part in __version__.split(".")) + (0,)
numbers = file_version()
return VSVersionInfo(
ffi=FixedFileInfo(filevers=numbers, prodvers=numbers),
kids=[
Expand Down
Loading
Loading