feat(auth): add PIN session persistence, vault sync, and pre-fill login credentials

This commit is contained in:
2026-08-27 09:02:09 +02:00
parent 569777ce8a
commit a7f6ce23e3
8 changed files with 258 additions and 105 deletions
+78 -11
View File
@@ -190,6 +190,25 @@ class Plugin:
# ===== Vault Operations =====
async def sync_vault(self) -> dict:
"""Sync vault from server and re-decrypt."""
try:
if not self.client or not self._session_active:
return {"success": False, "error": "No active session"}
if not self._enc_key or not self._mac_key:
return {"success": False, "error": "Encryption keys not available"}
sync_data = await self.client.sync_vault()
self._vault_data = sync_data
self._decrypted_vault = self.client.decrypt_vault(
sync_data, self._enc_key, self._mac_key
)
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
except Exception as e:
return {"success": False, "error": str(e)}
async def get_vault_items(self) -> dict:
"""Get decrypted vault items."""
if not self._decrypted_vault:
@@ -306,6 +325,21 @@ class Plugin:
settings["pin_kdf_iterations"] = 200000
settings["pin_envelope"] = envelope.hex()
settings["pin_envelope_iv"] = iv.hex()
# Persist tokens encrypted with PIN for session restore across restarts
if self.client and self.client.access_token:
import json as _json
token_data = _json.dumps({
"access_token": self.client.access_token,
"refresh_token": self.client.refresh_token or "",
"user_id": self.client.user_id or "",
}).encode("utf-8")
token_envelope, token_iv = self.pin_crypto.encrypt_user_key_for_pin(
token_data, pin_key
)
settings["pin_token_envelope"] = token_envelope.hex()
settings["pin_token_envelope_iv"] = token_iv.hex()
self._save_settings(settings)
self._pin_attempts = 0
@@ -350,18 +384,34 @@ class Plugin:
else:
raise ValueError("Invalid decrypted user key length")
# Re-sync and decrypt vault
if self.client:
sync_data = await self.client.sync_vault()
self._vault_data = sync_data
self._decrypted_vault = self.client.decrypt_vault(
sync_data, self._enc_key, self._mac_key
)
self._session_active = True
self._pin_attempts = 0
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
# Restore session from persisted tokens if no active client (restart scenario)
if not self.client:
token_envelope_hex = settings.get("pin_token_envelope", "")
if token_envelope_hex:
import json as _json
token_envelope = bytes.fromhex(token_envelope_hex)
token_data_bytes = self.pin_crypto.decrypt_user_key_with_pin(
token_envelope, pin_key
)
token_data = _json.loads(token_data_bytes.decode("utf-8"))
return {"success": False, "error": "No active session"}
server_url = settings.get("server_url", "https://api.bitwarden.com")
self.client = self.client_class(server_url)
self.client.access_token = token_data.get("access_token", "")
self.client.refresh_token = token_data.get("refresh_token", "")
self.client.user_id = token_data.get("user_id", "")
else:
return {"success": False, "error": "No active session and no persisted tokens"}
# Re-sync and decrypt vault
sync_data = await self.client.sync_vault()
self._vault_data = sync_data
self._decrypted_vault = self.client.decrypt_vault(
sync_data, self._enc_key, self._mac_key
)
self._session_active = True
self._pin_attempts = 0
return {"success": True, "vault_size": len(self._decrypted_vault.get("ciphers", []))}
except Exception as e:
self._pin_attempts += 1
@@ -380,6 +430,8 @@ class Plugin:
settings.pop("pin_kdf_iterations", None)
settings.pop("pin_envelope", None)
settings.pop("pin_envelope_iv", None)
settings.pop("pin_token_envelope", None)
settings.pop("pin_token_envelope_iv", None)
self._save_settings(settings)
return {"success": True}
@@ -388,6 +440,16 @@ class Plugin:
settings = self._load_settings()
return {"enabled": settings.get("pin_enabled", False)}
async def get_saved_credentials(self) -> dict:
"""Get saved server URL and email for pre-filling login form."""
settings = self._load_settings()
return {
"success": True,
"server_url": settings.get("server_url", "https://api.bitwarden.com"),
"email": settings.get("email", ""),
"pin_enabled": settings.get("pin_enabled", False),
}
# ===== Vault Lock =====
async def lock_vault(self) -> dict:
@@ -407,6 +469,11 @@ class Plugin:
await self.lock_vault()
self._master_key = None
self.client = None
# Clear persisted tokens from settings
settings = self._load_settings()
settings.pop("pin_token_envelope", None)
settings.pop("pin_token_envelope_iv", None)
self._save_settings(settings)
return {"success": True}
# ===== Settings =====