Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSH agent client support (KeeAgent compatible) #1098

Merged
merged 3 commits into from
Nov 19, 2017

Conversation

hifi
Copy link
Member

@hifi hifi commented Oct 21, 2017

This implements a ssh-agent client that sends keys from your KeePassXC database to a running agent when a database is unlocked and removes them when it's locked.

Description

New OpenSSH key types are supported (DSA, RSA, ECDSA and ED25519) including decryption. Old keys can be converted to new format with ssh-keygen or PuTTYgen without needing to rotate your public key on remote hosts.

POSIX and Mac agent access is through the environment SSH_AUTH_SOCK UNIX socket and the socket is opened on-demand when keys are added or removed from the agent. Communication is blocking through BinaryStream class that wraps around QIODevice and thus QLocalSocket.

Windows (Pageant) agent access is using shared memory and it shares the same protocol. This has been inlined and #ifdef separated from the socket version as it is quite small.

TODO

  • New OpenSSH key loading
  • New OpenSSH key decryption (through imported 3rd party code)
  • Support for all key formats
    • DSA
    • RSA
    • ECDSA
    • Ed25519
  • Support for external keys (local filesystem path)
  • ssh-agent client for Linux / Mac
    • Confirm constraint
    • Lifetime constraint
  • KeeAgent.settings reading/writing for compatibility
  • Add general setting to enable/disable agent support
  • Add per-entry controls (KeeAgent.settings compatible)
  • Windows (Pageant) support
  • Decrypt private key only when necessary
  • Decrypt private key in edit mode only when requested by user
  • Applying changes refreshes the agent page making new passphrase and attachments available
  • "Dirty" flagging is missing from settings, required to know if attachment needs to be updated or not
  • Check formatting of one line if statements and the position of * and & in arguments/variables
  • Error strings through tr()
  • Keep track of sent keys in DatabaseAgent with private key cleared to avoid having issues automatically removing them on database lock
  • Adding a key manually to agent and having auto remove on lock should work together

Non-goals (updated)

  • Old OpenSSH key format support (manual conversion possible)
  • PuTTY key (ppk) support (manual conversion possible)
  • Implementing an agent server (use ssh-agent on Linux/Mac or Pageant on Windows)

Motivation and context

I didn't know I need KeeAgent. Now I definitely need KeeAgent. Issue #278 is tracking the wishlist item.

How has this been tested?

In daily use by author. If you want to test this please build with -DWITH_XC_SSHAGENT=ON and enable SSH Agent from Settings.

Not working for you?

If the entry agent control buttons are greyed out on Linux/Mac when a key is loaded the environment does not have SSH_AUTH_SOCK variable set. You need to make sure your session has an agent running so that the environment is populated before you start KeePassXC.

On Windows, you can start Pageant at any time. The buttons will light up after you re-open entry edit.

Key conversion tips

New compatible keys can be generated via the following command:

ssh-keygen -o -f id_rsa

Old keys can be converted into the new format like this (make a copy of the key first just in case):

ssh-keygen -o -p -f id_rsa

PuTTYgen can import any key (OpenSSH or PuTTY key) and export them in the new format via the Conversions menu and selecting Export OpenSSH key (force new file format).

Screenshots (if appropriate):

image

Types of changes

  • ✅ New feature (non-breaking change which adds functionality)

Checklist:

  • ✅ I have read the CONTRIBUTING document. [REQUIRED]
  • ✅ My code follows the code style of this project. [REQUIRED]
  • ✅ All new and existing tests passed. [REQUIRED]
  • ✅ I have added tests to cover my changes.

@hifi hifi force-pushed the feature/sshagent branch 6 times, most recently from 1caf560 to d0ea74f Compare October 25, 2017 17:10
@yan12125
Copy link
Contributor

Hi hifi, there's a conflict after #1124 (https://github.com/keepassxreboot/keepassxc/pull/1124/files#diff-7b98a79aea3db9f4e15f2f31fe24c087R103), could you fix that? I always apply useful patches on top of latest git.

