Skip to content

Store passwords in user.db configs #251

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

Merged
merged 24 commits into from
Nov 10, 2016

Conversation

lukebarnard1
Copy link
Contributor

Send !storepass server.name password to have your virtual IRC client's password stored encrypted in the db.

The passwords are encrypted and decrypted using a PEM key stored in a file specified in the config file (See passwordEncryptionKeyPath).

The passwords are then used in place of the server password set in the config file, whether it is set or not.

This has been tested to work whilst bridging with freenode, and as expected, when !nick and !storepass are issued prior to the bridge restarting (or the IRC client reconnecting), NickServ asks for identification (if the nick is registered) but then immediately resolves (having received PASS with the recently decrypted password).

Luke Barnard added 2 commits October 24, 2016 15:21
The passwords are encrypted and decrypted using a PEM key stored in a file specified in the config file (See passwordEncryptionKeyPath).

The passwords are then used in place of the server password set in the config file.

This has been tested to work with freenode, and as expected, when !nick and !storepass are issued prior to the bridge restarting (or the IRC client reconnecting), NickServ asks foridentification (if the nick is registered) but then imediately resolves (having received PASS with the recently decrypted password).
# made for the corresponding virtual IRC user of the matrix user that has issued
# !storepass, they will connect with the password that has been persisted, if
# there is one.
passwordEncryptionKeyPath: "passkey.pem"
Copy link
Member

Choose a reason for hiding this comment

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

NL


