Create "Security*" classes for security-related jobs

Components

Labels

Description

Plan (part 1 - dependencies): - 0.5h (sum)

  1. add https://github.com/paragonie/random_compat Composer dependency (polyfill for "random_bytes" function added in PHP 7.0)

  2. add https://github.com/symfony/polyfill-php55 Composer dependency (polyfill for "hash_pbkdf2" function added in PHP 5.5)

  3. add https://github.com/symfony/polyfill-php56 Composer dependency (polyfill for "hash_equals" function added in PHP 5.6)

Plan (part 2 - CSPRNG): - 3.5h (sum)

  1. create final "*SecurityGeneratorPromise*" class with: 

    • 0.2h

    1. these protected properties:

      • "$method"

      • "$arguments = array()"

      • "$resolvedValue"

      • "$asSignature = false;"

      • "$signatureRawOutput = false;"

      • "$signatureKeyOverride"

    2. these constants:

      1. "TYPE_NUMBER" = 1

      2. "TYPE_STRING" = 2

      3. "TYPE_BYTES" = 3

  2. add "*SecurityGeneratorPromise::__construct($type, array $arguments = array())*" method, that will: 

    • 0.3h

    1. have mapping between "self::TYPE_" constants and actual "resolve" methods

    2. get {{'resolve*' }}method from mapping and throw exception if not found

    3. assign arguments to corresponding class properties

  3. add public "*SecurityGeneratorPromise::__toString*" method, that will: 

    • 0.1h

    1. if the "$this->resolvedValue" property isn't empty take it's value

    2. if the "$this->resolvedValue" property is empty:

      1. call $this->method with $this->arguments

      2. store result in "$this->resolvedValue" property

    3. if "$this->asSignature" is set to "false" then return "$this->resolvedValue" property

    4. if "$this->asSignature" is set to "true" then return "SecurityEncryptor::createSignature($this->resolvedValue, $this->signatureRawOutput, $this->signatureKeyOverride)" result

  4. add public "*SecurityGeneratorPromise**::forDatabase(**$prefix_or_table, $column**)*" method, that will: 

    • 0.4h

  1.  

    1. typecast $this into string (would internally call "__toString" method)

    2. if "$prefix_or_table" is unit config prefix, then get database table name from it

    3. query database table column for generated string

    4. if match found, then clear "$this->resolvedValue" property & repeat request

    5. if no match found return the value

  2. add public "SecurityGeneratorPromise::asSignature($raw_output = false, $key_override = null)" method, that will: 

    • 0.3h

    1. set "$this->asSignature" property to "true"

    2. set "$this->signatureRawOutput" property to "$raw_output" parameter value

    3. set "$this->signatureKeyOverride" property to "$key_override" parameter value

  3. add public "SecurityGeneratorPromise::asValue()" method, that will: 

    • 0.2h

    1. set "$this->asSignature" property to "false"

    2. set "$this->signatureRawOutput" property to "false"

    3. set "$this->signatureKeyOverride" property to "null"

  4. add protected "SecurityGeneratorPromise::resolveNumber($min, $max)" method, that will return result of calling "random_int" function 

    • 0.3h

  5. from "\RandomLib\Generator" class (see https://github.com/ircmaxell/RandomLib/blob/master/lib/RandomLib/Generator.php): 

    • 0.5h

  •  

    • copy all class constants

    • copy "\RandomLib\Generator::$charArrays" property

    • copy "\RandomLib\Generator::expandCharacterSets" method

  1. copy "UserHelper::generateRandomString" method from In-Portal 5.3.x and for it: 

    • 0.5h

    • rename into protected "SecurityGeneratorPromise::resolveString" method

    • as "\RandomLib\Generator::generateString" method:

      • change signature into "$length, $characters = ''"

      • call the "SecurityGeneratorPromise::expandCharacterSets"

    • replace call to "\SecurityGeneratorPromise::_generateRandomNumber" method with "SecurityGeneratorPromise::generateNumber"

    • rename "$password" local variable into "$ret"

  2. add protected "SecurityGenerator::resolveBytes($length = 16, $raw_output = false)" function (see "generateToken" function on https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence page), that would: 

    • 0.5h

    1. generate secure random token by calling random_bytes($length)

    2. when "$raw_output" parameter is set to "false" wrap result with "bin2hex" function call (would double result length)

    3. return the result

    4. when $raw_output = false, then result is hex-encoded = twice as long as requested length

  3. create final "SecurityGenerator" class 

    • 0.2h

  4. add these public methods, that would create "SecurityGeneratorPromise" with given arguments and this type: 

    • 0.5h

    1. "SecurityGenerator::generateNumber($min, $max)" - SecurityGeneratorPromise::TYPE_NUMBER

    2. "*SecurityGenerator**::generateString($length, $characters = '')*" - SecurityGeneratorPromise::TYPE_STRING

    3. "*SecurityGenerator**::generateBytes($length = 16, $raw_output = false)*" - SecurityGeneratorPromise::TYPE_BYTES

Plan (part 3 - encryption) - 4.5h (sum)

  1. create "SecurityEncryptor" class 

    • 0.2h

  2. add these constants: 

    • 0.3h

    • "SecurityEncryptor::HASHING_ALGORITHM" constant with "sha256" value

    • "SecurityEncryptor::HASHING_KEY_LENGTH" constant with "*32*" value

    • "SecurityEncryptor::ENCRYPTION_METHOD" constant with "*aes-128-cbc*" value (or "*aes-256-cbc*" value)

    • "SecurityEncryptor::ENCRYPTION_KEY_LENGTH" constant with "*16*" value (or "*32*" value)

  3. on the "System Requirements" step during In-Portal installation/upgrade: 

    • 0.5h

    • require "openssl" extension (check for "openssl_encrypt" function) to be present

    • require cipher presence (call "SecurityEncryptor::cipherAvailable" method)

  4. during installation/upgrade (the "\kInstallator::Run" method and "sys_config" step), when missing in the "/system/config.php" add these settings: 

    • 0.5h

  •  

    • "SecurityHmacKey" with value generated via "SecurityGenerator::generateString(self::HASHING_KEY_LENGTH, self::CHAR_ALNUM | self::CHAR_SYMBOLS)" method call

    • "SecurityEncryptionKey" with value generated via "SecurityGenerator::generateBytes(self::ENCRYPTION_KEY_LENGTH)" method call

  1. add public final "SecurityEncryptor::derivateKey($key, $length)" method, that will: 

    • 0.1h

    • return hash_pbkdf2(self::HASHING_ALGORITHM, $string, SecurityGenerator::generateBytes(16, true), 80000, $length, true);

  2. add protected "SecurityEncryptor::doGetHmacKey($key_override = null)" method, that will: 

    • 0.2h

    1. if "$key_override" parameter is specified, then return it's value

    2. otherwise return value of "SecurityHmacKey" setting from "/system/config.php"

  3. add private "SecurityEncryptor::_getHmacKey($key_override = null)" method, that will: 

    • 0.2h

    1. call "$this->doGetHmacKey($key_override)" method and assign result to "$ret" variable

    2. if "$ret" variable value is empty ('') or missing (false) throw an exception

    3. if "$ret" variable value length is self::HASHING_KEY_LENGTH, then return it

    4. return $this->derivateKey($ret, self::HASHING_KEY_LENGTH)

  4. add public final "SecurityEncryptor::createSignature($string, $raw_output = false, $key_override = null)" method, that would: 

    • 0.1h

    • return hash_hmac(self::HASHING_ALGORITHM, $string, $this->_getHmacKey($key_override), $raw_output);

    • if $raw_data = false, then response is 32 symbols long

    • if $raw_data = true, then response is 64 symbols long

  5. add protected "SecurityEncryptor::doGetEncryptionKey($key_override = null)" method, that will: 

    • 0.2h

    1. if "$key_override" parameter is specified, then return it's value

    2. otherwise:

      1. get value of "SecurityEncryptionKey" value (expected 32 bytes, because it's in hex form) from "/system/config.php" into a "$ret" variable

      2. convert it from hex to binary form using pack('H*', $hex_string)

      3. return it

  6. add private "SecurityEncryptor::_getEncryptionKey($key_override = null)" method, that will: 

    • 0.2h

    1. call "$this->doGetEncryptionKey($key_override)" method and assign result to "$ret" variable

    2. if "$ret" variable value is empty ('') or missing (false) throw an exception

    3. if "$ret" variable value length is self::ENCRYPTION_KEY_LENGTH, then return it

    4. return $this->derivateKey($ret, self::ENCRYPTION_KEY_LENGTH)

  7. add public final "SecurityEncryptor*::encrypt*($plaintext, $key_override = null)" and public final "SecurityEncryptor*::decrypt*($ciphertext, $key_override = null)" methods like so: 

    • 1.5h

    1. take implementation from https://paragonie.com/blog/2015/05/using-encryption-and-authentication-correctly page (the "setLessUnsafeCookie" and "getLessUnsafeCookie" functions)

    2. replace "mcrypt" usage with "openssl" equivalent from https://paragonie.com/blog/2015/05/if-you-re-typing-word-mcrypt-into-your-code-you-re-doing-it-wrong page  (the methods of "UnsafeOpensslAES" class)

    3. use "SecurityEncryptor::ENCRYPTION_METHOD" instead of "aes-256-cbc" cipher

    4. use "SecurityEncryptor::ENCRYPTION_KEY_LENGTH" key length instead of 32 bytes

    5. use "$this->createSignature" instead of calling "hash_hmac" directly

    6. call "$this->_getEncryptionKey($key_override)" to get encryption key (externally provided key can be used for re-encrypting data with different key)

    7. when decryption fails throw an exception right away

    8. validate data during decryption process (e.g. https://github.com/defuse/php-encryption/blob/master/src/Crypto.php#L221) and throw an exception early as soon as we realize forgery attempt

  8. add public final "SecurityEncryptor*::cipherAvailable*()" method, that will: 

    • 0.5h

    1. call "openssl_get_cipher_methods" function

    2. return true, when "self::ENCRYPTION_METHOD" is present in above obtained array 

    3. return false otherwise

Quote: 8.5h*1.4=12h

Context Information

None

Additional information (do not use)

None

is blocked by

Activity

[API] Administrator August 29, 2022 at 7:49 AM

User committed a fix to 5.2.x. Commit Message:

Fixes - Create "Security*" classes for security-related jobs

Differential Revision: http://qa.in-portal.org/D426

Alex August 23, 2022 at 4:13 PM

Need to generate encryption/hmac keys as soon as possible to make them available immediately after kApplicaition class can be initialized.

Alex March 18, 2021 at 4:29 PM

alex closed D354: - Create "Security*" classes for security-related jobs.
alex updated the diff for D354: - Create "Security*" classes for security-related jobs.

Alex March 18, 2021 at 2:29 PM

alex added "INP-1756" JIRA issue(s) to rINP16691: Fixes - Create "Security*" classes for security-related jobs.

Alex March 18, 2021 at 2:29 PM

alex committed rINP16691: Fixes - Create "Security*" classes for security-related jobs.

Fixes - Create "Security*" classes for security-related jobs

Differential Revision: http://qa.in-portal.org/D354

Fixed

Details

Priority

Assignee

Reporter

Developer

Reviewer

Change Log Group

Added

Change Log Message

Patch Instructions

Patches must be submitted through Phabricator.

Time tracking

6h 42m logged5h 20m remaining

Story Points

Original estimate

Fix versions

Created November 13, 2018 at 3:54 PM
Updated December 29, 2024 at 8:49 PM
Resolved August 29, 2022 at 7:49 AM