Skip to content

Commit

Permalink
added secure mechanism for oauth key pair and encryption key (openemr…
Browse files Browse the repository at this point in the history
…#4022)

1. Encryption key is created when needed and stored in encrypted form in database.
2. Key pair is created when needed with keys stored in filesystem and key passphrase
   stored in encrypted form in database.
3. No access to oath unless api is turned on.
  • Loading branch information
bradymiller committed Nov 10, 2020
1 parent 96cd252 commit 59ec53d
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 41 deletions.
4 changes: 2 additions & 2 deletions _rest_config.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ public static function Init(): void
self::$ROOT_URL = self::$web_root . "/apis";
self::$VENDOR_DIR = self::$webserver_root . "/vendor";
self::$REST_FULLAUTH_URL = self::$REST_BASE_URL . '/oauth2/' . self::$SITE;
self::$privateKey = self::$webserver_root . "/sites/" . self::$SITE . "/documents/certificates/private.key";
self::$publicKey = self::$webserver_root . "/sites/" . self::$SITE . "/documents/certificates/public.key";
self::$privateKey = self::$webserver_root . "/sites/" . self::$SITE . "/documents/certificates/oaprivate.key";
self::$publicKey = self::$webserver_root . "/sites/" . self::$SITE . "/documents/certificates/oapublic.key";
self::$IS_INITIALIZED = true;
}

Expand Down
6 changes: 6 additions & 0 deletions oauth2/authorize.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@

use OpenEMR\RestControllers\AuthorizationController;

// exit if api is not turned on
if (empty($GLOBALS['rest_api']) && empty($GLOBALS['rest_fhir_api']) && empty($GLOBALS['rest_portal_api']) && empty($GLOBALS['rest_portal_fhir_api'])) {
http_response_code(404);
exit;
}

$end_point = $gbl::getRequestEndPoint();

$authServer = new AuthorizationController();
Expand Down
3 changes: 3 additions & 0 deletions sites/default/documents/certificates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ For ldap tls support:
- `Common Name` of CA certificate: This can be anything, but needs to be different than what is used for Server and Client
- `Common Name` of Server certificate: This has to be the host name(or ip address) that the client uses to log into the mysql server.
- `Common Name` of Client certificate: Set this to the host name of the client.

For oauth key pair support:
1. This is done automatically by OpenEMR. When oauth is used, the a oaprivate.key and oaprublic.key will be created in this directory.
27 changes: 0 additions & 27 deletions sites/default/documents/certificates/private.key

This file was deleted.

9 changes: 0 additions & 9 deletions sites/default/documents/certificates/public.key

This file was deleted.

106 changes: 103 additions & 3 deletions src/RestControllers/AuthorizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* @package OpenEMR
* @link http:https://www.open-emr.org
* @author Jerry Padgett <[email protected]>
* @author Brady Miller <[email protected]>
* @copyright Copyright (c) 2020 Jerry Padgett <[email protected]>
* @copyright Copyright (c) 2020 Brady Miller <[email protected]>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/

Expand All @@ -15,6 +17,7 @@
use DateInterval;
use Exception;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\Grant\PasswordGrant;
Expand All @@ -33,6 +36,8 @@
use OpenEMR\Common\Auth\OpenIDConnect\Repositories\RefreshTokenRepository;
use OpenEMR\Common\Auth\OpenIDConnect\Repositories\ScopeRepository;
use OpenEMR\Common\Auth\OpenIDConnect\Repositories\UserRepository;
use OpenEMR\Common\Crypto\CryptoGen;
use OpenEMR\Common\Utils\RandomGenUtils;
use OpenEMR\Common\Uuid\UuidRegistry;
use OpenIDConnectServer\ClaimExtractor;
use OpenIDConnectServer\Entities\ClaimSetEntity;
Expand All @@ -47,6 +52,8 @@ class AuthorizationController
public $serverBaseUrl;
public $authBaseUrl;
private $privateKey;
private $passphrase;
private $publicKey;
private $encryptionKey;
private $grantType;
private $providerForm;
Expand All @@ -59,8 +66,101 @@ public function __construct($providerForm = true)
$this->serverBaseUrl = $gbl::$REST_BASE_URL;
$this->authBaseUrl = $gbl::$REST_FULLAUTH_URL;
$this->authRequestSerial = $_SESSION['authRequestSerial'] ?? '';
$this->encryptionKey = 'lxZFUEsBCJ2Yb14IF2ygAHI5N4+ZAUXXaSeeJm6+twsUmIen';
$this->privateKey = $gbl::$privateKey;

// Create a crypto object that will be used for for encryption/decryption
$this->cryptoGen = new CryptoGen();