if (pkeyPath) {
try {
this._privateKey = fs.readFileSync(pkeyPath, "utf8").toString();
Copy link
Member

Choose a reason for hiding this comment

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

Can you please default-initialise this._privateKey to null.

# WARNING - STORING PASSWORDS ENCRYPTED IS PRECAUTIONARY TO MAKE COMPROMISE LESS
# LIKELY. THIS MEASURE IS ONLY AS SECURE AS THE KEY FILE, BELOW.
#
# The path to the RSA PEM-formatted key to use when encrypting and decrypting
Copy link
Member

Choose a reason for hiding this comment

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

  • You don't mention if this should be the private or public key.
  • You don't mention if this is allowed to have a password on it.

@@ -328,3 +328,14 @@ ircService:
# allotted time period, the provisioning request will fail.
# Default: 300 seconds (5 mins)
requestTimeoutSeconds: 300

# WARNING - STORING PASSWORDS ENCRYPTED IS PRECAUTIONARY TO MAKE COMPROMISE LESS
# LIKELY. THIS MEASURE IS ONLY AS SECURE AS THE KEY FILE, BELOW.
Copy link
Member

Choose a reason for hiding this comment

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

I would tweak the wording on this. You want to tell them:

  • This is only "data at rest" encryption, the passwords are completely retrievable with the key (rather than being stored as a hash) because the bridge needs to send the password in the plain.
  • Said encryption is only as secure as the key file, so there should be restrictions on who can read the PEM file on the machine.

if (!this._privateKey && !forcePlaintextStorage) {
throw new Error(
'WARNING: Store plaintext passwords at your own ' +
'risk; use -f to force'
Copy link
Member

Choose a reason for hiding this comment

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

-f?

`is provided to the bridge. See config 'passwordEncryptionKeyPath'.`
);
}
ircClientConfig.setPrivateKey(this.getStore()._privateKey);
Copy link
Member

Choose a reason for hiding this comment

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

Please don't gut-wrench private member variables.

this._dataStore = new DataStore(
this._bridge.getUserStore(),
this._bridge.getRoomStore(),
pkeyPath
Copy link
Member

Choose a reason for hiding this comment

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

In-line please.


try {
let force = args[1] === '-f';
yield this.ircBridge.getStore().storePass(userId, domain, args[0], force);
Copy link
Member

Choose a reason for hiding this comment

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

Right now I see why you mentioned -f before. Why is -f A Thing? This should not be an option for random Matrix users.

}
if (typeof privateKey !== 'string') {
let actual = typeof privateKey;
throw new Error("Private key must be an RSA PEM-formatted string, not " + actual);
Copy link
Member

Choose a reason for hiding this comment

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

This all feels like needless boilerplate. We load the PEM key from file in DataStore, why aren't we just checking the types there? Why do we pass the private key through as a member variable to IrcClientConfig? Why is it even an option to store passwords unencrypted? It means you get these scenarios where you have no idea what _config.password is (unencrypted or encrypted) and you just make more work for yourself.

@kegsay
Copy link
Member

kegsay commented Oct 26, 2016

Broadly speaking this is looking good, but the form of the API is questionable.

  • I don't like how we pass around the private key between DataStore and IrcClientConfigs, and do some checks in each: just choose one and stick with it (probably DataStore).
  • I don't like how users can choose to not encrypt their passwords: it's unthinkable that we should persist any password in the clear on the database.
  • I don't like how the config has to "redact" the password field - it shouldn't be stored in _config in the clear. getPassword should decrypt the encrypted form and return that and that's it.

@lukebarnard1
Copy link
Contributor Author

lukebarnard1 commented Oct 26, 2016

On your three points:

  • If DataStore is the only thing to ever store the private key, we would need some way to get the privateKey from there into calls to IrcClientConfig.getPassword(privateKey) because that is how the IrcBridge class gets the passwords. To be honest, I just couldn't see a clearer way of doing it architecturally, any input would be appreciated. To clarify, the problem with storing privateKey in DataStore is that the IrcBridge would have to inspect the DataStore every time it calls getPassword.
  • Fair enough, I'll remove the whole '-f' thing and replace 'use -f' notice with 'no private key loaded'.
  • I think it's perfectly fine to store the encrypted password in _config and I don't understand why we wouldn't redact the encrypted form. (Also, I'm assuming 'in the clear' means plaintext as opposed to encrypted?)

@kegsay
Copy link
Member

kegsay commented Oct 26, 2016

Well it doesn't help you appear to be trying to reuse getPassword when it is currently being used for the bot password: https://github.com/matrix-org/matrix-appservice-irc/blob/master/lib/irc/IrcServer.js#L94 - but of course this will break when you add encryption and expect that field to be encrypted.

The private key is loaded from file when the DataStore starts up. When you hit storePass you give the plaintext and behind the scenes it does the necessary magic to plop it into the DB encrypted. This feels right: nothing else should really give a damn about this.

Can we do the same for getPassword? That would imply we have the client config store the unencrypted form inside itself, since it doesn't know about the encryption at all. When the bridge tries to load the IrcClientConfig for a user, the DataStore magically decrypts entries. This would also mean my first point about it breaking for the bot user is invalid: since it'll still be the unencrypted form. WDYT?

@lukebarnard1
Copy link
Contributor Author

That sounds reasonable, WFM.

Luke Barnard added 4 commits October 27, 2016 13:59
Passwords are kept in config objects unencrypted; passwords are encrypted when the config itself
is stored in the database and dencrypted when the config is retrieved from the database.
log.info(`Private key loaded from ${pkeyPath} - IRC password encryption enabled.`);
}
catch (err) {
log.error('Could not load private key ${err.message}.');
Copy link
Member

Choose a reason for hiding this comment

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

Feels like this should be fatal.

Copy link
Member

Choose a reason for hiding this comment

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

Then you can remove :473 guard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But what if someone wants to use the bridge without bothering with generating a PEM key?

Copy link
Member

Choose a reason for hiding this comment

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

I don't follow. This block only executes if (pkeyPath). So if you want to use the bridge without bothering to generate a PEM key, then don't fill in that config field?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But then how can I remove the guard on line 473? If the private key is null, surely we just want to throw an error?

# `!storepass server.name passw0rd. When a connection is made for the corresponding
# virtual IRC user of the matrix user that has issued !storepass, they will connect
# with the (decrypted) password that has been persisted, if there is one.
passwordEncryptionKeyPath: "passkey.pem"
Copy link
Member

Choose a reason for hiding this comment

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

NL

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Done)

let notice;

try {
yield this.ircBridge.getStore().storePass(userId, domain, args[0]);
Copy link
Member

Choose a reason for hiding this comment

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

Are spaces allowed in NickServ passwords? If so, then args[0] ain't gonna cut it.

Copy link
Member

Choose a reason for hiding this comment

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

Or rather, it will cut it 😆


// The PEM string used to encrypt passwords when setPassword is
// called and decrypt when getPassword is called.
this._privateKey = null;
Copy link
Member

Choose a reason for hiding this comment

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

Never used.

@@ -42,7 +47,7 @@ class IrcClientConfig {
}

getPassword() {
return this._config.password;
return this._config.password;
Copy link
Member

Choose a reason for hiding this comment

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

3 space indent?

if (pkeyPath) {
try {
this._privateKey = fs.readFileSync(pkeyPath, "utf8").toString();
log.info(`Private key loaded from ${pkeyPath} - IRC password encryption enabled.`);
Copy link
Member

Choose a reason for hiding this comment

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

We should probably assert that the key is usable first so we can fail fast if _privateKey isn't a PEM file. Try encrypting/decrypting a test string in this constructor.

@@ -328,3 +328,13 @@ ircService:
# allotted time period, the provisioning request will fail.
# Default: 300 seconds (5 mins)
requestTimeoutSeconds: 300

# WARNING - STORED PASSWORDS ENCRYPTED ARE COMPLETELY RETRIEVABLE WITH THE KEY.
# THE BRIDGE NEEDS TO SEND PASSWORDS TO THE IRC SERVERS AS PLAINTEXTS.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe rephrase to:

WARNING: The bridge needs to send plaintext passwords to the IRC server, it cannot send a password hash. As a result, passwords (NOT hashes) are stored encrypted in the database.

Copy link
Contributor Author

@lukebarnard1 lukebarnard1 Nov 4, 2016

Choose a reason for hiding this comment

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

I'm not sure it's worth mentioning hashes at all. The PEM key requirement is clearly for not making hashes. How about:

WARNING: The bridge needs to send plaintext passwords to the IRC server, and so passwords are stored encrypted in the database.

?

# for storage in the database. Passwords are stored by using the admin room command
# `!storepass server.name passw0rd. When a connection is made for the corresponding
# virtual IRC user of the matrix user that has issued !storepass, they will connect
# with the (decrypted) password that has been persisted, if there is one.
Copy link
Member

Choose a reason for hiding this comment

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

When a connection is made for the corresponding virtual IRC user of the matrix user that has issued !storepass, they will connect with the (decrypted) password that has been persisted, if there is one.

Maybe rephrase to:

When a connection is made to IRC on behalf of the Matrix user, this password will be sent as the server password (PASS command).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

WFM

this._privateKey,
new Buffer(clientConfig.getPassword(), 'base64')
).toString();
decryptedPass = decryptedPass.split(' ')[1];
Copy link
Member

Choose a reason for hiding this comment

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

Uhh what? Explanation please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's to remove the salt and extract the real password

Copy link
Member

Choose a reason for hiding this comment

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

Can you put that as a comment then please :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure!

let pass = args.join('');
yield this.ircBridge.getStore().storePass(userId, domain, pass);
notice = new MatrixAction(
"notice", `Successfully stored password`
Copy link
Member

Choose a reason for hiding this comment

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

We should mention that this only applies on reconnects and that they still need to poke NickServ initially. Can we name the domain as well please (since the server is often implicit, they may not have typed it in).

Successfully stored password on ${domain}. When you next reconnect to ${domain}, this password will be automatically sent in a PASS command which most IRC networks will use as your NickServ password. This means you will not need to talk to NickServ. This does NOT apply to your currently active connection: you still need to talk to NickServ one last time to authenticate your current connection if you haven't already.

Copy link
Member

Choose a reason for hiding this comment

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

We should probably also allow them to remove their stored password.

@kegsay
Copy link
Member

kegsay commented Nov 4, 2016

Other than wordings on things, this is looking good.

I did have a thought though: we should probably allow people to remove their stored password somehow.

@lukebarnard1
Copy link
Contributor Author

lukebarnard1 commented Nov 4, 2016

  • Add !removepass command to unset the password
  • Handle empty password as an error case

@kegsay
Copy link
Member

kegsay commented Nov 4, 2016

Should an empty pass be treated as an error or should we display the usage (similar to !nick)?

@lukebarnard1
Copy link
Contributor Author

I went for the latter as it does indicate that the user doesn't really understand the command.

notice = new MatrixAction(
"notice",
"Format: '!storepass password' " +
"or '!storepass irc.server.name password'\n"
Copy link
Member

Choose a reason for hiding this comment

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

Explanation of what this does would be good. I would move the blurb you have on success ("When you next reconnect...") to a var and then use that here in addition to on success.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

WFM

yield this.ircBridge.sendMatrixAction(adminRoom, botUser, notice, req);
return;
}
else if (cmd === "!removepass") {
Copy link
Member

Choose a reason for hiding this comment

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

Both this and !storepass need to be added to !help.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@kegsay
Copy link
Member

kegsay commented Nov 9, 2016

After you add more words to the commands, LGTM!

@3v1n0
Copy link

3v1n0 commented Dec 21, 2018

I'm wondering... Couldn't be the matrix protocol to be updated so that the key is encrypted using some runtime data that is valid for this user only?

I'm not into the deep of matrix, but having the service to require a matrix user pass whenever the nick pass should be used could be a way. Indeed this wouldn't make auto-connection work in case until the user has approved this, but it would make possible to trust the system whoever is hosting it.

@kegsay
Copy link
Member

kegsay commented Dec 22, 2018

Sadly I don't think what you want is technically feasible. It always bugged me that we were/are persisting passwords on the bridge since it slaps a big target on our back, but you must understand the requirements here.

The bridge has to send the user/password to the target IRC server. Even if you only ever gave the bridge an encrypted password which you had the key for, then did some dance to decrypt it on one of your clients, it still wouldn't improve trust. There would be nothing to stop the bridge from retrieving the decrypted password from your client then doing bad things with it, in addition to logging you in to IRC. Simply put, the bridge has to have the password in the plain somewhere so it can do the necessary operation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants