Secure method for storing/retrieving a PGP private

2020-02-10 17:22发布

问题:

I have a web application that needs to store server login information. I'm using a 2048bit PGP public key to encrypt inserted passwords (see the insertServerDef) and a private key with a passphrase to decrypt the passwords (see getServerDef).

As I understand things, the weakest link in this chain is the handling of the private key and passphrase. As you can see from my code below, I'm just using file_get_contents to retrieve the key and passphrase from files located in the current web directory--not good.

My question is: what is a good method for securely retrieving the private key and passphrase for use in decrypting login info? Maybe I should store/retrieve the private key via an authenticated remote file server?

I've searched for best practices, but haven't been able to find much.

class DB {

    protected $_config;
    protected $_iUserId;
    protected $_iServerId;
    protected $_dbConn;
    protected $_sPubKey;
    protected $_sPrivKey;


    public function __construct($iUserId, $iServerId) {

        //bring the global config array into local scope
        global $config;
        $this->_config = $config;

        $this->_iUserId = $iUserId;
        $this->_iServerId = $iServerId;

        $this->_sPubKey = file_get_contents("public_key");
        $this->_sPrivKey = file_get_contents("private_key");
        $this->_sPrivKeyPass = trim(file_get_contents("private_key_pass"));

    }

    //connect to the database
    public function connect() {
        try {


            $this->_dbConn = new PDO("pgsql:host=".$this->_config['db_host']." dbname=".$this->_config['db_name'],$this->_config['db_username'],$this->_config['db_password']);

            echo "PDO connection object created";
        } catch(PDOException $e) {

            echo $e->getMessage();

        }

    }

    public function insertServerDef($sHost, $iPort, $sUser, $sPass) {

        //testing
        $iUserId = 1;

        $oStmt = $this->_dbConn->prepare("INSERT INTO upze_server_def (server_id, host_address, ssh_port, username, pass, user_id) VALUES (DEFAULT, :host_address, :ssh_port, :username, pgp_pub_encrypt(:pass,dearmor(:pub_key)), :user_id)");
        $oStmt->bindParam(':host_address',$sHost);
        $oStmt->bindParam(':ssh_port',$iPort);
        $oStmt->bindParam(':username',$sUser);
        $oStmt->bindParam(':pass',$sPass);
        $oStmt->bindParam(':pub_key',$this->_sPubKey);

        $oStmt->bindParam(':user_id',$iUserId);
        $oStmt->execute();

    }

    public function getServerDef($iServerId) {

        $oStmt = $this->_dbConn->prepare("  SELECT server_id, pgp_pub_decrypt(pass,dearmor(:priv_key),:priv_key_pass) As decryptpass 
                                            FROM upze_server_def usd 
                                            WHERE usd.server_id = :server_id
                                        ");

        $oStmt->bindParam(':server_id', $iServerId);
        $oStmt->bindParam(':priv_key', $this->_sPrivKey);
        $oStmt->bindParam(':priv_key_pass', $this->_sPrivKeyPass);
        $oStmt->execute();

        while($row = $oStmt->fetch()) {
            echo "<pre>".print_r($row)."</pre>";
        }

    }

    //close any existing db connection
    public function close() {
        $this->_dbConn = null;
    }


    //close any existing db connections on unload
    public function __destruct() {
        $this->_dbConn = null;
    }

}

回答1:

(Note: I'm no security expert. I have an interest in the area, but that's it. Keep that in mind.)

If possible, don't store passwords at all

It depends a lot on what your needs are. The best option of all is not to use two-way encryption at all; if you can store only salted and one-way-hashed password digests that's ideal. You can still test them to see if they match a supplied password from the user, but you never store it.

Better still, if your clients use some sane protocol (ie: not HTTP as commonly implemented) you can use a challenge-response authentication mechanism that means your app never ever needs to see the user's password, not even when authenticating them. Sadly this is rarely possible on the public web, which has security that'd put 80's programmers to shame.

If you must store the password, isolate the keys from the app

If you must be able to decrypt the passwords, ideally you shouldn't have all the details to do so in one place, and certainly not one copyable, easily accessible place.

For that reason I'd personally prefer not to use PgCrypto (as you're doing) for this purpose because it forces you to reveal the private key and (if it has one) passphrase to the server, where it could be exposed in PostgreSQL's log files or otherwise potentially sniffed. I'd want to do my crypto client-side, where I could use PKCS#11, a key agent, or other tools that let me decrypt the data without ever having my code able to access the key.

The problem of secure key storage is part of what PKCS#11 was invented for. It provides a generic interface for applications and crypto providers to talk to anything that can provide certain signing and decryption services without ever revealing its key. The usual, but not only, use is with hardware based crypto like smart cards and hardware crypto modules. Such devices can be told to sign or decrypt data passed to them, and can do so without ever revealing the key. If possible, consider using a smartcard or HSM. As far as I know PgCrypto cannot use PKCS#11 or other HSMs/smartcards.

If you can't do that, you can still probably use a key management agent, where you load your key into a key management program manually when the server boots, and the key management program provides a PKCS#11 (or some other) interface for signing and decryption via a socket. That way your web app never needs to know the key at all. gpg-agent may qualify for this purpose. Again, as far as I know PgCrypto cannot use a key management agent, though it'd be a great feature to add.

Even a small improvement can help. It's best if the passphrase for your key isn't stored on disk, so you might require it to be entered when the app is started up so the key can be decrypted. You're still storing the decrypted key in memory, but all the details to decrypt it are no longer on disk and easy to get at. It's much harder for an attacker to steal the decrypted key from memory than to grab a "password.txt" from disk.

What you choose to do depends a lot on the details of your security needs and the data you're working with. In your position I'd just not store the passwords if at all possible, and if I had to I'd want to use a PKCS#11-compatible hardware device.



回答2:

I may be doing something similar to you. I currently wish to protect personal information on a local web server database, so I am encrypting it with a public key (stored on the web server itself) and decrypting it with a private key stored in a cookie with a short lifetime (30 minutes for me).

Over an SSL connection, this will keep the key from falling into the wrong hands, and it doesn't store it on the server. Ideally I should double-check that PHP doesn't cache cookie values on the server, but even if it does, this security layer still represents a bigger hurdle for attackers than simply stealing the plaintext database.

Whether this would be a good approach for you depends on whether your application needs to access server credentials even when users are not logged in via the web. In my case, decryption is only required via the web app, so the cookie suffices. However if you need unattended use, you will need to store the private key on the server.



回答3:

I believe there cannot be an answer unless you describe the threat scenario you want to avoid.

Let me rephrase your situation: You need to have a plain text password for accessing remote systems via SSH. The goal is to protect this password, but have it available when needed.

There really is no way to both protect the password and be able to use in its plain text form. There always has to be a way to decrypt it, and this needs both the mechanism and the secret key.

You can try to iterate a bit on this, like protecting this secret again, but in the end you will need to store the final plain text password into a variable and pass it into the authentication scheme.

What is the threat scenario you want to avoid?

  • someone steals a dump of your database and should not know the stored passwords
  • someone breaks into your server and reads both the database and the file with the secret passphrase
  • someone breaks into your server, alters your script and intercepts at the point where the decrypted plain text password is used

You see, there is always a way to do harm. Most harm can be done if you do not check regularly against irregular activity on your systems. Like monitor all log files for attacks. Probably send the logs to another syslog server so that an attacker cannot change them if they are on the same server. If you do everything you can to prevent attackers from getting onto your system, then the need to securely store the secret passphrase diminishes.

It might be an idea to store the passphrase into RAM, like on a RAM disk or inside a dedicated memory. This way, if the server gets stolen, it most likely will be unpowered and forget the passphrase. But you then must have a way to restore the passphrase from remote to continue operation after a reboot. But again: if you cannot detect that an attacker is on your system, it's meaningless whether the passphrase is inside RAM or on magnetic disk - it can be read.

I wonder why you deal with passwords in the first place. If I use SSH, I always try to use SSH keys for cryptographic authentication. This is more secure on the target server, because one account (like root) can have multiple SSH keys, and if the one stored on your server is compromised, it can be deleted without interfering with the other keys. Yes, these SSH keys can be secured with a password, too.