// encryption key
$eKey = sqlQueryNoLog("SELECT `name`, `value` FROM `keys` WHERE `name` = 'oauth2key'");
if (!empty($eKey['name']) && ($eKey['name'] == 'oauth2key')) {
// collect the encryption key from database
$this->encryptionKey = $this->cryptoGen->decryptStandard($eKey['value']);
if (empty($this->encryptionKey)) {
// if decrypted key is empty, then critical error and must exit
error_log("OpenEMR error - oauth2 key was blank after it was decrypted, so forced exit");
http_response_code(500);
exit;
}
} else {
// create a encryption key and store it in database
$this->encryptionKey = RandomGenUtils::produceRandomBytes(32);
if (empty($this->encryptionKey)) {
// if empty, then force exit
error_log("OpenEMR error - random generator broken during oauth2 encryption key generation, so forced exit");
http_response_code(500);
exit;
}
$this->encryptionKey = base64_encode($this->encryptionKey);
if (empty($this->encryptionKey)) {
// if empty, then force exit
error_log("OpenEMR error - base64 encoding broken during oauth2 encryption key generation, so forced exit");
http_response_code(500);
exit;
}
sqlStatementNoLog("INSERT INTO `keys` (`name`, `value`) VALUES ('oauth2key', ?)", [$this->cryptoGen->encryptStandard($this->encryptionKey)]);
}

// private key
$this->privateKey = $GLOBALS['OE_SITE_DIR'] . '/documents/certificates/oaprivate.key';
$this->publicKey = $GLOBALS['OE_SITE_DIR'] . '/documents/certificates/oapublic.key';
if (!file_exists($this->privateKey)) {
// create the private/public key pair (store in filesystem) with a random passphrase (store in database)
// first, create the passphrase (removing any prior passphrases)
sqlStatementNoLog("DELETE FROM `keys` WHERE `name` = 'oauth2passphrase'");
$this->passphrase = RandomGenUtils::produceRandomString(60, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
if (empty($this->passphrase)) {
// if empty, then force exit
error_log("OpenEMR error - random generator broken during oauth2 key passphrase generation, so forced exit");
http_response_code(500);
exit;
}
// second, create and store the private/public key pain
$keys = openssl_pkey_new(["private_key_bits" => 2048, "private_key_type" => OPENSSL_KEYTYPE_RSA]);
if ($keys === false) {
// if unable to create keys, then force exit
error_log("OpenEMR error - key generation broken during oauth2, so forced exit");
http_response_code(500);
exit;
}
openssl_pkey_export($keys, $privkey, $this->passphrase);
$pubkey = openssl_pkey_get_details($keys);
$pubkey = $pubkey["key"];
if (empty($privkey) || empty($pubkey)) {
// if unable to construct keys, then force exit
error_log("OpenEMR error - key construction broken during oauth2, so forced exit");
http_response_code(500);
exit;
}
// third, store the keys on drive and store the passphrase in the database
file_put_contents($this->privateKey, $privkey);
chmod($this->privateKey, 0640);
file_put_contents($this->publicKey, $pubkey);
sqlStatementNoLog("INSERT INTO `keys` (`name`, `value`) VALUES ('oauth2passphrase', ?)", [$this->cryptoGen->encryptStandard($this->passphrase)]);
}
// confirm existence of passphrase
$pkey = sqlQueryNoLog("SELECT `name`, `value` FROM `keys` WHERE `name` = 'oauth2passphrase'");
if (!empty($pkey['name']) && ($pkey['name'] == 'oauth2passphrase')) {
$this->passphrase = $this->cryptoGen->decryptStandard($pkey['value']);
if (empty($this->passphrase)) {
// if decrypted pssphrase is empty, then critical error and must exit
error_log("OpenEMR error - oauth2 passphrase was blank after it was decrypted, so forced exit");
http_response_code(500);
exit;
}
} else {
// oauth2passphrase is missing so must exit
error_log("OpenEMR error - oauth2 passphrase is missing, so forced exit");
http_response_code(500);
exit;
}
// confirm existence of key pair
if (!file_exists($this->privateKey) || !file_exists($this->publicKey)) {
// key pair is missing so must exit
error_log("OpenEMR error - oauth2 keypair is missing, so forced exit");
http_response_code(500);
exit;
}

$this->providerForm = $providerForm;
}

Expand Down Expand Up @@ -354,7 +454,7 @@ public function getAuthorizationServer(): AuthorizationServer
new ClientRepository(),
new AccessTokenRepository(),
new ScopeRepository(),
$this->privateKey,
new CryptKey($this->privateKey, $this->passphrase),
$this->encryptionKey,
$responseType
);
Expand Down

0 comments on commit 59ec53d

Please sign in to comment.