-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add generic encryption methods for use on WordPress.org. (#390)
* Add initial Encryption methods. * Clarify what '$additional_data' is. * Tests: Add tests for the Encryption methods. * Use #[\SensitiveParameter] within HiddenString. * Encrypted values don't need to return HiddenString. * Remove the non-auth encryption methods, and require $additional_data be specified. * Rename $additional_data to $context for easier reading.
- Loading branch information
Showing
5 changed files
with
604 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
<?php | ||
namespace WordPressdotorg\MU_Plugins\Encryption; | ||
/** | ||
* Class HiddenString. This is a copy of https://github.com/paragonie/hidden-string without the additional dependencies. | ||
* | ||
* The purpose of this class is to encapsulate strings and hide their contents | ||
* from stack traces should an unhandled exception occur. | ||
* | ||
* The only things that should be protected: | ||
* - Passwords | ||
* - Plaintext (before encryption) | ||
* - Plaintext (after decryption) | ||
* | ||
* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
*/ | ||
final class HiddenString | ||
{ | ||
/** | ||
* @var string | ||
*/ | ||
protected $internalStringValue = ''; | ||
|
||
/** | ||
* Disallow the contents from being accessed via __toString()? | ||
* | ||
* @var bool | ||
*/ | ||
protected $disallowInline = false; | ||
|
||
/** | ||
* Disallow the contents from being accessed via __sleep()? | ||
* | ||
* @var bool | ||
*/ | ||
protected $disallowSerialization = false; | ||
|
||
/** | ||
* HiddenString constructor. | ||
* @param string $value | ||
* @param bool $disallowInline | ||
* @param bool $disallowSerialization | ||
* | ||
* @throws \TypeError | ||
*/ | ||
public function __construct( | ||
#[\SensitiveParameter] | ||
string $value, | ||
bool $disallowInline = true, | ||
bool $disallowSerialization = true | ||
) { | ||
$this->internalStringValue = self::safeStrcpy($value); | ||
$this->disallowInline = $disallowInline; | ||
$this->disallowSerialization = $disallowSerialization; | ||
} | ||
|
||
/** | ||
* @param HiddenString $other | ||
* @return bool | ||
* @throws \TypeError | ||
*/ | ||
public function equals(HiddenString $other) | ||
{ | ||
return \hash_equals( | ||
$this->getString(), | ||
$other->getString() | ||
); | ||
} | ||
|
||
/** | ||
* Hide its internal state from var_dump() | ||
* | ||
* @return array | ||
*/ | ||
public function __debugInfo() | ||
{ | ||
return [ | ||
'internalStringValue' => | ||
'*', | ||
'attention' => | ||
'If you need the value of a HiddenString, ' . | ||
'invoke getString() instead of dumping it.' | ||
]; | ||
} | ||
|
||
/** | ||
* Wipe it from memory after it's been used. | ||
* @return void | ||
*/ | ||
public function __destruct() | ||
{ | ||
if (\is_callable('\sodium_memzero')) { | ||
try { | ||
\sodium_memzero($this->internalStringValue); | ||
return; | ||
} catch (\Throwable $ex) { | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Explicit invocation -- get the raw string value | ||
* | ||
* @return string | ||
* @throws \TypeError | ||
*/ | ||
public function getString(): string | ||
{ | ||
return self::safeStrcpy($this->internalStringValue); | ||
} | ||
|
||
/** | ||
* Returns a copy of the string's internal value, which should be zeroed. | ||
* Optionally, it can return an empty string. | ||
* | ||
* @return string | ||
* @throws \TypeError | ||
*/ | ||
public function __toString(): string | ||
{ | ||
if (!$this->disallowInline) { | ||
return self::safeStrcpy($this->internalStringValue); | ||
} | ||
return ''; | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public function __sleep(): array | ||
{ | ||
if (!$this->disallowSerialization) { | ||
return [ | ||
'internalStringValue', | ||
'disallowInline', | ||
'disallowSerialization' | ||
]; | ||
} | ||
return []; | ||
} | ||
|
||
/** | ||
* PHP 7 uses interned strings. We don't want altering this one to alter | ||
* the original string. | ||
* | ||
* @param string $string | ||
* @return string | ||
* @throws \TypeError | ||
*/ | ||
public static function safeStrcpy(string $string): string | ||
{ | ||
$length = mb_strlen($string, '8bit'); | ||
$return = ''; | ||
/** @var int $chunk */ | ||
$chunk = $length >> 1; | ||
if ($chunk < 1) { | ||
$chunk = 1; | ||
} | ||
for ($i = 0; $i < $length; $i += $chunk) { | ||
$return .= mb_substr($string, $i, $chunk, '8bit'); | ||
} | ||
return $return; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
<?php | ||
use WordPressdotorg\MU_Plugins\Encryption\HiddenString; | ||
/** | ||
* This file contains globally-exported function names for the Encryption plugin. | ||
* | ||
* It provides a wrapper around the libsodium's Authenticated Encryption with | ||
* Additional Data ciphers (AEAD with XChaCha20-Poly1305) | ||
* | ||
* NOTE: $context should always be passed, and should either be set to the stringy User ID, or a unique-per-item string. | ||
* The context is not stored within the Encrypted data, but is used to validate that the value is being decrypted in the same context. | ||
*/ | ||
|
||
/** | ||
* Encrypt a value, with authentication. | ||
* | ||
* Unlike the Encryption plugin, this function simply returns false for any errors. | ||
* | ||
* @param string $value The plaintext value. | ||
* @param string $context Additional, authenticated data. This is used in the verification of the authentication tag appended to the ciphertext, but it is not encrypted or stored in the ciphertext. | ||
* @param string $key_name The name of the key to use for encryption. Optional. | ||
* @return string|false The encrypted value, or false on error. | ||
*/ | ||
function wporg_encrypt( $value, string $context, string $key_name = '' ) { | ||
try { | ||
return \WordPressdotorg\MU_Plugins\Encryption\encrypt( $value, $context, $key_name ); | ||
} catch ( Exception $e ) { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Decrypt a value, with authentication. | ||
* | ||
* Unlike the Encryption plugin, this function simply returns false for any errors, and | ||
* HiddenStrings that can be cast to string as needed. | ||
* | ||
* @param string $value The encrypted value. | ||
* @param string $context Additional, authenticated data. This is used in the verification of the authentication tag appended to the ciphertext, but it is not encrypted or stored in the ciphertext. | ||
* @param string $key_name The name of the key to use for decryption. Optional. | ||
* @return HiddenString|false The decrypted value stored within a HiddenString instance, or false on error. | ||
*/ | ||
function wporg_decrypt( string $value, string $context, string $key_name = '' ) { | ||
try { | ||
$value = \WordPressdotorg\MU_Plugins\Encryption\decrypt( $value, $context, $key_name ); | ||
|
||
return new HiddenString( $value->getString(), false ); | ||
} catch ( Exception $e ) { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Determine if a value is encrypted. | ||
* | ||
* @param HiddenString|string $value The value to check. | ||
* @return bool True if the value is encrypted, false otherwise. | ||
*/ | ||
function wporg_is_encrypted( string $value ) : bool { | ||
return \WordPressdotorg\MU_Plugins\Encryption\is_encrypted( $value ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
<?php | ||
namespace WordPressdotorg\MU_Plugins\Encryption; | ||
use Exception; | ||
/** | ||
* Plugin Name: WordPress.org Encryption | ||
* Description: Encryption functions for use on WordPress.org. | ||
*/ | ||
require __DIR__ . '/exports.php'; | ||
|
||
/** | ||
* Prefix for encrypted secrets. Contains a version identifier. | ||
* | ||
* $t1$ -> v1 (RFC 6238, encrypted with XChaCha20-Poly1305, with a key derived from HMAC-SHA256 | ||
* of the defined key. | ||
* | ||
* @var string | ||
*/ | ||
const PREFIX = '$t1$'; | ||
|
||
/** | ||
* The length of the keys. | ||
* | ||
* @var int | ||
*/ | ||
const KEY_LENGTH = SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES; | ||
|
||
/** | ||
* The length of the per-encrypted-item nonce. | ||
* | ||
* @var int | ||
*/ | ||
const NONCE_LENGTH = SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES; | ||
|
||
/** | ||
* Encrypt a value. | ||
* | ||
* @param string $value Value to encrypt. | ||
* @param string $context Additional, authenticated data. This is used in the verification of the authentication tag appended to the ciphertext, but it is not encrypted or stored in the ciphertext. | ||
* @param string $key_name The name of the key to use for encryption. Optional. | ||
* @return string Encrypted value, exceptions thrown on error. | ||
*/ | ||
function encrypt( $value, string $context, string $key_name = '' ) { | ||
$nonce = random_bytes( NONCE_LENGTH ); | ||
if ( ! $nonce ) { | ||
throw new Exception( 'Unable to create a nonce.' ); | ||
} | ||
|
||
if ( empty( $context ) ) { | ||
throw new Exception( '$context cannot be empty.' ); | ||
} | ||
|
||
if ( $value instanceOf HiddenString ) { | ||
$value = $value->getString(); | ||
} | ||
|
||
$key = get_encryption_key( $key_name ); | ||
$encrypted = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( $value, $context, $nonce, $key->getString() ); | ||
|
||
sodium_memzero( $value ); | ||
|
||
return PREFIX . sodium_bin2hex( $nonce . $encrypted ); | ||
} | ||
|
||
/** | ||
* Decrypt a value. | ||
* | ||
* @param string $value Value to decrypt. | ||
* @param string $context Additional, authenticated data. This is used in the verification of the authentication tag appended to the ciphertext, but it is not encrypted or stored in the ciphertext. | ||
* @param string $key_name The name of the key to use for decryption. Optional. | ||
* @return HiddenString Decrypted value. | ||
*/ | ||
function decrypt( string $value, string $context, string $key_name = '' ) : HiddenString { | ||
if ( ! is_encrypted( $value ) ) { | ||
throw new Exception( 'Value is not encrypted.' ); | ||
} | ||
|
||
// Remove the prefix, and convert back to binary. | ||
$value = mb_substr( $value, mb_strlen( PREFIX, '8bit' ), null, '8bit' ); | ||
$value = sodium_hex2bin( $value ); | ||
|
||
if ( mb_strlen( $value, '8bit' ) < NONCE_LENGTH ) { | ||
throw new Exception( 'Invalid cipher text.' ); | ||
} | ||
|
||
$key = get_encryption_key( $key_name ); | ||
$nonce = mb_substr( $value, 0, NONCE_LENGTH, '8bit' ); | ||
$value = mb_substr( $value, NONCE_LENGTH, null, '8bit' ); | ||
$plaintext = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( $value, $context, $nonce, $key->getString() ); | ||
|
||
sodium_memzero( $nonce ); | ||
sodium_memzero( $value ); | ||
|
||
if ( false === $plaintext ) { | ||
throw new Exception( 'Invalid cipher text.' ); | ||
} | ||
|
||
return new HiddenString( $plaintext ); | ||
} | ||
|
||
/** | ||
* Check if a value is encrypted. | ||
* | ||
* @param HiddenString|string $value Value to check. | ||
* @return bool True if the value is encrypted, false otherwise. | ||
*/ | ||
function is_encrypted( $value ) { | ||
if ( $value instanceOf HiddenString ) { | ||
$value = $value->getString(); | ||
} | ||
|
||
if ( ! str_starts_with( $value, PREFIX ) ) { | ||
return false; | ||
} | ||
|
||
if ( mb_strlen( $value, '8bit' ) < NONCE_LENGTH + mb_strlen( PREFIX, '8bit' ) ) { | ||
return false; | ||
} | ||
|
||
sodium_memzero( $value ); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Get the encryption key. | ||
* | ||
* @param string $key_name The name of the key to use for decryption. | ||
* @return HiddenString The encryption key. | ||
*/ | ||
function get_encryption_key( string $key_name = '' ) { | ||
|
||
$keys = []; | ||
if ( function_exists( 'wporg_encryption_keys' ) ) { | ||
$keys = wporg_encryption_keys(); | ||
} | ||
|
||
if ( ! $key_name ) { | ||
$key_name = 'default'; | ||
} | ||
|
||
if ( ! isset( $keys[ $key_name ] ) ) { | ||
throw new Exception( sprintf( 'Encryption key "%s" not defined.', $key_name ) ); | ||
} | ||
|
||
return $keys[ $key_name ]; | ||
} | ||
|
||
/** | ||
* Generate a random encryption key. | ||
* | ||
* @return HiddenString The encryption key. | ||
*/ | ||
function generate_encryption_key() { | ||
return new HiddenString( random_bytes( KEY_LENGTH ) ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.