feat(auth): add PIN session persistence, vault sync, and pre-fill login credentials
This commit is contained in:
@@ -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:
|
||||
@@ -295,9 +314,9 @@ class Plugin:
|
||||
pin, pin_salt, kdf_iterations=200000
|
||||
)
|
||||
|
||||
# Encrypt user key with PIN key
|
||||
# Encrypt user key with PIN key (store both enc_key + mac_key)
|
||||
envelope, iv = self.pin_crypto.encrypt_user_key_for_pin(
|
||||
self._enc_key, pin_key
|
||||
self._enc_key + self._mac_key, pin_key
|
||||
)
|
||||
|
||||
# Save PIN settings
|
||||
@@ -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
|
||||
@@ -345,23 +379,48 @@ class Plugin:
|
||||
self._enc_key = user_key[:32]
|
||||
self._mac_key = user_key[32:]
|
||||
elif len(user_key) == 32:
|
||||
self._enc_key = user_key
|
||||
self._mac_key = user_key
|
||||
# Old format: only enc_key was stored, mac_key is wrong.
|
||||
# Clear stale PIN and force re-setup.
|
||||
settings["pin_enabled"] = False
|
||||
settings.pop("pin_salt", None)
|
||||
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": False, "error": "PIN was set with an older version. Please log in and set up PIN again."}
|
||||
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 +439,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 +449,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 +478,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 =====
|
||||
|
||||
Reference in New Issue
Block a user