Skip to content

Releases: kjanat/kp2bw

v3.7.0

Choose a tag to compare

@kjanat kjanat released this 21 Jun 22:33
v3.7.0
f0c57f2

PyPI

Added

  • --bitwarden-collection nested -- recreate the full KeePass folder
    hierarchy as nested collections
    (issue #33, PR #34). auto only ever used
    the top-level folder name, so Work/Servers/ssh collapsed onto a single
    Work collection and the nesting was lost. nested builds collections from
    the full /-joined path (Work/Servers), which Bitwarden renders as a nested
    tree. Collections are matched by org + name and seeded from the existing set
    on connect, so an unchanged tree is a no-op and entries sharing a path never
    duplicate a collection.

  • --folder / --no-folder (env KP2BW_CREATE_FOLDERS) -- control whether
    personal-vault folders are created from KeePass groups.
    Defaults on for
    personal imports, so existing behaviour is unchanged there (issue #33, PR
    #34).

Changed

  • An organization import (--bitwarden-org) no longer also builds a personal
    folder tree by default
    (issue #33, PR #34). Bitwarden folders are a
    personal-vault concept; the org-side equivalent is a collection, so creating
    both double-filed every item -- the collection and a redundant
    personal-folder copy. With an org set, --no-folder is now the default and
    items are filed into collections only. Pass --folder (or
    KP2BW_CREATE_FOLDERS=1) to restore the personal folder tree alongside the
    collections. Personal imports (no --bitwarden-org) are unaffected.

v3.6.0

Choose a tag to compare

@kjanat kjanat released this 14 Jun 10:01
v3.6.0
1ce905f

PyPI

Fixed

  • bw serve is no longer orphaned on teardown (POSIX), which previously hung
    the process.
    On Linux/macOS bw is commonly a node launcher that spawns a
    worker; teardown signalled only the tracked PID, leaving the worker alive --
    it kept the port and, when kp2bw's stdout was a pipe, held the pipe open so
    the parent pipeline never reached EOF (a multi-minute "still running" hang)
    and accumulated orphaned bw serve processes across runs. bw serve is now
    started in its own session (start_new_session=True) and torn down by
    signalling the whole process group (SIGTERM, then SIGKILL after a timeout), so
    the launcher and worker die together. Windows teardown (taskkill /T + port
    reap) is unchanged.

  • An empty environment variable no longer shadows the matching .env entry.
    KP2BW_KEEPASS_FILE="" (or any empty-string export) used to override the
    .env value -- load_dotenv(override=False) treats an empty export as "set"
    -- producing a baffling "KeePass database path is required" even when .env
    clearly had it. _load_dotenv now fills any variable that is unset or empty
    from the file, while a real non-empty shell variable still wins (the
    documented CLI flag > env var > default precedence is preserved for meaningful
    values).

Added

  • Manual edits made in Bitwarden are no longer silently reverted on re-run
    (issue #30). kp2bw is KeePass-authoritative, so any difference on an existing
    item used to be overwritten with the KeePass value -- quietly undoing a title
    you fixed, a note you added or a URI you corrected in Bitwarden. Every item
    kp2bw writes now carries a hidden-from-noise KP2BW_SYNC content signature; a
    re-run that finds an item's current content no longer matching that stamp
    knows a user edited it (kp2bw's own writes restamp, so they never self-trip)
    and preserves the edit, reporting it as protected in the summary instead
    of clobbering it. --force-update (env KP2BW_FORCE_UPDATE) makes KeePass
    win regardless. Unchanged re-runs stay idempotent (the stamp is excluded from
    the content diff), legacy/ unstamped items keep current behaviour, and
    --strip-ids now removes KP2BW_SYNC alongside KP2BW_ID.

  • --report-uris keepass|bitwarden -- a read-only URI collision report (env
    KP2BW_REPORT_URIS). Groups every login URL by registrable domain (a curated
    two-level public-suffix heuristic, so 10bis.co.il stays whole) and lists the
    domains with more than one host -- exactly the logins that all surface
    together under Bitwarden's base-domain matching. keepass reads the database
    (previewing post-migration collisions); bitwarden reads the live vault
    (honouring -o/-c). It changes nothing -- it just prints, so you can decide
    which entries to switch to Host match (or flip your account's default URI
    match detection).

  • Additional URLs and Android packages migrate as Bitwarden login URIs, not
    custom fields
    -- a KeePass(XC) entry's additional URLs
    (KP2A_URL/KP2A_URL_n, plus the plainer URL/URL_n convention) and
    Android packages (AndroidApp/AndroidApp_n, including the no-underscore
    AndroidApp1 variant) were copied verbatim into custom fields, where they
    were inert. They now become real entries in login.uris, so one login
    autofills across every site and app it covered in KeePass; free-text URL
    labels (API Url, Alt. URL, Website, …) are deliberately left as custom
    fields. Each URI gets a per-URI match mode reproducing KeePassXC's behaviour:
    a plain URL → the account default (match left unset -- what Bitwarden itself
    writes on export; --uri-match / KP2BW_URI_MATCH overrides, e.g. domain
    forces base-domain to replicate KeePassXC's host-based matching), a
    double-quoted URL → exact, and a * wildcard → starts-with (trailing path) or
    regex. AndroidApp becomes an androidapp:// URI. Non-web schemes
    (keepassxc://, cmd://, kdbx://, file://) and unresolved {REF:…} URLs
    are dropped rather than left as dead URIs. --no-interpret-uri-syntax
    (KP2BW_INTERPRET_URI_SYNTAX) disables the quote/wildcard interpretation for
    a literal import. Bitwarden applies a regex to the whole URL (unlike
    KeePassXC's separate host/path regexes), so complex wildcards are emitted as a
    best-effort whole-URL regex with a warning to review. Items imported before
    this are upgraded by a normal re-run (the change is detected and the item
    updated in place); for users who don't want to re-import,
    kp2bw --migrate-uris (env KP2BW_MIGRATE_URIS) is a Bitwarden-only one-shot
    pass that re-folds the legacy fields into URIs on every existing item. Both
    honour --uri-match / --interpret-uri-syntax and -o/-c scope.

  • Configurable per-request HTTP timeout via KP2BW_HTTP_TIMEOUT -- the
    timeout for a single bw serve request is now overridable through the
    KP2BW_HTTP_TIMEOUT environment variable (seconds), so a slow self-hosted
    server (e.g. Vaultwarden) where an individual item write outlasts the default
    no longer times out. Non-numeric or non-positive values are ignored with a
    warning and the default is used; values above the 3600s sanity ceiling are
    clamped (with a warning) so a typo like 999999 can't silently hang a
    migration for hours. The value is applied as httpx's single timeout, which
    stretches the connect/read/write/pool phases together rather than the
    slow-write phase alone. The built-in default is also raised from 60s to 180s,
    since bw serve forwards writes to the (possibly remote) Bitwarden server and
    a single create can legitimately take longer than local work. This complements
    the existing non-fatal-failure handling: raising the ceiling avoids the
    slow-request failure in the first place, rather than only tolerating it on a
    re-run. (#27)

  • --strip-ids finalize mode (env KP2BW_STRIP_IDS) to remove kp2bw's
    KP2BW_ID dedup stamps
    -- every migrated item carries a plain-text
    KP2BW_ID custom field (the KeePass UUID kp2bw matches on for idempotent
    re-runs). Once a user is satisfied the migration is complete and ready to
    fully adopt Bitwarden, kp2bw --strip-ids removes that stamp from every
    migrated item and exits -- no KeePass database is read, no migration runs.
    Scope follows -o/-c exactly as a migration would; other vault data is
    untouched. The strip itself is safe to repeat (a second pass finds nothing),
    but it is irreversible and makes future migration re-runs unreliable:
    without the stamp a re-run falls back to folder + name matching -- the exact
    collision the stamp disambiguates -- so entries sharing a folder and title can
    be duplicated or mismatched. Because of that it confirms before changing
    anything (skippable with -y for callers who know what they want) -- a
    deliberate final step, not a routine flag. (#28)

v3.5.0

Choose a tag to compare

@kjanat kjanat released this 10 Jun 10:43
v3.5.0
88003a0

PyPI

Added

  • Automatic .env loading and a KP2BW_KEEPASS_FILE env var for the database
    path
    -- kp2bw now loads a .env file (searched upward from the current
    working directory) into the environment on startup, so settings can live in a
    file instead of your shell history; a real shell variable still overrides a
    .env entry, keeping the documented CLI flag > env var > default precedence.
    The KeePass database path -- previously CLI-only -- can now be supplied via
    KP2BW_KEEPASS_FILE, making the positional FILE optional. A new
    .env.example documents every supported variable.
  • Always-on DEBUG log file -- a complete DEBUG trace (including third-party
    httpx/bw serve detail) is now always written to a per-user log file
    regardless of console verbosity -- %LOCALAPPDATA%\kp2bw\logs on Windows, the
    platform data dir elsewhere -- so a failed run leaves a full record to share
    without re-running with -v/-d. Override the file with KP2BW_LOG_FILE or
    the directory with KP2BW_LOG_DIR. The console stays as quiet as before by
    default.
  • A heads-up when the bw serve server and CLI versions disagree -- a
    server/CLI version mismatch (a common source of confusing bw serve failures)
    is flagged up front instead of surfacing later as an opaque error.

Changed

  • KeePass tags and expiry now fold into a single KP2BW_META field;
    Created/Modified are no longer migrated
    -- the metadata Bitwarden has no
    native slot for is serialised as YAML into one KP2BW_META custom field
    (PyYAML safe_dump, so control characters and the U+0085/2028/2029 line
    breaks are escaped, not silently corrupted) instead of several separate
    Tags/Expires/date fields, and the field is omitted entirely when an entry
    has neither tags nor an expiry. Creation/modification timestamps are dropped:
    Bitwarden manages its own creation/revision dates and the API cannot backdate
    them (a client-supplied date is ignored on create and rejected on update), so
    they had no real home.

Removed

  • The unused legacy bitwardenclient and bw_import modules -- the
    deprecated subprocess-per-operation CLI wrapper and the file-based bw import
    path, both long superseded by the bw serve HTTP transport and reachable from
    no supported entry point, were removed (git history retains them for
    reference).

Fixed

  • kp2bw --version (and usage/error messages) now print kp2bw, not the
    launcher path.
    Python 3.14's argparse derives the default program name from
    how the script was launched, so a console-script run through uv's trampoline
    printed python.exe C:\...\Scripts\kp2bw 3.4.1. The program name is now
    pinned to kp2bw.
  • Distinct entries sharing a title no longer collapse onto one Bitwarden item
    (silent data loss)
    -- deduplication keyed on (folder, title), so several
    different logins that happened to share a title (e.g. four accounts all named
    192.168.2.67) merged into a single item and re-runs churned
    non-idempotently. Every migrated item now carries its source KeePass entry
    UUID in a KP2BW_ID field and dedup keys on that: a match by UUID stays
    idempotent across title/folder edits, an unstamped legacy item is adopted once
    and back-stamped, and only a genuinely new entry creates a new item.
  • A single slow or dropped bw serve request no longer aborts the whole
    migration
    (#24) -- a create that timed out or hit a dropped keep-alive
    connection (httpx.ReadTimeout/ReadError) crashed the run and stranded
    every entry after it. Idempotent requests (including the startup sync/unlock)
    are now retried on a transient transport error, and a per-entry create,
    folder, or collection failure is reported and skipped rather than fatal -- so
    the migration finishes, the summary counts what failed, and a re-run safely
    adopts anything a timed-out request already created server-side.
  • bw serve HTTP errors now carry the server's actual message -- a failed
    request surfaces the response body (Bitwarden/Vaultwarden's real message /
    validation error) instead of an opaque HTTP 400.
  • Windows: orphaned bw serve processes are reaped reliably -- a
    shim-launched bw serve runs as a node grandchild that taskkill /T did
    not always reap, leaving orphans that deadlocked the shared bw app-data on
    later runs. Teardown now also kills whatever still listens on the serve port,
    regardless of process-tree shape.

Full Changelog: v3.4.1...v3.5.0

v3.4.1

Choose a tag to compare

@kjanat kjanat released this 08 Jun 22:02
v3.4.1
65952da

PyPI

Fixed

  • A newly created item could lose an attachment on re-run -- when kp2bw
    created a fresh Bitwarden item and immediately uploaded its attachment,
    bw serve could fail to resolve the just-created item (it looks the id up in
    its local vault cache, not on the server), report Not found, and silently
    drop that one attachment. The vault is now synced after items are created and
    before attachments upload, and an upload that still hits a not-found error
    syncs and retries once.

Full Changelog: v3.4.0...v3.4.1

v3.4.0

Choose a tag to compare

@kjanat kjanat released this 08 Jun 20:56
v3.4.0
e32f63f

PyPI

Added

  • Reproducible snapshot-backed Vaultwarden e2e suite (#20) -- the
    integration test migrates a comprehensive KeePass seed (every TOTP shape, text
    and hidden custom fields, real file/image attachments, nested folders,
    unicode, empty password) into a real Vaultwarden and captures the result as
    normalized golden snapshots under tests/__snapshots__/, so migration drift
    surfaces as a reviewable diff; idempotency is proven at the snapshot level
    (pass 1 == pass 2). CI runs a host-mode @bitwarden/cli version matrix via a
    new setup-bw composite action -- the pinned leg gates the golden, latest
    is a behavioral-only canary -- with Vaultwarden and bw version-pinned and
    Dependabot-managed.
  • --include-oversize-secrets to recover over-limit secret fields (#21) --
    a hidden OTP secret (e.g. an HOTP HmacOtp-Secret), a passkey attribute, or a
    KeePass-protected custom field whose value tops the 10k inline limit survives
    nowhere else, so it was dropped. The new flag (env:
    KP2BW_INCLUDE_OVERSIZE_SECRETS, default off) offloads it to a <key>.txt
    attachment like any other long field. Off by default so a secret is never
    written to a readable attachment without consent.

Fixed

  • Oversize custom fields routed to their attachment, not inline (#21) -- a
    custom field whose value exceeds the 10k inline limit is offloaded to its
    <key>.txt attachment only, decided at the source when the Bitwarden item is
    built (symmetric with how a long note goes to notes.txt), instead of being
    left among the inline fields. Regression coverage locks the long-field →
    <key>.txt path into the golden e2e snapshots.
  • Over-limit secret custom fields were dropped silently (#21) -- a hidden
    OTP secret, passkey attribute, or KeePass-protected field longer than the 10k
    inline limit was filtered out of the inline fields and excluded from the
    .txt attachment offload, vanishing with no log line. Such a field is now
    warned-and-dropped by default
    (pointing at --include-oversize-secrets to keep it), so data is never lost
    without notice. A consumed OTP key over the limit is still dropped silently --
    it is already preserved in login.totp, so dropping the raw field is
    deduplication, not loss.

What's Changed

  • Reproducible Vaultwarden e2e with golden snapshots by @kjanat in #20
  • fix: route oversize custom fields to .txt; stop silently dropping oversize secrets by @kjanat in #23

Full Changelog: v3.3.0...v3.4.0

v3.3.0

Choose a tag to compare

@kjanat kjanat released this 08 Jun 10:48
v3.3.0
254105b

PyPI

Added

  • In-place updates for changed entries -- re-running kp2bw now syncs
    KeePass edits onto existing Bitwarden items instead of skipping them. Changed
    notes, passwords, usernames, URIs and custom fields are written via an
    idempotent PUT (an unchanged entry issues no request). Collection
    membership is only ever added, never removed, and a Bitwarden-side favorite
    flag or a passkey absent from KeePass is preserved. Opt out with --no-update
    (env: KP2BW_UPDATE=0) to restore the previous skip-only behavior.

Fixed

  • Updated KeePass notes never reached Bitwarden (#11) -- editing an entry's
    notes (e.g. pasting in new recovery keys) without touching credentials left
    the existing Bitwarden item unchanged, forcing a full vault purge to
    re-import. Existing items are now updated in place on re-run.
  • Long notes not attached to (or refreshed on) previously imported entries
    (#11) -- notes over 10k chars migrate to a notes.txt attachment, but
    previously imported (skipped) entries never received it, and an edited
    attachment that kept the same filename was never updated. Re-runs now upload
    any attachment an existing item is missing and refresh one whose content
    changed (the stale copy is removed only after the replacement uploads), all
    without creating duplicates. Applies to every attachment kp2bw manages --
    long notes, long custom fields, and real KeePass file attachments.
  • A single rejected attachment aborted the whole migration (#11) -- an
    attachment the server refused (for example a .jpg rejected for premium or
    storage-quota reasons) raised an opaque HTTP 400 that stopped everything.
    Upload failures are now non-fatal: the real server message is surfaced
    instead of just the status code, and the migration continues with the
    remaining entries.

What's Changed

Full Changelog: v3.2.0...v3.3.0

v3.2.0

Choose a tag to compare

@kjanat kjanat released this 08 Jun 05:08
v3.2.0
b314f9a

PyPI

Added

  • Windows shim support (bw.cmd/bw.bat/bw.ps1) -- Installer methods put
    different shims on PATH and CreateProcess can only run real .exe/.com
    images. resolve_bw_command now resolves all flavours, most-reliable first:
    native bw.exe/bw.com run directly; bw.cmd/bw.bat are routed through
    cmd.exe /c (invoked by basename from their own directory to avoid
    shell-quoting issues); and a bw.ps1 (which isn't in PATHEXT, so
    shutil.which can't see it) is found on PATH and run through PowerShell. So
    when npm ships both bw.cmd and bw.ps1, the cmd shim is preferred.
    terminate_serve tears the bw serve process tree down with taskkill /F /T
    so the real server isn't orphaned behind a cmd.exe/PowerShell wrapper. A
    windows-bw-cmd CI job installs bw via npm and runs a live smoke test
    (tests/windows_bw_cmd_smoke.py) covering shim invocation (bw --version) and
    cmd.exe-wrapped process-tree teardown. Fixes #8.

Fixed

  • Missing bw CLI traceback -- When the Bitwarden CLI (bw) was not on
    PATH, kp2bw crashed with a long, intimidating FileNotFoundError
    traceback from subprocess. The CLI now checks for bw up front (before
    prompting for passwords) and exits cleanly with an actionable message; any
    BitwardenClientError/ConversionError raised during conversion is reported
    the same way instead of as a stack trace. BitwardenServeClient raises a
    BitwardenClientError rather than letting FileNotFoundError escape.
    Detection uses shutil.which, so Windows bw.exe/bw.cmd shims are found via
    PATHEXT; the bw subprocess calls also catch FileNotFoundError, so a
    genuinely missing CLI still yields the friendly message. Fixes #5.
  • Chained {REF:...} references -- a reference whose target was itself
    another reference entry (a chain A -> B -> C) raised KeyError in
    _resolve_entries_with_references, logged a Could not resolve entry
    warning, and dropped the referencing entry from the import even though
    KeePass resolves such chains correctly. Unresolved targets that are
    themselves REF entries are now resolved transitively and on demand, with
    memoization and cycle detection, so the chain collapses onto whatever it
    ultimately maps to. Fixes #6.
  • Malformed {REF:...} tokens no longer abort the run -- a reference whose
    field/lookup part lacked the @ separator (e.g. {REF:UI:...}) raised an
    uncaught ValueError in _parse_kp_ref_string that stopped the whole
    migration. Such tokens are now reported and the offending entry is skipped,
    consistent with other unresolvable references.

What's Changed

Full Changelog: v3.1.0...v3.2.0

v3.1.0

Choose a tag to compare

@kjanat kjanat released this 08 Jun 04:04
v3.1.0
f71df5e

PyPI

Added

  • KeePass2/KeePassXC native TOTP migration -- entries that store TOTP in the
    TimeOtp-* custom fields (rather than the otp field) now migrate to
    Bitwarden's login.totp. All four KeePass secret encodings are supported
    (TimeOtp-Secret UTF-8, -Hex, -Base32, -Base64), and non-default
    TimeOtp-Length / -Period / -Algorithm settings are emitted as a full
    otpauth:// URI so Bitwarden generates correct codes instead of silently
    defaulting to 6 digits / 30 s / SHA-1. A default-config Base32 secret still
    migrates as a bare secret; entry.otp keeps precedence when both are present.
    Logic lives in a new pure, unit-tested kp2bw/otp.py module.

Fixed

  • Lossy/leaky TOTP fallback -- the initial TimeOtp-Secret-Base32 fallback
    dropped non-default OTP configuration (producing wrong 2FA codes), ignored the
    other three secret encodings, and stripped the Base32 secret from custom
    fields even when it was not the value migrated. Secrets are now removed from
    custom fields only when actually folded into login.totp; any OTP secret left
    behind (HOTP, an undecodable value, or one shadowed by entry.otp) is
    preserved as a hidden custom field rather than dropped or exposed.
  • Silent HOTP loss -- counter-based HOTP (HmacOtp-Secret*) has no
    time-based target in Bitwarden. It is now reported with a warning and its
    secret kept as a hidden field, instead of silently becoming a visible
    plaintext custom field.

What's Changed

  • feat: lossless KeePass TOTP/HOTP migration by @kjanat in #15 — thanks @Eryniox for the original implementation in #12 🙏

Full Changelog: v3.0.1...v3.1.0

v3.0.1

Choose a tag to compare

@kjanat kjanat released this 08 Jun 02:10
v3.0.1
4e432c0

PyPI

Fixed

  • None username/password in REF resolution -- _resolve_entries_with_references raised TypeError: argument of type 'NoneType' is not iterable when an entry with a {REF:...} field also had a None username or password. The field is now guarded before the KP_REF_IDENTIFIER membership test, and None values are normalized to "" for the username/password match so a resolved entry merges its URI instead of spawning a duplicate. Fixes #9, #4.

What's Changed

  • chore(deps): bump black from 26.1.0 to 26.3.1 in the uv group across 1 directory by @dependabot[bot] in #7
  • chore(deps): bump the uv group across 1 directory with 3 updates by @dependabot[bot] in #13
  • Handle None values in reference resolution by @BrainStone in #10 — thanks @szotsaki for reporting #4 🙏

New Contributors

Full Changelog: v3.0.0...v3.0.1

v3.0.0

Choose a tag to compare

@kjanat kjanat released this 26 Feb 22:33
v3.0.0
59ba3c2

PyPI

Highlights

Stable release of the bw serve HTTP transport rewrite. All features from the 3.0.0a1/a2 prereleases, plus:

  • Strict TypedDict API layerBwItemCreate, BwItemResponse, BwItemLogin, BwUri, BwField, BwFido2Credential, BwFolder, BwCollection. All dict[str, Any] eliminated from transport code. Generated _bw_api_types.py is the canonical source for spec-derived shapes.
  • Org-scoped dedup index — Fixes collection-blind imports where personal-vault entries shadowed an empty org vault. Dedup is now scoped to organizationId and optionally collectionId.
  • Collection-aware import — Items already in the org but in a different collection get a PUT to add the target collection instead of being silently skipped.
  • Rich progress bars — Live progress for processing, creating, and uploading phases, plus a final migration summary.
  • FIDO2/passkey migration — KeePassXC KPEX_PASSKEY_* attributes converted to Bitwarden fido2Credentials.
  • Codegen drift CIcodegen-check.yml fails PRs when _bw_api_types.py drifts from specs/vault-management-api.json.

New since v2.0.0

  • bw serve HTTP transport (persistent process, automatic port, health polling, signal-safe cleanup) by @kjanat in #2
  • Batch import via bw import for bulk creation
  • Async parallel attachment uploads (bounded concurrency)
  • O(1) dedup index (no more per-item API lookups) by @kjanat in #3
  • -d / --debug flag for full DEBUG logging
  • -v now shows kp2bw operational detail only (httpx silenced)
  • KeePass metadata migration (tags, expiry, timestamps) as custom fields
  • --skip-expired / --include-recycle-bin / --metadata flags
  • CLI help metavars improved
  • All env vars documented (KP2BW_*)

Key fixes

  • Shell injection in bw import (replaced shell=True)
  • bw serve IPv6 binding mismatch causing 60s timeout
  • organizationId query param casing on collection list
  • Dedup cache stale after collection PUT
  • Signal handler init race on early timeout
  • close() double-call crash
  • Sensitive secrets redacted from diagnostic output

Breaking changes

  • Requires Python ≥ 3.14
  • New dependencies: httpx>=0.28.0, rich>=13.0.0
  • Transport layer completely replaced — CLI behavior is the same but internal API changed

Full Changelog: v2.0.0...v3.0.0