diff --git a/src/format/BitwardenReader.cpp b/src/format/BitwardenReader.cpp index 37e3619292..35aa916f2f 100644 --- a/src/format/BitwardenReader.cpp +++ b/src/format/BitwardenReader.cpp @@ -257,6 +257,36 @@ 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(); + const auto privateKey = sshKey.value("privateKey").toString(); + + 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"); + } + + entry->attachments()->set(baseName + ".pub", publicKey.toUtf8()); + entry->attachments()->set(baseName, privateKey.toUtf8()); + } + } + } + entry->setEmitModified(true); return entry.take(); } diff --git a/tests/TestImports.cpp b/tests/TestImports.cpp index 41e5f732cf..ed835a46f4 100644 --- a/tests/TestImports.cpp +++ b/tests/TestImports.cpp @@ -427,6 +427,92 @@ void TestImports::testBitwardenNestedFolders() QCOMPARE(entry->group(), longGroup); } +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 f7d37e5155..d0e19bf8c7 100644 --- a/tests/TestImports.h +++ b/tests/TestImports.h @@ -32,6 +32,7 @@ private slots: void testBitwardenEncrypted(); void testBitwardenPasskey(); void testBitwardenNestedFolders(); + 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 0000000000..1b8ba4e8c1 --- /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 + } + ] +}