Compare commits
3
Commits
dc68a477b6
...
f8730cc0ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8730cc0ab | ||
|
|
8a766181de | ||
|
|
bd0d2cd812 |
No files matched your search
@@ -1554,6 +1554,10 @@ def _resolve_credential(cred: dict) -> dict | None:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Detect scrypt hash (salt_hex:hash_hex) — passwords stored securely
|
||||||
|
if re.match(r'^[0-9a-f]{32}:[0-9a-f]{64,}$', raw):
|
||||||
|
value = "(stored securely — use Security Reset to set a new one)"
|
||||||
|
else:
|
||||||
value = prefix + raw + suffix
|
value = prefix + raw + suffix
|
||||||
result = {"label": label, "value": value, "multiline": multiline}
|
result = {"label": label, "value": value, "multiline": multiline}
|
||||||
|
|
||||||
@@ -5173,7 +5177,7 @@ async def api_security_reset():
|
|||||||
try:
|
try:
|
||||||
os.makedirs("/var/lib/secrets", exist_ok=True)
|
os.makedirs("/var/lib/secrets", exist_ok=True)
|
||||||
with open("/var/lib/secrets/root-password", "w") as f:
|
with open("/var/lib/secrets/root-password", "w") as f:
|
||||||
f.write(new_root_password)
|
f.write(_hash_password(new_root_password))
|
||||||
os.chmod("/var/lib/secrets/root-password", 0o600)
|
os.chmod("/var/lib/secrets/root-password", 0o600)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
errors.append(f"write root-password: {exc}")
|
errors.append(f"write root-password: {exc}")
|
||||||
@@ -5195,7 +5199,7 @@ async def api_security_reset():
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass # Non-fatal
|
pass # Non-fatal
|
||||||
|
|
||||||
return {"ok": True, "new_password": new_free_password, "errors": errors}
|
return {"ok": True, "new_password": new_free_password, "new_root_password": new_root_password, "errors": errors}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/security/verify-integrity")
|
@app.post("/api/security/verify-integrity")
|
||||||
|
|||||||
@@ -111,7 +111,15 @@ in
|
|||||||
echo "$ROOT_PASS" > "$SECRET_FILE"
|
echo "$ROOT_PASS" > "$SECRET_FILE"
|
||||||
chmod 600 "$SECRET_FILE"
|
chmod 600 "$SECRET_FILE"
|
||||||
fi
|
fi
|
||||||
echo "root:$(cat "$SECRET_FILE")" | chpasswd
|
# If the file contains a scrypt hash (salt:hash), skip chpasswd — the
|
||||||
|
# password was already set via the Hub security reset endpoint and this
|
||||||
|
# service is only re-running as a manual recovery step.
|
||||||
|
CONTENT="$(cat "$SECRET_FILE")"
|
||||||
|
if echo "$CONTENT" | grep -qE '^[0-9a-f]{32}:[0-9a-f]{64,}$'; then
|
||||||
|
echo "root-password-setup: stored value is already hashed — skipping chpasswd" >&2
|
||||||
|
else
|
||||||
|
echo "root:$CONTENT" | chpasswd
|
||||||
|
fi
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py
|
|
||||||
index 84c88eb..394eec9 100644
|
|
||||||
--- a/app/sovran_systemsos_web/server.py
|
|
||||||
+++ b/app/sovran_systemsos_web/server.py
|
|
||||||
@@ -107,6 +107,7 @@ AUTOLAUNCH_DISABLE_FLAG = "/var/lib/sovran/hub-autolaunch-disabled"
|
|
||||||
# ── Hub web authentication ────────────────────────────────────────
|
|
||||||
|
|
||||||
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"
|
|
||||||
+FREE_PASSWORD_FILE_WEB = "/var/lib/secrets/free-password-web"
|
|
||||||
MIGRATION_NEWPASS_FILE = "/var/lib/secrets/free-password-migration-newpass"
|
|
||||||
HUB_SESSION_SECRET_FILE = "/var/lib/secrets/hub-session-secret"
|
|
||||||
SESSION_COOKIE_NAME = "hub_session"
|
|
||||||
@@ -611,12 +612,44 @@ def _read_free_password() -> str | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def _hash_password(password: str) -> str:
|
|
||||||
+ """Return a scrypt-hash with 16-byte salt, formatted salt_hex:hash_hex."""
|
|
||||||
+ salt = os.urandom(16)
|
|
||||||
+ hashed = hashlib.scrypt(password.encode(), salt=salt, n=16384, r=8, p=1)
|
|
||||||
+ return salt.hex() + ":" + hashed.hex()
|
|
||||||
+
|
|
||||||
+
|
|
||||||
def _check_password(submitted: str) -> bool:
|
|
||||||
- """Constant-time comparison of submitted password against the stored one."""
|
|
||||||
- stored = _read_free_password()
|
|
||||||
- if stored is None:
|
|
||||||
- return False
|
|
||||||
- return hmac.compare_digest(submitted.encode(), stored.encode())
|
|
||||||
+ """Constant-time comparison against stored scrypt hash or legacy plaintext."""
|
|
||||||
+ for fp in (FREE_PASSWORD_FILE_WEB, FREE_PASSWORD_FILE):
|
|
||||||
+ try:
|
|
||||||
+ with open(fp, "r") as f:
|
|
||||||
+ stored = f.read().strip()
|
|
||||||
+ except Exception:
|
|
||||||
+ continue
|
|
||||||
+ if not stored:
|
|
||||||
+ continue
|
|
||||||
+ if ":" in stored:
|
|
||||||
+ try:
|
|
||||||
+ salt_hex, hash_hex = stored.split(":", 1)
|
|
||||||
+ salt = bytes.fromhex(salt_hex)
|
|
||||||
+ submitted_hash = hashlib.scrypt(submitted.encode(), salt=salt, n=16384, r=8, p=1)
|
|
||||||
+ if hmac.compare_digest(submitted_hash.hex().encode(), hash_hex.encode()):
|
|
||||||
+ return True
|
|
||||||
+ except Exception:
|
|
||||||
+ pass
|
|
||||||
+ else:
|
|
||||||
+ if hmac.compare_digest(submitted.encode(), stored.encode()):
|
|
||||||
+ try:
|
|
||||||
+ with open(FREE_PASSWORD_FILE_WEB, "w") as f:
|
|
||||||
+ f.write(_hash_password(submitted))
|
|
||||||
+ os.chmod(FREE_PASSWORD_FILE_WEB, 0o600)
|
|
||||||
+ except Exception:
|
|
||||||
+ pass
|
|
||||||
+ return True
|
|
||||||
+ return False
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_onboarding_reopened_for_migration() -> None:
|
|
||||||
@@ -5118,9 +5151,22 @@ async def api_security_reset():
|
|
||||||
# Write new passwords to secrets files
|
|
||||||
try:
|
|
||||||
os.makedirs("/var/lib/secrets", exist_ok=True)
|
|
||||||
- with open("/var/lib/secrets/free-password", "w") as f:
|
|
||||||
- f.write(new_free_password)
|
|
||||||
- os.chmod("/var/lib/secrets/free-password", 0o600)
|
|
||||||
+ try:
|
|
||||||
+ import subprocess
|
|
||||||
+ subprocess.run(
|
|
||||||
+ ["chpasswd"],
|
|
||||||
+ input=f"free:{new_free_password}\n".encode(),
|
|
||||||
+ check=True,
|
|
||||||
+ capture_output=True,
|
|
||||||
+ )
|
|
||||||
+ except Exception:
|
|
||||||
+ pass
|
|
||||||
+ try:
|
|
||||||
+ with open(FREE_PASSWORD_FILE_WEB, "w") as f:
|
|
||||||
+ f.write(_hash_password(new_free_password))
|
|
||||||
+ os.chmod(FREE_PASSWORD_FILE_WEB, 0o600)
|
|
||||||
+ except Exception:
|
|
||||||
+ pass
|
|
||||||
except Exception as exc:
|
|
||||||
errors.append(f"write free-password: {exc}")
|
|
||||||
|
|
||||||
@@ -5301,9 +5347,21 @@ async def api_change_password(req: ChangePasswordRequest):
|
|
||||||
# Write new password to secrets file so Hub credentials stay in sync
|
|
||||||
try:
|
|
||||||
os.makedirs(os.path.dirname(FREE_PASSWORD_FILE), exist_ok=True)
|
|
||||||
- with open(FREE_PASSWORD_FILE, "w") as f:
|
|
||||||
- f.write(req.new_password)
|
|
||||||
- os.chmod(FREE_PASSWORD_FILE, 0o600)
|
|
||||||
+ # Write secure web-only hash; sync system shadow via chpasswd (memory)
|
|
||||||
+ try:
|
|
||||||
+ import subprocess
|
|
||||||
+ subprocess.run(
|
|
||||||
+ ["chpasswd"],
|
|
||||||
+ input=f"free:{req.new_password}\n".encode(),
|
|
||||||
+ check=True,
|
|
||||||
+ capture_output=True,
|
|
||||||
+ )
|
|
||||||
+ except Exception as exc:
|
|
||||||
+ # Log but do not block; web hash is the critical fix
|
|
||||||
+ pass
|
|
||||||
+ with open(FREE_PASSWORD_FILE_WEB, "w") as f:
|
|
||||||
+ f.write(_hash_password(req.new_password))
|
|
||||||
+ os.chmod(FREE_PASSWORD_FILE_WEB, 0o600)
|
|
||||||
except Exception as exc:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to write secrets file: {exc}")
|
|
||||||
|
|
||||||
Reference in new issue
Block a user