-
Notifications
You must be signed in to change notification settings - Fork 153
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
Conversation
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" |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
Broadly speaking this is looking good, but the form of the API is questionable.
|
On your three points:
|
Well it doesn't help you appear to be trying to reuse The private key is loaded from file when the Can we do the same for |
That sounds reasonable, WFM. |
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}.'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NL
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.`); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uhh what? Explanation please.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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. |
|
Should an empty pass be treated as an error or should we display the usage (similar to |
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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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") { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
After you add more words to the commands, LGTM! |
Conflicts: lib/bridge/MatrixHandler.js
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. |
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. |
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).