05/18 Enhanced codes and functionalities 3
This commit is contained in:
+19
-7
@@ -922,8 +922,11 @@ def recovery_items():
|
||||
the client must derive it by decrypting the recovery blob with the recovery
|
||||
code. This ensures only the holder of the recovery code can compute the proof.
|
||||
|
||||
The challenge row is NOT consumed here — it is consumed by the final
|
||||
POST /recover call so that endpoint can also validate the proof.
|
||||
Replay prevention: the challenge is consumed (deleted) on success, then
|
||||
immediately re-issued with the same expected_proof but a new nonce and a
|
||||
fresh TTL. This means each call to /recovery/items rotates the challenge,
|
||||
so a captured X-Recovery-Proof header cannot be replayed by a third party.
|
||||
POST /recover will consume the rotated challenge on final commit.
|
||||
Items are returned as encrypted ciphertext blobs only.
|
||||
"""
|
||||
from app.models.recovery_challenge import RecoveryChallenge
|
||||
@@ -938,11 +941,9 @@ def recovery_items():
|
||||
if not user or not user.recovery_enc_salt:
|
||||
return jsonify({'error': 'No recovery data found'}), 404
|
||||
|
||||
# Validate against the DB-stored challenge without consuming it —
|
||||
# POST /recover will consume it atomically on commit.
|
||||
challenge = RecoveryChallenge.query.filter_by(user_id=user.id).first()
|
||||
from datetime import datetime, timezone
|
||||
if not challenge or challenge.expires_at < datetime.now(timezone.utc).replace(tzinfo=None):
|
||||
# Consume the current challenge atomically.
|
||||
challenge = RecoveryChallenge.consume(user.id)
|
||||
if not challenge:
|
||||
return jsonify({'error': 'No active recovery challenge. Call /recovery/data first.'}), 400
|
||||
|
||||
if not verify_recovery_proof(challenge.expected_proof, client_proof):
|
||||
@@ -957,6 +958,17 @@ def recovery_items():
|
||||
db.session.commit()
|
||||
return jsonify({'error': 'Invalid recovery proof'}), 401
|
||||
|
||||
# Re-issue a fresh challenge with the same expected_proof but a new nonce
|
||||
# and TTL. POST /recover will consume this rotated challenge on final commit.
|
||||
# The client continues to send the same proof value — no client change needed.
|
||||
new_nonce = generate_recovery_nonce()
|
||||
RecoveryChallenge.create(
|
||||
user_id=user.id,
|
||||
nonce=new_nonce,
|
||||
expected_proof=challenge.expected_proof, # same proof, new nonce
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
from app.models.vault_item import VaultItem
|
||||
items = VaultItem.query.filter_by(user_id=user.id).all()
|
||||
return jsonify({
|
||||
|
||||
+32
-3
@@ -130,9 +130,11 @@ const Vault = (() => {
|
||||
res = await fetch(path, merged);
|
||||
}
|
||||
|
||||
if (!res.ok && res.status !== 404) {
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `HTTP ${res.status}`);
|
||||
const e = new Error(err.error || `HTTP ${res.status}`);
|
||||
e.status = res.status;
|
||||
throw e;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -974,7 +976,35 @@ const Vault = (() => {
|
||||
|
||||
let _importViewInitialised = false;
|
||||
|
||||
function _resetImportView() {
|
||||
_importRows = [];
|
||||
const fileInput = document.getElementById("import-file-input");
|
||||
const fileNameEl = document.getElementById("import-file-name");
|
||||
const previewEl = document.getElementById("import-preview");
|
||||
const confirmBtn = document.getElementById("btn-import-confirm");
|
||||
const resultEl = document.getElementById("import-result");
|
||||
if (fileInput) fileInput.value = "";
|
||||
if (fileNameEl) fileNameEl.textContent = "No file chosen";
|
||||
if (previewEl) {
|
||||
previewEl.innerHTML = "";
|
||||
previewEl.classList.add("hidden");
|
||||
}
|
||||
if (confirmBtn) {
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.textContent = "Import items";
|
||||
delete confirmBtn.dataset.mode;
|
||||
}
|
||||
if (resultEl) {
|
||||
resultEl.innerHTML = "";
|
||||
resultEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function loadImportExportView() {
|
||||
// Reset import panel state every time the view is entered so a previous
|
||||
// import result or file preview is never shown stale on re-entry.
|
||||
_resetImportView();
|
||||
|
||||
if (_importViewInitialised) return;
|
||||
_importViewInitialised = true;
|
||||
|
||||
@@ -1238,7 +1268,6 @@ const Vault = (() => {
|
||||
});
|
||||
if (!res) return;
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Import failed.");
|
||||
|
||||
resultEl.innerHTML = `✅ Imported <strong>${data.imported}</strong> item(s)${data.skipped ? `, skipped ${data.skipped} malformed row(s)` : ""}.`;
|
||||
resultEl.className = "import-result import-result-ok";
|
||||
|
||||
Reference in New Issue
Block a user