@hifi
Copy link
Member Author

hifi commented Oct 27, 2017

@yan12125 Rebased.

@hifi hifi force-pushed the feature/sshagent branch 4 times, most recently from ab208b4 to 5577ed6 Compare October 28, 2017 08:12
@hifi hifi mentioned this pull request Oct 28, 2017
@CRCinAU
Copy link

CRCinAU commented Oct 28, 2017

Ok, so I'm going to apologise for this in advance, but:

You sir, are a fucking legend.

@mfulz
Copy link

mfulz commented Oct 28, 2017

Trying to use that but I've just always the comment "unknown cipher aes256-ctr" and can't add keys to agent. Can you help me?
More Info: Sorry now I understood this: Adding the privkey as attachment and then referencing it is the intentional way, isn't it?
I was thinking I can store the pw and just decrypt the privkey.

@yan12125
Copy link
Contributor

@mfulz: Support for aes256-ctr is currently missing from this PR. That shouldn't be too long to come :)


As references for developers:

  1. ssh-keygen of OpenSSH switches from aes256-cbc to aes256-ctr since version 7.6 openssh/openssh-portable@0f34553. Before that the default had always been aes256-cbc.
  2. -Z can force another cipher name (e.g., ssh-keygen -f test-ed25519 -t ed25519 -Z aes256-cbc) It is an undocumented option, so it should be safe to assume there are only aes256-cbc and aes256-ctr keys in the wild.
  3. Seems only a few more lines are necessary for aes256-ctr as gcrypt already supports CTR mode.

@mfulz
Copy link

mfulz commented Oct 28, 2017

One Bug: I'f auto adding key when db is unlocked the user confirmation is not working.

@mfulz
Copy link

mfulz commented Oct 28, 2017

@yan12125 Great thanks for the help, this undocumented option was the missing link. Perfect feature. Just the bug with user confirmation on db unlock is not working.

