Environment
- Commander version: 18.0.9 (reproduced; latest at time of writing is 18.0.11, same code path present on
master)
- OS: Windows 11 (PowerShell 7)
- Import command used:
keeper import --format=lastpass <email> --file-cache=<dir> --folder=NEW_IMPORT --update --show-skipped
- Source vault: LastPass export with 58 file attachments across a set of Secure Notes / Logins
Summary
When importing from LastPass, attachment download and decryption both succeed for all 58 attachments (confirmed — see evidence below), but only a consistent subset (~24 of 58) actually get uploaded to Keeper via upload_v3_attachments(). The remaining ~34 are dropped with no console output, no logging.warning, no exception, and no entry in --debug file logging — the import reports full success (e.g. 957 records imported successfully) and the "Uploading v3 attachments:" section simply never mentions the missing files.
This is fully reproducible: across four independent runs (a plain import, two --update re-imports, and a --debug run), the same ~24 files upload every time and the same ~34 never do — regardless of whether --update is used, ruling out the record-deduplication path as the cause.
Steps to reproduce
- Have a LastPass account with a mix of attachment types/sizes across many records (in our case: certs/keys/PFX files plus many small MFA-backup
.png screenshots).
- Run
import --format=lastpass <email> --file-cache=<dir>.
- Compare the count of
N. Downloading <file> ... Done lines (58) against the filenames actually listed under Uploading v3 attachments: (24).
- Re-run with
--update against the same target folder — the same ~24 files are the only ones ever uploaded, every time.
Expected behavior
All 58 downloaded/decrypted attachments are uploaded, or, if some cannot be uploaded, Commander logs a clear reason per file (as it already does in several other code paths — see below).
Actual behavior
~34 of 58 attachments vanish between the download phase and the vault/files_add request with zero diagnostic output.
Diagnostic evidence
We spent considerable effort ruling out every logged failure path in keepercommander/importer/lastpass/vault.py, parser.py, and keepercommander/importer/imp_exp.py:
decrypted_file_sizes.json (written by Vault.process_attachments() after successfully decrypting each attachment purely to measure its size) shows valid, nonzero sizes for every attachment that was ever cached — ruling out decrypt corruption/failure (vault.py's skip_bad_attachments path, which does log a warning, never fired).
- A full session captured with
debug --file <path> (root logger at DEBUG, capturing every logging.* call for the entire session, including the vault/files_add REST request/response) contained exactly 2 WARNING-level lines in 2.6MB of output, both the unrelated "Previous temporary directories from interrupted imports detected" cleanup notice. Zero occurrences of:
Attachment {name} is corrupted and failed to decrypt (vault.py)
Attachment {name} failed to download (vault.py)
Upload of {name} failed: Parent record ... is missing (imp_exp.py, upload_v3_attachments)
Shared folder decryption error (parser.py, parse_SHAR)
- The
vault/files_add REST request itself (captured via --debug) consistently contains ~23–24 file entries — matching the same subset every run.
- Re-running with
--update (which triggers the dedup/reconciliation path in imp_exp.py comparing title+size against a matched existing record's current fileRef list) produces an identical result to the very first plain import, which had no pre-existing duplicate records to match against at all — ruling out the reconciliation logic (elif r.uid in external_lookup: block) as the root cause, since it could only apply on subsequent runs.
This points at one of the two genuinely silent continue statements in upload_v3_attachments() (keepercommander/importer/imp_exp.py):
atta.prepare()
if isinstance(atta.size, int):
if atta.size == 0:
continue
if atta.size > 100 * 2 ** 20:
logging.warning(...)
continue
else:
continue
Neither branch logs anything. Given the decrypted sizes we can observe are all valid and nonzero, either:
atta.size is not consistently populated by the time this runs for every attachment object (e.g. a given ImportAttachment instance reaching this function is not the same object instance that had .size set during the earlier Vault.process_attachments() pass), or
- There's a batching/ordering issue elsewhere between record parsing and this function that causes a subset of attachment objects to arrive here without their size having been computed.
We were not able to pin down which of these two exactly, because both are silent by design — reproducing this reliably but being unable to get a definitive answer even with full DEBUG-level logging enabled is itself part of the bug report.
Suggested fix
Add a logging.debug or logging.warning call in both silent branches of upload_v3_attachments(), e.g.:
if isinstance(atta.size, int):
if atta.size == 0:
logging.warning(f'Skipping {atta.name}: computed size is 0')
continue
...
else:
logging.warning(f'Skipping {atta.name}: size not set (type={type(atta.size)})')
continue
This alone would have made root-causing this a five-minute job instead of the multi-hour investigation it took us, and would surface this class of bug to any future user experiencing the same silent data loss.
Workaround
We wrote a small standalone script using Commander's own LastPassImporter/Vault/attachment classes to decrypt every attachment directly to a local folder, bypassing the Keeper upload path entirely:
from keepercommander.importer.lastpass.lastpass import LastPassImporter
importer = LastPassImporter()
records = importer.do_import(username, tmpdir=file_cache_dir)
for item in records:
for atta in getattr(item, 'attachments', None) or []:
with atta.open() as src, open(out_path, 'wb') as dst:
shutil.copyfileobj(src, dst)
This confirms all 58 attachments decrypt correctly outside of the Keeper upload path, reinforcing that the loss happens specifically within upload_v3_attachments() or just before it, not during LastPass-side decryption.
Happy to share the full reproduction log (with sensitive values redacted) if useful.
Environment
master)Summary
When importing from LastPass, attachment download and decryption both succeed for all 58 attachments (confirmed — see evidence below), but only a consistent subset (~24 of 58) actually get uploaded to Keeper via
upload_v3_attachments(). The remaining ~34 are dropped with no console output, nologging.warning, no exception, and no entry in--debugfile logging — the import reports full success (e.g.957 records imported successfully) and the "Uploading v3 attachments:" section simply never mentions the missing files.This is fully reproducible: across four independent runs (a plain import, two
--updatere-imports, and a--debugrun), the same ~24 files upload every time and the same ~34 never do — regardless of whether--updateis used, ruling out the record-deduplication path as the cause.Steps to reproduce
.pngscreenshots).import --format=lastpass <email> --file-cache=<dir>.N. Downloading <file> ... Donelines (58) against the filenames actually listed underUploading v3 attachments:(24).--updateagainst the same target folder — the same ~24 files are the only ones ever uploaded, every time.Expected behavior
All 58 downloaded/decrypted attachments are uploaded, or, if some cannot be uploaded, Commander logs a clear reason per file (as it already does in several other code paths — see below).
Actual behavior
~34 of 58 attachments vanish between the download phase and the
vault/files_addrequest with zero diagnostic output.Diagnostic evidence
We spent considerable effort ruling out every logged failure path in
keepercommander/importer/lastpass/vault.py,parser.py, andkeepercommander/importer/imp_exp.py:decrypted_file_sizes.json(written byVault.process_attachments()after successfully decrypting each attachment purely to measure its size) shows valid, nonzero sizes for every attachment that was ever cached — ruling out decrypt corruption/failure (vault.py'sskip_bad_attachmentspath, which does log a warning, never fired).debug --file <path>(root logger atDEBUG, capturing everylogging.*call for the entire session, including thevault/files_addREST request/response) contained exactly 2WARNING-level lines in 2.6MB of output, both the unrelated "Previous temporary directories from interrupted imports detected" cleanup notice. Zero occurrences of:Attachment {name} is corrupted and failed to decrypt(vault.py)Attachment {name} failed to download(vault.py)Upload of {name} failed: Parent record ... is missing(imp_exp.py,upload_v3_attachments)Shared folder decryption error(parser.py,parse_SHAR)vault/files_addREST request itself (captured via--debug) consistently contains ~23–24 file entries — matching the same subset every run.--update(which triggers the dedup/reconciliation path inimp_exp.pycomparingtitle+sizeagainst a matched existing record's currentfileReflist) produces an identical result to the very first plain import, which had no pre-existing duplicate records to match against at all — ruling out the reconciliation logic (elif r.uid in external_lookup:block) as the root cause, since it could only apply on subsequent runs.This points at one of the two genuinely silent
continuestatements inupload_v3_attachments()(keepercommander/importer/imp_exp.py):Neither branch logs anything. Given the decrypted sizes we can observe are all valid and nonzero, either:
atta.sizeis not consistently populated by the time this runs for every attachment object (e.g. a givenImportAttachmentinstance reaching this function is not the same object instance that had.sizeset during the earlierVault.process_attachments()pass), orWe were not able to pin down which of these two exactly, because both are silent by design — reproducing this reliably but being unable to get a definitive answer even with full
DEBUG-level logging enabled is itself part of the bug report.Suggested fix
Add a
logging.debugorlogging.warningcall in both silent branches ofupload_v3_attachments(), e.g.:This alone would have made root-causing this a five-minute job instead of the multi-hour investigation it took us, and would surface this class of bug to any future user experiencing the same silent data loss.
Workaround
We wrote a small standalone script using Commander's own
LastPassImporter/Vault/attachment classes to decrypt every attachment directly to a local folder, bypassing the Keeper upload path entirely:This confirms all 58 attachments decrypt correctly outside of the Keeper upload path, reinforcing that the loss happens specifically within
upload_v3_attachments()or just before it, not during LastPass-side decryption.Happy to share the full reproduction log (with sensitive values redacted) if useful.