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
|
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.
|
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
|
Replay prevention: the challenge is consumed (deleted) on success, then
|
||||||
POST /recover call so that endpoint can also validate the proof.
|
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.
|
Items are returned as encrypted ciphertext blobs only.
|
||||||
"""
|
"""
|
||||||
from app.models.recovery_challenge import RecoveryChallenge
|
from app.models.recovery_challenge import RecoveryChallenge
|
||||||
@@ -938,11 +941,9 @@ def recovery_items():
|
|||||||
if not user or not user.recovery_enc_salt:
|
if not user or not user.recovery_enc_salt:
|
||||||
return jsonify({'error': 'No recovery data found'}), 404
|
return jsonify({'error': 'No recovery data found'}), 404
|
||||||
|
|
||||||
# Validate against the DB-stored challenge without consuming it —
|
# Consume the current challenge atomically.
|
||||||
# POST /recover will consume it atomically on commit.
|
challenge = RecoveryChallenge.consume(user.id)
|
||||||
challenge = RecoveryChallenge.query.filter_by(user_id=user.id).first()
|
if not challenge:
|
||||||
from datetime import datetime, timezone
|
|
||||||
if not challenge or challenge.expires_at < datetime.now(timezone.utc).replace(tzinfo=None):
|
|
||||||
return jsonify({'error': 'No active recovery challenge. Call /recovery/data first.'}), 400
|
return jsonify({'error': 'No active recovery challenge. Call /recovery/data first.'}), 400
|
||||||
|
|
||||||
if not verify_recovery_proof(challenge.expected_proof, client_proof):
|
if not verify_recovery_proof(challenge.expected_proof, client_proof):
|
||||||
@@ -957,6 +958,17 @@ def recovery_items():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({'error': 'Invalid recovery proof'}), 401
|
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
|
from app.models.vault_item import VaultItem
|
||||||
items = VaultItem.query.filter_by(user_id=user.id).all()
|
items = VaultItem.query.filter_by(user_id=user.id).all()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|||||||
+33
-4
@@ -130,9 +130,11 @@ const Vault = (() => {
|
|||||||
res = await fetch(path, merged);
|
res = await fetch(path, merged);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!res.ok && res.status !== 404) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({}));
|
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;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -974,7 +976,35 @@ const Vault = (() => {
|
|||||||
|
|
||||||
let _importViewInitialised = false;
|
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() {
|
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;
|
if (_importViewInitialised) return;
|
||||||
_importViewInitialised = true;
|
_importViewInitialised = true;
|
||||||
|
|
||||||
@@ -1238,7 +1268,6 @@ const Vault = (() => {
|
|||||||
});
|
});
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
const data = await res.json();
|
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.innerHTML = `✅ Imported <strong>${data.imported}</strong> item(s)${data.skipped ? `, skipped ${data.skipped} malformed row(s)` : ""}.`;
|
||||||
resultEl.className = "import-result import-result-ok";
|
resultEl.className = "import-result import-result-ok";
|
||||||
@@ -3987,4 +4016,4 @@ const Vault = (() => {
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", Vault.init);
|
document.addEventListener("DOMContentLoaded", Vault.init);
|
||||||
Reference in New Issue
Block a user