Skip to content
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

Add Encryption methods. #390

Merged
merged 19 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
482235d
Add initial Encryption methods.
dd32 Apr 12, 2023
65cd2e6
Clarify what '$additional_data' is.
dd32 Apr 13, 2023
e887fb8
Add wporg_authenticated_encrypt() and wporg_authenticated_decrypt() w…
dd32 Apr 13, 2023
dc879db
Remove the $key parameter from is_encrypted(). It's not an ideal scen…
dd32 Apr 13, 2023
542fe9f
Always use mb_strlen() for consistentness.
dd32 Apr 13, 2023
636e792
Tests: Add tests for the Encryption methods.
dd32 Apr 13, 2023
0b6a979
Whitespace and zero'ing of strings.
dd32 Apr 14, 2023
52133b1
Standardise on Exceptions with a trailing full-stop.
dd32 Apr 14, 2023
e50fd63
get_encryption_key() throws an exception, there's no need to check it…
dd32 Apr 14, 2023
0651e3b
Tests: Don't use `expectException()` and instead test the exceptions …
dd32 Apr 14, 2023
6c9d58a
Use #[\SensitiveParameter] within HiddenString.
dd32 Apr 18, 2023
3d5b2f6
Switch from storing keys in constants, and instead within the return …
dd32 Apr 18, 2023
3caed56
Encrypted values don't need to return HiddenString.
dd32 Apr 18, 2023
6f8d488
Remove the non-auth encryption methods, and require $additional_data …
dd32 Apr 18, 2023
a484032
Rename $additional_data to $context for easier reading.
dd32 Apr 18, 2023
6468c2d
Rename the $key parameters to $key_name to better define that it's th…
dd32 Apr 18, 2023
c12a107
encrypt() no longer returns HiddenString, so when decrypting we don't…
dd32 Apr 18, 2023
1face28
Document the key methods as returning a HiddenString.
dd32 Apr 18, 2023
c535603
Correct incorrect types in docblock.
dd32 Apr 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions mu-plugins/encryption/class-hiddenstring.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?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(
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;
}
}
56 changes: 56 additions & 0 deletions mu-plugins/encryption/exports.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
use WordPressdotorg\MU_Plugins\Encryption\HiddenString;
/**
* This file contains globally-exported function names for the Encryption plugin.
*/

/**
* Encrypt a value.
*
* 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 plaintext value.
* @param string $key The key to use for encryption. Optional.
* @return string|false The encrypted value, or false on error.
*/
function wporg_encrypt( string $value, string $key = '' ) {
try {
$value = \WordPressdotorg\MU_Plugins\Encryption\encrypt( $value, '', $key );

return new HiddenString( $value->getString(), false );
} catch ( Exception $e ) {
return false;
}
}

/**
* Decrypt a value.
*
* 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 $key The key to use for decryption. Optional.
* @return string|false The decrypted value, or false on error.
*/
function wporg_decrypt( string $value, string $key = '' ) {
try {
$value = \WordPressdotorg\MU_Plugins\Encryption\decrypt( $value, '', $key );

return new HiddenString( $value->getString(), false );
} catch ( Exception $e ) {
return false;
}
}

/**
* Determine if a value is encrypted.
*
* @param string $value The value to check.
* @param string $key Check if it uses this key. Optional. If not specified, it only validates that it's encrypted.
* @return bool True if the value is encrypted, false otherwise.
*/
function wporg_is_encrypted( string $value, string $key = '' ) : bool {
return \WordPressdotorg\MU_Plugins\Encryption\is_encrypted( $value, $key );
}
160 changes: 160 additions & 0 deletions mu-plugins/encryption/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?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;
const NONCE_LENGTH = SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES;

/**
* Encrypt a value.
*
* @param string $value Value to encrypt.
* @param string $additional_data Additional data to include in the encryption. Optional.
* @param string $key Key to use for encryption. Optional.
* @return string Encrypted value, exceptions thrown on error.
*/
function encrypt( $value, string $additional_data = '', string $key = '' ) {
$key = get_encryption_key( $key );
$nonce = random_bytes( NONCE_LENGTH );
if ( ! $key || ! $nonce ) {
throw new Exception( 'Unable to create a nonce.' );
}

if ( $value instanceOf HiddenString ) {
$value = $value->getString();
}

$encrypted = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( $value, $additional_data, $nonce, $key->getString() );

sodium_memzero( $value );

return new HiddenString( PREFIX . sodium_bin2hex( $nonce . $encrypted ) );
}

/**
* Decrypt a value.
*
* @param string $value Value to decrypt.
* @param string $additional_data Additional data to include in the encryption. Optional.
* @param string $key Key to use for decryption. Optional.
* @return string Decrypted value.
*/
function decrypt( $value, string $additional_data = '', string $key = '' ) : HiddenString {
// Fetch the encryption key.
$key = get_encryption_key( $key );
if ( ! $key ) {
throw new Exception( 'Unable to get the encryption key.' );
}

if ( $value instanceOf HiddenString ) {
$value = $value->getString();
}

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' );
}

$nonce = mb_substr( $value, 0, NONCE_LENGTH, '8bit' );
$value = mb_substr( $value, NONCE_LENGTH, null, '8bit' );
$plaintext = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( $value, $additional_data, $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 string $value Value to check.
* @param string $key Key to use for decryption. Optional.
* @return bool True if the value is encrypted, false otherwise.
*/
function is_encrypted( $value, string $key = '' ) {
if ( $value instanceOf HiddenString ) {
$value = $value->getString();
}

if ( ! str_starts_with( $value, PREFIX ) ) {
return false;
}

if ( mb_strlen( $value, '8bit' ) < NONCE_LENGTH + strlen( PREFIX ) ) {
return false;
}

// Check if that's the key.
if ( $key ) {
try {
if ( ! decrypt( $value, $key ) ) {
return false;
}
} catch ( Exception $e ) {
return false;
}
}

return true;
}

/**
* Get the encryption key.
*
* @param string $key The key to use for decryption.
* @return string The encryption key.
*/
function get_encryption_key( $key = '' ) {
$constant = 'WPORG_ENCRYPTION_KEY';

if ( $key ) {
$constant = 'WPORG_' . str_replace( '-', '_', strtoupper( $key ) ) . '_ENCRYPTION_KEY';
}

if ( ! defined( $constant ) ) {
throw new Exception( sprintf( 'Encryption key "%s" not defined.', $constant ) );
}

return new HiddenString( sodium_hex2bin( constant( $constant ) ) );
}

/**
* Generate a random encryption key.
*
* @return string The encryption key.
*/
function generate_encryption_key() {
return sodium_bin2hex( random_bytes( KEY_LENGTH ) );
}
1 change: 1 addition & 0 deletions mu-plugins/loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@
require_once __DIR__ . '/rest-api/index.php';
require_once __DIR__ . '/skip-to/skip-to.php';
require_once __DIR__ . '/db-user-sessions/index.php';
require_once __DIR__ . '/encryption/index.php';