From 4104f31c4a4bf4354ef0f1448474d80dca0de454 Mon Sep 17 00:00:00 2001 From: Lukas Niederbremer Date: Thu, 5 Mar 2026 01:18:16 +0100 Subject: [PATCH 1/4] Add SSH Key support in Bitwarden import Fixes #12778 --- src/format/BitwardenReader.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/format/BitwardenReader.cpp b/src/format/BitwardenReader.cpp index 43b2a34f3..0573e627c 100644 --- a/src/format/BitwardenReader.cpp +++ b/src/format/BitwardenReader.cpp @@ -257,6 +257,34 @@ namespace } } + // Parse SSH-Key entry, if present + if (itemMap.contains("sshKey")) { + const auto sshKey = itemMap.value("sshKey").toMap(); + + if (sshKey.contains("publicKey") && sshKey.contains("privateKey")) { + const auto publicKey = sshKey.value("publicKey").toString(); + + auto baseName = QString("id"); + + if (publicKey.startsWith("ssh-ed25519 ")) { + baseName.append("_ed25519"); + } else if (publicKey.startsWith("ssh-rsa ") + || publicKey.startsWith("ssh-sha2-256") + || publicKey.startsWith("ssh-sha2-512")) { + baseName.append("_rsa"); + } else if (publicKey.startsWith("ecdsa-sha2-nistp256 ") + || publicKey.startsWith("ecdsa-sha2-nistp384") + || publicKey.startsWith("ecdsa-sha2-nistp521")) { + baseName.append("_ecdsa"); + } else if (publicKey.startsWith("ssh-dss")) { + baseName.append("_dss"); + } + + entry->attachments()->set(baseName + ".pub", sshKey.value("publicKey").toString().toUtf8()); + entry->attachments()->set(baseName, sshKey.value("privateKey").toString().toUtf8()); + } + } + entry->setEmitModified(true); return entry.take(); } From 15a11ac936fcbb4948ca02f2522f8a08ff10ebf0 Mon Sep 17 00:00:00 2001 From: Lukas Niederbremer Date: Thu, 5 Mar 2026 01:54:32 +0100 Subject: [PATCH 2/4] Add unit tests for Bitwarden SSH Key import --- tests/TestImports.cpp | 78 ++++++++++++++++++++++++++++ tests/TestImports.h | 1 + tests/data/bitwarden_export_ssh.json | 40 ++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 tests/data/bitwarden_export_ssh.json diff --git a/tests/TestImports.cpp b/tests/TestImports.cpp index d17beb389..53c4683ba 100644 --- a/tests/TestImports.cpp +++ b/tests/TestImports.cpp @@ -327,6 +327,84 @@ void TestImports::testBitwardenPasskey() QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw")); } +void TestImports::testBitwardenSSHKey() +{ + auto bitwardenPath = QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_export_ssh.json")); + + BitwardenReader reader; + auto db = reader.convert(bitwardenPath); + QVERIFY2(!reader.hasError(), qPrintable(reader.errorString())); + QVERIFY(db); + + // Confirm Login fields + // Test expected RSA Key + + auto entry = db->rootGroup()->findEntryByPath("/SSH RSA Key"); + QVERIFY(entry); + + // Public and private key + QVERIFY(entry->attachments()->keys().length() == 2); + + // Test expected names + QVERIFY(entry->attachments()->hasKey("id_rsa")); + QVERIFY(entry->attachments()->hasKey("id_rsa.pub")); + + // Test expected contents + QCOMPARE(entry->attachments()->value("id_rsa"), + QByteArray("-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\n" + "NhAAAAAwEAAQAAAQEAqF8zLkrpDi129pdQnR6WrEGcASFd9J0hxnrvrx8epuLvR83OsFNz\n" + "gcI3K9eqm3W00wuSqRmOfqtHZMix8K6yrVHWgivaJU+k72DE3tOOlPSfaPdZ24kbZhW8ta\n" + "21+O1hP3zCJrEpVP3leKrji8mOSUYXbSJg7gQlrMXFr2dYgHE1w07aHk0vYS49O0am9m+l\n" + "pLnhUlFrZj556+yIoRPKNnyRpRO/wjGg0PbRPMceOmK+phQ1PVblwUBGoU82U14k6mWiAr\n" + "fj8yzTxcOYxKIXPqB31Bxd3D2LTcz1JZT5nbcJNDQ6cb5DqOAmNV7c2F5puwBqIZHmXmtj\n" + "SWSrOcpHjQAAA8gHdoH6B3aB+gAAAAdzc2gtcnNhAAABAQCoXzMuSukOLXb2l1CdHpasQZ\n" + "wBIV30nSHGeu+vHx6m4u9Hzc6wU3OBwjcr16qbdbTTC5KpGY5+q0dkyLHwrrKtUdaCK9ol\n" + "T6TvYMTe046U9J9o91nbiRtmFby1rbX47WE/fMImsSlU/eV4quOLyY5JRhdtImDuBCWsxc\n" + "WvZ1iAcTXDTtoeTS9hLj07Rqb2b6WkueFSUWtmPnnr7IihE8o2fJGlE7/CMaDQ9tE8xx46\n" + "Yr6mFDU9VuXBQEahTzZTXiTqZaICt+PzLNPFw5jEohc+oHfUHF3cPYtNzPUllPmdtwk0ND\n" + "pxvkOo4CY1XtzYXmm7AGohkeZea2NJZKs5ykeNAAAAAwEAAQAAAQAHChPlt5QO16/Fl4Xz\n" + "S7gY85VGJtL6yycCWVl0BOUPLSW75srhbFvD7Q7JcnbbkQxCVpWHJF5kxVxyxkFKQsONo4\n" + "JIZvTz4mSO7YjNmCK575BKnyzOlOjkV7xQDDczdRk/wkOLwpRrzUGuzdY9neuo/Jk2It3S\n" + "lbHNi2c8ciGtHP0w8QUZGv1Cz4aaxcbX18pywUdmjN0+KPPXKKSDuokO8KMPN8ZvKfwZrJ\n" + "fkX+XGgY4AIkE72Z4V4HtiM95WWS+PmVYy+P/FMRFGv8Rh6xM2sAaT89Sy6f8bDkcQENLp\n" + "HnP65xNU9gVTdcAHGWlV0HsNmCDudGfygDUUMkt8zQwBAAAAgQDIDJFnNMushRbf3qrVNK\n" + "E8xZBkyUfRrRs9fMHkvNSCrduQO+LNX67P7ixRg0D5yP4o1KS7dmPenRi45d31usIwcUio\n" + "Nu4M5qMadPOoU4pSoQK4L9FxxWj38BZhJaiTULnevsK5OWJVHUIBwS8qWCExTwFswwSz+G\n" + "V4xvboxKteAwAAAIEA3Xj1hbqKSDAKSN4Q63u8NEoE+KCihplLs6ZsVPDP+MIdVBtoux7B\n" + "VrPbAIECDurFnj/gaE97uQ40auXbd5NDUm0kjIhmeuUWZXvQTNNU52HI1nDr2egB36Mf0s\n" + "oWvK3s/Bs0z4eM+aGalgVBvpnpSeYmV5STMdstgNP8A3V1W00AAACBAMKe+jaqwq6Xd4lD\n" + "wtk5zQP9TS+3o1PjKqVfncGrHx6tZKjPShHRXgu74D9D0w91y8toDA7hoXDDn7wWtdf02B\n" + "8Hcp76lGyWawkG9N9n0oAMYEtu7G/j6UNMviCkwNs2q02f//TQpyv+A9X+pdwq2rIjCUyc\n" + """MU6WqS6pCoG7T/1BAAAADWNvbW1lbnQtZmllbGQBAgMEBQ==\n" + "-----END OPENSSH PRIVATE KEY-----\n")); + QCOMPARE(entry->attachments()->value("id_rsa.pub"), + QByteArray("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCoXzMuSukOLXb2l1CdHpasQZwBIV30nSHGeu+vHx6m4u9Hzc6wU3OBwjcr16qbdbTTC5KpGY5+q0dkyLHwrrKtUdaCK9olT6TvYMTe046U9J9o91nbiRtmFby1rbX47WE/fMImsSlU/eV4quOLyY5JRhdtImDuBCWsxcWvZ1iAcTXDTtoeTS9hLj07Rqb2b6WkueFSUWtmPnnr7IihE8o2fJGlE7/CMaDQ9tE8xx46Yr6mFDU9VuXBQEahTzZTXiTqZaICt+PzLNPFw5jEohc+oHfUHF3cPYtNzPUllPmdtwk0NDpxvkOo4CY1XtzYXmm7AGohkeZea2NJZKs5ykeN comment-field")); + + entry = db->rootGroup()->findEntryByPath("/SSH Ed25519 Key"); + + QVERIFY(entry); + + // Public and private key + QVERIFY(entry->attachments()->keys().length() == 2); + + // Test expected names + QVERIFY(entry->attachments()->hasKey("id_ed25519")); + QVERIFY(entry->attachments()->hasKey("id_ed25519.pub")); + + // Test expected contents + QCOMPARE(entry->attachments()->value("id_ed25519"), + QByteArray("-----BEGIN OPENSSH PRIVATE KEY-----\n" + "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\n" + "QyNTUxOQAAACB9f1yjUpItVaL1Z6qBiYe0brp1GDnCiTQgBuZir4vwQAAAAJBvHurObx7q\n" + "zgAAAAtzc2gtZWQyNTUxOQAAACB9f1yjUpItVaL1Z6qBiYe0brp1GDnCiTQgBuZir4vwQA\n" + "AAAECIazL429hSbLe02vjjDKfd5sn6YXGVMegywgMhBx4asH1/XKNSki1VovVnqoGJh7Ru\n" + "unUYOcKJNCAG5mKvi/BAAAAADWNvbW1lbnQtZmllbGQ=\n" + "-----END OPENSSH PRIVATE KEY-----\n")); + QCOMPARE(entry->attachments()->value("id_ed25519.pub"), + QByteArray("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH1/XKNSki1VovVnqoGJh7RuunUYOcKJNCAG5mKvi/BA comment-field")); +} + void TestImports::testProtonPass() { auto protonPassPath = diff --git a/tests/TestImports.h b/tests/TestImports.h index 728fa6377..5638af741 100644 --- a/tests/TestImports.h +++ b/tests/TestImports.h @@ -31,6 +31,7 @@ private slots: void testBitwarden(); void testBitwardenEncrypted(); void testBitwardenPasskey(); + void testBitwardenSSHKey(); void testProtonPass(); }; diff --git a/tests/data/bitwarden_export_ssh.json b/tests/data/bitwarden_export_ssh.json new file mode 100644 index 000000000..1b8ba4e8c --- /dev/null +++ b/tests/data/bitwarden_export_ssh.json @@ -0,0 +1,40 @@ +{ + "folders": [], + "items": [ + { + "passwordHistory": [], + "revisionDate": "2026-03-05T12:09:28.531Z", + "creationDate": "2026-03-05T12:05:30.549Z", + "id": "3b085e11-c2cd-42f3-a0b2-59e83e3dd3b9", + "type": 5, + "reprompt": 0, + "name": "SSH RSA Key", + "notes": null, + "favorite": false, + "fields": [], + "sshKey": { + "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAQEAqF8zLkrpDi129pdQnR6WrEGcASFd9J0hxnrvrx8epuLvR83OsFNz\ngcI3K9eqm3W00wuSqRmOfqtHZMix8K6yrVHWgivaJU+k72DE3tOOlPSfaPdZ24kbZhW8ta\n21+O1hP3zCJrEpVP3leKrji8mOSUYXbSJg7gQlrMXFr2dYgHE1w07aHk0vYS49O0am9m+l\npLnhUlFrZj556+yIoRPKNnyRpRO/wjGg0PbRPMceOmK+phQ1PVblwUBGoU82U14k6mWiAr\nfj8yzTxcOYxKIXPqB31Bxd3D2LTcz1JZT5nbcJNDQ6cb5DqOAmNV7c2F5puwBqIZHmXmtj\nSWSrOcpHjQAAA8gHdoH6B3aB+gAAAAdzc2gtcnNhAAABAQCoXzMuSukOLXb2l1CdHpasQZ\nwBIV30nSHGeu+vHx6m4u9Hzc6wU3OBwjcr16qbdbTTC5KpGY5+q0dkyLHwrrKtUdaCK9ol\nT6TvYMTe046U9J9o91nbiRtmFby1rbX47WE/fMImsSlU/eV4quOLyY5JRhdtImDuBCWsxc\nWvZ1iAcTXDTtoeTS9hLj07Rqb2b6WkueFSUWtmPnnr7IihE8o2fJGlE7/CMaDQ9tE8xx46\nYr6mFDU9VuXBQEahTzZTXiTqZaICt+PzLNPFw5jEohc+oHfUHF3cPYtNzPUllPmdtwk0ND\npxvkOo4CY1XtzYXmm7AGohkeZea2NJZKs5ykeNAAAAAwEAAQAAAQAHChPlt5QO16/Fl4Xz\nS7gY85VGJtL6yycCWVl0BOUPLSW75srhbFvD7Q7JcnbbkQxCVpWHJF5kxVxyxkFKQsONo4\nJIZvTz4mSO7YjNmCK575BKnyzOlOjkV7xQDDczdRk/wkOLwpRrzUGuzdY9neuo/Jk2It3S\nlbHNi2c8ciGtHP0w8QUZGv1Cz4aaxcbX18pywUdmjN0+KPPXKKSDuokO8KMPN8ZvKfwZrJ\nfkX+XGgY4AIkE72Z4V4HtiM95WWS+PmVYy+P/FMRFGv8Rh6xM2sAaT89Sy6f8bDkcQENLp\nHnP65xNU9gVTdcAHGWlV0HsNmCDudGfygDUUMkt8zQwBAAAAgQDIDJFnNMushRbf3qrVNK\nE8xZBkyUfRrRs9fMHkvNSCrduQO+LNX67P7ixRg0D5yP4o1KS7dmPenRi45d31usIwcUio\nNu4M5qMadPOoU4pSoQK4L9FxxWj38BZhJaiTULnevsK5OWJVHUIBwS8qWCExTwFswwSz+G\nV4xvboxKteAwAAAIEA3Xj1hbqKSDAKSN4Q63u8NEoE+KCihplLs6ZsVPDP+MIdVBtoux7B\nVrPbAIECDurFnj/gaE97uQ40auXbd5NDUm0kjIhmeuUWZXvQTNNU52HI1nDr2egB36Mf0s\noWvK3s/Bs0z4eM+aGalgVBvpnpSeYmV5STMdstgNP8A3V1W00AAACBAMKe+jaqwq6Xd4lD\nwtk5zQP9TS+3o1PjKqVfncGrHx6tZKjPShHRXgu74D9D0w91y8toDA7hoXDDn7wWtdf02B\n8Hcp76lGyWawkG9N9n0oAMYEtu7G/j6UNMviCkwNs2q02f//TQpyv+A9X+pdwq2rIjCUyc\nMU6WqS6pCoG7T/1BAAAADWNvbW1lbnQtZmllbGQBAgMEBQ==\n-----END OPENSSH PRIVATE KEY-----\n", + "publicKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCoXzMuSukOLXb2l1CdHpasQZwBIV30nSHGeu+vHx6m4u9Hzc6wU3OBwjcr16qbdbTTC5KpGY5+q0dkyLHwrrKtUdaCK9olT6TvYMTe046U9J9o91nbiRtmFby1rbX47WE/fMImsSlU/eV4quOLyY5JRhdtImDuBCWsxcWvZ1iAcTXDTtoeTS9hLj07Rqb2b6WkueFSUWtmPnnr7IihE8o2fJGlE7/CMaDQ9tE8xx46Yr6mFDU9VuXBQEahTzZTXiTqZaICt+PzLNPFw5jEohc+oHfUHF3cPYtNzPUllPmdtwk0NDpxvkOo4CY1XtzYXmm7AGohkeZea2NJZKs5ykeN comment-field", + "keyFingerprint": "SHA256:lO60nR+FVwqLeV3d7BD9i9QutNwu5LYbV4NOVDrHV9c" + }, + "collectionIds": null + }, { + "passwordHistory": [], + "revisionDate": "2026-03-05T12:09:28.531Z", + "creationDate": "2026-03-05T12:05:30.549Z", + "id": "be6d0a99-9981-457e-9aba-963b9854fd2f", + "type": 5, + "reprompt": 0, + "name": "SSH Ed25519 Key", + "notes": null, + "favorite": false, + "fields": [], + "sshKey": { + "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACB9f1yjUpItVaL1Z6qBiYe0brp1GDnCiTQgBuZir4vwQAAAAJBvHurObx7q\nzgAAAAtzc2gtZWQyNTUxOQAAACB9f1yjUpItVaL1Z6qBiYe0brp1GDnCiTQgBuZir4vwQA\nAAAECIazL429hSbLe02vjjDKfd5sn6YXGVMegywgMhBx4asH1/XKNSki1VovVnqoGJh7Ru\nunUYOcKJNCAG5mKvi/BAAAAADWNvbW1lbnQtZmllbGQ=\n-----END OPENSSH PRIVATE KEY-----\n", + "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH1/XKNSki1VovVnqoGJh7RuunUYOcKJNCAG5mKvi/BA comment-field", + "keyFingerprint": "SHA256:1ZF2xpdCmXtrcY0nHNBgOT7xQmr88XFBiLkNBVzooYY" + }, + "collectionIds": null + } + ] +} From f4e82be7e62c3b23e6900b1149d86789e743e6af Mon Sep 17 00:00:00 2001 From: Lukas Niederbremer Date: Thu, 5 Mar 2026 01:57:55 +0100 Subject: [PATCH 3/4] Add missing spaces in SSH key type determination --- src/format/BitwardenReader.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/format/BitwardenReader.cpp b/src/format/BitwardenReader.cpp index 0573e627c..b23117b28 100644 --- a/src/format/BitwardenReader.cpp +++ b/src/format/BitwardenReader.cpp @@ -269,14 +269,14 @@ namespace if (publicKey.startsWith("ssh-ed25519 ")) { baseName.append("_ed25519"); } else if (publicKey.startsWith("ssh-rsa ") - || publicKey.startsWith("ssh-sha2-256") - || publicKey.startsWith("ssh-sha2-512")) { + || publicKey.startsWith("ssh-sha2-256 ") + || publicKey.startsWith("ssh-sha2-512 ")) { baseName.append("_rsa"); } else if (publicKey.startsWith("ecdsa-sha2-nistp256 ") - || publicKey.startsWith("ecdsa-sha2-nistp384") - || publicKey.startsWith("ecdsa-sha2-nistp521")) { + || publicKey.startsWith("ecdsa-sha2-nistp384 ") + || publicKey.startsWith("ecdsa-sha2-nistp521 ")) { baseName.append("_ecdsa"); - } else if (publicKey.startsWith("ssh-dss")) { + } else if (publicKey.startsWith("ssh-dss ")) { baseName.append("_dss"); } From 48fdc08ca5b0100c5ec0a3bfd4cae0d78562afc4 Mon Sep 17 00:00:00 2001 From: Lukas Niederbremer Date: Thu, 5 Mar 2026 02:14:03 +0100 Subject: [PATCH 4/4] Ignore empty or invalid SSH key components --- src/format/BitwardenReader.cpp | 35 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/format/BitwardenReader.cpp b/src/format/BitwardenReader.cpp index b23117b28..a9ec6248d 100644 --- a/src/format/BitwardenReader.cpp +++ b/src/format/BitwardenReader.cpp @@ -263,25 +263,28 @@ namespace if (sshKey.contains("publicKey") && sshKey.contains("privateKey")) { const auto publicKey = sshKey.value("publicKey").toString(); + const auto privateKey = sshKey.value("privateKey").toString(); - auto baseName = QString("id"); + if (!publicKey.isEmpty() && !privateKey.isEmpty()) { + auto baseName = QString("id"); - if (publicKey.startsWith("ssh-ed25519 ")) { - baseName.append("_ed25519"); - } else if (publicKey.startsWith("ssh-rsa ") - || publicKey.startsWith("ssh-sha2-256 ") - || publicKey.startsWith("ssh-sha2-512 ")) { - baseName.append("_rsa"); - } else if (publicKey.startsWith("ecdsa-sha2-nistp256 ") - || publicKey.startsWith("ecdsa-sha2-nistp384 ") - || publicKey.startsWith("ecdsa-sha2-nistp521 ")) { - baseName.append("_ecdsa"); - } else if (publicKey.startsWith("ssh-dss ")) { - baseName.append("_dss"); + if (publicKey.startsWith("ssh-ed25519 ")) { + baseName.append("_ed25519"); + } else if (publicKey.startsWith("ssh-rsa ") + || publicKey.startsWith("ssh-sha2-256 ") + || publicKey.startsWith("ssh-sha2-512 ")) { + baseName.append("_rsa"); + } else if (publicKey.startsWith("ecdsa-sha2-nistp256 ") + || publicKey.startsWith("ecdsa-sha2-nistp384 ") + || publicKey.startsWith("ecdsa-sha2-nistp521 ")) { + baseName.append("_ecdsa"); + } else if (publicKey.startsWith("ssh-dss ")) { + baseName.append("_dss"); + } + + entry->attachments()->set(baseName + ".pub", publicKey.toUtf8()); + entry->attachments()->set(baseName, privateKey.toUtf8()); } - - entry->attachments()->set(baseName + ".pub", sshKey.value("publicKey").toString().toUtf8()); - entry->attachments()->set(baseName, sshKey.value("privateKey").toString().toUtf8()); } }