|| mode == DatabaseWidget::EditMode)
&& !m_sentKeys) {
for (QSharedPointer<OpenSSHKey> e : keys) {
AgentClient::instance()->addIdentity(*e.data());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here lifetime and confirm should be passed, too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to do a bit of refactoring there, it was written before constraints existed.

@hifi
Copy link
Member Author

hifi commented Oct 29, 2017

@yan12125 AES-256-CTR should work with the latest version.

@hifi
Copy link
Member Author

hifi commented Oct 29, 2017

@mfulz You can also use external keys with the Browse button. Like @yan12125 said it was missing decryption support for OpenSSH 7.6+ keys but that has been now sorted.

Will fix constraints not being applied on unlock later today.

@hifi
Copy link
Member Author

hifi commented Oct 29, 2017

@mfulz Constraints should now work if using automatic add on unlock. Thank you for testing.

@hifi hifi force-pushed the feature/sshagent branch 2 times, most recently from 2a7b1d8 to bdad109 Compare October 29, 2017 15:21
@mfulz
Copy link

mfulz commented Oct 29, 2017

@hifi Thank you very much, I can confirm that everything is working now absolutely perfect: aes256-ctr, confirmation on auto unlock.
Great job man !!!

@hifi hifi force-pushed the feature/sshagent branch 2 times, most recently from 0752358 to 9581dba Compare October 30, 2017 17:24
@TheZ3ro
Copy link
Contributor

TheZ3ro commented Nov 12, 2017

@hifi you are reporting errors in the commentEdit field, what about using showMessage()?

void showMessage(const QString& text, MessageWidget::MessageType type);

@hifi
Copy link
Member Author

hifi commented Nov 12, 2017

@TheZ3ro Fixed missing translation, moved to showMessage() error reporting.

Copy link
Member

@droidmonkey droidmonkey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a little bit of work. Overall great job! Instead of rolling your own BinaryStream you need to use the QDataStream which will be inherently platform agnostic.

Visually, on the SSH Agent settings page, align the checkboxes with the second column (ie not the label text) to give a cleaner appearance.

@@ -28,6 +29,7 @@
#include <QMenu>
#include <QSortFilterProxyModel>
#include <QTemporaryFile>
#include <QtWidgets>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this include line to prevent compile errors on Windows.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think removing it will make Travis fail with older Qt 5.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps you are missing a more specific include then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of you told me on IRC to include QtWidgets instead of the specific ones when I had them in the early incarnations. Please decide.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I don't care. But if including QWidgets produces errors on Windows, this needs to be addressed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was throw away advice to get you going. You always want to scope down if possible, and especially if it causes issues. The ui_XXXX.h files bring in all the widget headers you need. It was likely a false positive that Travis was barfing on a widget file.

@@ -122,14 +144,20 @@ private slots:

bool m_create;
bool m_history;
#ifdef WITH_XC_SSHAGENT
bool m_sshAgent;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest renaming this to m_sshAgentEnabled as it is named now it implies holding an object not a boolean.

#include <QIODevice>
#include <QBuffer>

class BinaryStream : QObject
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend removing this implementation entirely in favor of QDataStream (you can set Endianness!) http://doc.qt.io/qt-5/qdatastream.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenSSH has its own buffer format, which is not compatible with Qt's. Maybe renaming BinaryStream to something like SSHBufferStream is better.

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2017 Toni Spets <toni.spets@iki.fi>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All new files need to include the standard KeePassXC team copyright (in can be in addition to your own):

Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>


void AgentSettingsWidget::saveSettings()
{
config()->set("SSHAgent", m_ui->enableSSHAgentCheckBox->isChecked());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point you should emit a signal that the SSH Agent settings have changed which would alert the system to either start or stop the ssh agent. We should never have to restart the application to enact a setting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will require some refactoring as yeah, it needs to send the keys out to an agent if databases are unlocked when you enable it and emulate a lock if you disable while keys have been sent that are supposed to be removed on lock.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can leave that to a future enhancement after this merge. I'll create an issue to track.

<item>
<widget class="QCheckBox" name="lifetimeCheckBox">
<property name="text">
<string>Remove key from agent after</string>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not entirely clear when this timeout kicks in. Is it on database open? Is it on last use? Is it a timeout between uses? I think it needs to be reworded to be clear.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree on the ambiguity but if someone has a good rewording that works, sure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the answer though... I didn't parse the code to find out

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regardless how the key is added to the agent, the agent will auto-remove the key after said timeout. It's an agent feature and we just tell it how long it should keep the key.

}

if (beginEx.cap(1) != "OPENSSH PRIVATE KEY") {
m_error = tr("This is not an OpenSSH key, only modern keys are supported");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is obtuse. What is meant by "modern keys"?? None of my keys loaded up and I have absolutely no idea how to fix the problem. Why isn't RSA PRIVATE KEY accepted?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what to call the new type keys. Suggestions welcome.

Copy link
Member

@droidmonkey droidmonkey Nov 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After researching it they are elliptic curve generated keys (ie, ed25519). Why don't you allow loading RSA keys? RSA 4096-bits is perfectly secure...

http://blog.siphos.be/2015/08/switching-openssh-to-ed25519-keys/

Do note that not all SSH servers support the new ed25519. Dropbear is a noted hold out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It means the new "OpenSSH file format", it supports all key types and conversions are possible as stated in OP with instructions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, I guess I should read your description first. Either way the link to the documentation (could even be within the GUI layout, not the error string) needs to be settled for the lay user. We will get SO MANY bug reports as it is implemented now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be a link to the wiki? Is there any other reasonable way to document things like this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly, we miss a lot of documentation, it's an open issue #696

@CRCinAU
Copy link

CRCinAU commented Nov 13, 2017

May I also suggest at some point we have some documentation on how to configure this to work?

@hifi
Copy link
Member Author

hifi commented Nov 13, 2017

@CRCinAU It most likely is needed because it's not obvious what the new key format means and what would be the troubleshooting steps to get your agent visible on Linux/BSD.

The key format error really needs to point to some documentation how you convert your key the very least. As @droidmonkey said for the live enable/disable configuration option it could also be tracked as a separate issue to improve the UX before 2.3.0 lands.

QString begin = rows.first();
QString end = rows.last();

QRegExp beginEx("-----BEGIN (.+)-----");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More thorough review pending, but I just saw this while reading my notifications. This should be

"^-----BEGIN ([^\\-]+)-----$"

Also use QRegularExpression instead of QRegExp. Or maybe even split after -----BEGIN and before ----- using QString::indexOf().

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's using exactMatch which implies ^...$.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still leaves the thing in the middle.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it like that first but opted to use the shorter version as it doesn't matter much with a single line exact match. Sure, it bleeds extra dashes into the capture. I'll change it back.

Copy link
Member

@phoerious phoerious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a few things that need to be changed. After that I think this is ready for merging. Most of the changes are minor nitpicks, but I also found a few potential gotchas.

I would also prefer some doc comments for the new methods, so we can generate an API documentation in the future, but that can be done in a separate PR.

continue;
}

m_sshAgentUi->privateKeyComboBox->addItem("Attachment: " + fileName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be a translatable string.

}

if (settings.selectedType() == "attachment") {
m_sshAgentUi->privateKeyComboBox->setCurrentText("Attachment: " + settings.attachmentName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dito

m_instance = new SSHAgent(parent);
}

bool SSHAgent::agentRunning() const
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be named isAgentRunning()

settings.setUseLifetimeConstraintWhenAdding(m_sshAgentUi->lifetimeCheckBox->isChecked());
settings.setLifetimeConstraintDuration(m_sshAgentUi->lifetimeSpinBox->value());

QString prefix = "Attachment: ";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Translation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the prefix format for KeeAgent so shoudn't be translatable

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's my own convention unrelated to KeeAgent so no worries there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's an hardcoded prefix you should consider something like _ATTACHMENT: , like this it really seems a translatable string

settings.setLifetimeConstraintDuration(m_sshAgentUi->lifetimeSpinBox->value());

QString prefix = "Attachment: ";
if (privateKeyPath.startsWith(prefix)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startsWith() may not work any more, once the string has been translated.


// load private if no encryption
if (!encrypted()) {
return decrypt();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I missing something? Why do you decrypt if it is not encrypted?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decryption also implies parsing the private portion regardless if there's actual encryption used. Function naming issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Should be renamed then.


QByteArray phraseData = passphrase.toLatin1();
if (bcrypt_pbkdf(phraseData, salt, decryptKey, rounds) < 0) {
m_error = tr("Decryption failed, wrong passphrase?");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error message is confusing. You are deriving the decryption key here, not decrypting the data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It happens because of an empty passphrase. Will catch that special case earlier and have a proper error message for both.

keyData.setRawData(decryptKey.data(), cipher->keySize());
ivData.setRawData(decryptKey.data() + cipher->keySize(), cipher->blockSize());

cipher->init(keyData, ivData);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it peculiar, that the OpenSSH format has no HMAC for the cipher text. PuTTY format has that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want me to file an issue with OpenSSH developers? 😕

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that was just a comment on the sidelines.

return false;
}

for (int i = 0; i < keyParts; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++i

return false;
}

for (int i = 0; i < keyParts; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++i

Includes AES-256-CTR non-stream tests
This additionally makes keySize() and blockSize() work before
setting the key and IV. Required for SSH agent decryption.
@phoerious phoerious merged commit ac73e25 into keepassxreboot:develop Nov 19, 2017
@phoerious
Copy link
Member

Congratulations, you got merged with the Matrix!

@hifi
Copy link
Member Author

hifi commented Nov 19, 2017

🎉

@CRCinAU
Copy link

CRCinAU commented Nov 19, 2017

So urrrm... When are we looking at doing a new release? I'm super keen to look at this plus the yubikey fixes of a while ago...

@Mogost
Copy link

Mogost commented Nov 19, 2017

Great news! Waiting for release.

@hifi
Copy link
Member Author

hifi commented Nov 19, 2017

@CRCinAU Official daily builds are being discussed as the 2.3.0 cycle will bring in many new features that will require heavy testing before release.

@CRCinAU
Copy link

CRCinAU commented Nov 19, 2017

@hifi Does that include the new browser plugin I keep hearing about?

@droidmonkey
Copy link
Member

Yes absolutely

@JelteF
Copy link

JelteF commented Jan 2, 2018

Oh man, thanks a lot for this! Keeagent was the only reason keeping me on Keepass (even though that crashed for me when pressing the windows key on linux). I've tried this out and it is works great so far (had to convert my key though)!

phoerious added a commit that referenced this pull request Feb 27, 2018
- Add support for KDBX 4.0, Argon2 and ChaCha20 [#148, #1179, #1230, #1494]
- Add SSH Agent feature [#1098, #1450, #1463]
- Add preview panel with details of the selected entry [#879, #1338]
- Add more and configurable columns to entry table and allow copying of values by double click [#1305]
- Add KeePassXC-Browser API as a replacement for KeePassHTTP [#608]
- Deprecate KeePassHTTP [#1392]
- Add support for Steam one-time passwords [#1206]
- Add support for multiple Auto-Type sequences for a single entry [#1390]
- Adjust YubiKey HMAC-SHA1 challenge-response key generation for KDBX 4.0 [#1060]
- Replace qHttp with cURL for website icon downloads [#1460]
- Remove lock file [#1231]
- Add option to create backup file before saving [#1385]
- Ask to save a generated password before closing the entry password generator [#1499]
- Resolve placeholders recursively [#1078]
- Add Auto-Type button to the toolbar [#1056]
- Improve window focus handling for Auto-Type dialogs [#1204, #1490]
- Auto-Type dialog and password generator can now be exited with ESC [#1252, #1412]
- Add optional dark tray icon [#1154]
- Add new "Unsafe saving" option to work around saving problems with file sync services [#1385]
- Add IBus support to AppImage and additional image formats to Windows builds [#1534, #1537]
- Add diceware password generator to CLI [#1406]
- Add --key-file option to CLI [#816, #824]
- Add DBus interface for opening and closing KeePassXC databases [#283]
- Add KDBX compression options to database settings [#1419]
- Discourage use of old fixed-length key files in favor of arbitrary files [#1326, #1327]
- Correct reference resolution in entry fields [#1486]
- Fix window state and recent databases not being remembered on exit [#1453]
- Correct history item generation when configuring TOTP for an entry [#1446]
- Correct multiple TOTP bugs [#1414]
- Automatic saving after every change is now a default [#279]
- Allow creation of new entries during search [#1398]
- Correct menu issues on macOS [#1335]
- Allow compilation on OpenBSD [#1328]
- Improve entry attachments view [#1139, #1298]
- Fix auto lock for Gnome and Xfce [#910, #1249]
- Don't remember key files in file dialogs when the setting is disabled [#1188]
- Improve database merging and conflict resolution [#807, #1165]
- Fix macOS pasteboard issues [#1202]
- Improve startup times on some platforms [#1205]
- Hide the notes field by default [#1124]
- Toggle main window by clicking tray icon with the middle mouse button [#992]
- Fix custom icons not copied over when databases are merged [#1008]
- Allow use of DEL key to delete entries [#914]
- Correct intermittent crash due to stale history items [#1527]
- Sanitize newline characters in title, username and URL fields [#1502]
- Reopen previously opened databases in correct order [#774]
- Use system's zxcvbn library if available [#701]
- Implement various i18n improvements [#690, #875, #1436]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.