diff --git a/internal/html/assets/src/app.ts b/internal/html/assets/src/app.ts
index 2b1a7811..03677185 100644
--- a/internal/html/assets/src/app.ts
+++ b/internal/html/assets/src/app.ts
@@ -341,13 +341,27 @@ type UIShare = ParsedShare & { isHolder?: boolean };
// Load embedded manifest if available (included when MANIFEST.age is small enough)
if (personalization.manifestB64) {
- const binary = atob(personalization.manifestB64);
- const bytes = new Uint8Array(binary.length);
- for (let i = 0; i < binary.length; i++) {
- bytes[i] = binary.charCodeAt(i);
+ try {
+ const binary = atob(personalization.manifestB64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ state.manifest = bytes;
+ showManifestLoaded('MANIFEST.age', state.manifest.length, 'embedded');
+ } catch (err) {
+ // atob throws DOMException on corrupt base64 (bit rot, truncated copy,
+ // storage degradation). Surface it so the user knows to try another
+ // copy of recover.html rather than assuming they need MANIFEST.age
+ // separately.
+ showError(
+ t('error_corrupt_embedded_manifest_message'),
+ {
+ title: t('error_corrupt_embedded_manifest_title'),
+ guidance: t('error_corrupt_embedded_manifest_guidance'),
+ }
+ );
}
- state.manifest = bytes;
- showManifestLoaded('MANIFEST.age', state.manifest.length, 'embedded');
}
checkRecoverReady();
diff --git a/internal/translations/recover/en.json b/internal/translations/recover/en.json
index 87128145..3542ccd1 100644
--- a/internal/translations/recover/en.json
+++ b/internal/translations/recover/en.json
@@ -77,6 +77,9 @@
"error_extract_status": "Extraction failed. The archive may be corrupted.",
"error_recovery_title": "Recovery failed",
"error_recovery_guidance": "Check that you have the correct pieces and the right MANIFEST.age file. You can try again with different pieces.",
+ "error_corrupt_embedded_manifest_title": "Corrupted embedded archive",
+ "error_corrupt_embedded_manifest_message": "The archive embedded in this recover.html is damaged and couldn't be loaded.",
+ "error_corrupt_embedded_manifest_guidance": "Your copy of recover.html may be truncated or corrupted. Try another copy of the file, or load MANIFEST.age manually in Step 2.",
"action_reload": "Reload page",
"action_use_cli": "Use CLI tool",
"action_try_different_shares": "Try different pieces",