-
Type: Feature Request
-
Status: Resolved
-
Priority: Minor
-
Resolution: Fixed
-
Affects Version/s: None
-
Fix Version/s: 5.2.2-B3
-
Component/s: Security
-
Labels:None
-
Change Log Group:Added
-
Change Log Message:Added classes for performing secure encryption/decryption and random value generation
-
Story Points:3
-
Copy Issue Key:
-
Patch Instructions:
Plan (part 1 - dependencies): - 0.5h (sum)
- add https://github.com/paragonie/random_compat Composer dependency (polyfill for "random_bytes" function added in PHP 7.0)
- add https://github.com/symfony/polyfill-php55 Composer dependency (polyfill for "hash_pbkdf2" function added in PHP 5.5)
- 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)
- create final "SecurityGeneratorPromise" class with: - 0.2h
- these protected properties:
- "$method"
- "$arguments = array()"
- "$resolvedValue"
- "$asSignature = false;"
- "$signatureRawOutput = false;"
- "$signatureKeyOverride"
- these constants:
- "TYPE_NUMBER" = 1
- "TYPE_STRING" = 2
- "TYPE_BYTES" = 3
- these protected properties:
- add "SecurityGeneratorPromise::__construct($type, array $arguments = array())" method, that will: - 0.3h
- have mapping between "self::TYPE_" constants and actual "resolve" methods
- get {{'resolve*' }}method from mapping and throw exception if not found
- assign arguments to corresponding class properties
- add public "SecurityGeneratorPromise::__toString" method, that will: - 0.1h
- if the "$this->resolvedValue" property isn't empty take it's value
- if the "$this->resolvedValue" property is empty:
- call $this->method with $this->arguments
- store result in "$this->resolvedValue" property
- if "$this->asSignature" is set to "false" then return "$this->resolvedValue" property
- if "$this->asSignature" is set to "true" then return "SecurityEncryptor::createSignature($this->resolvedValue, $this->signatureRawOutput, $this->signatureKeyOverride)" result
- add public "SecurityGeneratorPromise::forDatabase($prefix_or_table, $column)" method, that will: - 0.4h
-
- typecast $this into string (would internally call "__toString" method)
- if "$prefix_or_table" is unit config prefix, then get database table name from it
- query database table column for generated string
- if match found, then clear "$this->resolvedValue" property & repeat request
- if no match found return the value
- add public "SecurityGeneratorPromise::asSignature($raw_output = false, $key_override = null)" method, that will: - 0.3h
- set "$this->asSignature" property to "true"
- set "$this->signatureRawOutput" property to "$raw_output" parameter value
- set "$this->signatureKeyOverride" property to "$key_override" parameter value
- add public "SecurityGeneratorPromise::asValue()" method, that will: - 0.2h
- set "$this->asSignature" property to "false"
- set "$this->signatureRawOutput" property to "false"
- set "$this->signatureKeyOverride" property to "null"
- add protected "SecurityGeneratorPromise::resolveNumber($min, $max)" method, that will return result of calling "random_int" function - 0.3h
- 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
- 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"
- 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
- generate secure random token by calling random_bytes($length)
- when "$raw_output" parameter is set to "false" wrap result with "bin2hex" function call (would double result length)
- return the result
- when $raw_output = false, then result is hex-encoded = twice as long as requested length
- create final "SecurityGenerator" class - 0.2h
- add these public methods, that would create "SecurityGeneratorPromise" with given arguments and this type: - 0.5h
- "SecurityGenerator::generateNumber($min, $max)" - SecurityGeneratorPromise::TYPE_NUMBER
- "SecurityGenerator::generateString($length, $characters = '')" - SecurityGeneratorPromise::TYPE_STRING
- "SecurityGenerator::generateBytes($length = 16, $raw_output = false)" - SecurityGeneratorPromise::TYPE_BYTES
Plan (part 3 - encryption) - 4.5h (sum)
- create "SecurityEncryptor" class - 0.2h
- 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)
- 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)
- 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
- 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);
- add protected "SecurityEncryptor::doGetHmacKey($key_override = null)" method, that will: - 0.2h
- if "$key_override" parameter is specified, then return it's value
- otherwise return value of "SecurityHmacKey" setting from "/system/config.php"
- add private "SecurityEncryptor::_getHmacKey($key_override = null)" method, that will: - 0.2h
- call "$this->doGetHmacKey($key_override)" method and assign result to "$ret" variable
- if "$ret" variable value is empty ('') or missing (false) throw an exception
- if "$ret" variable value length is self::HASHING_KEY_LENGTH, then return it
- return $this->derivateKey($ret, self::HASHING_KEY_LENGTH)
- 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
- add protected "SecurityEncryptor::doGetEncryptionKey($key_override = null)" method, that will: - 0.2h
- if "$key_override" parameter is specified, then return it's value
- otherwise:
- get value of "SecurityEncryptionKey" value (expected 32 bytes, because it's in hex form) from "/system/config.php" into a "$ret" variable
- convert it from hex to binary form using pack('H*', $hex_string)
- return it
- add private "SecurityEncryptor::_getEncryptionKey($key_override = null)" method, that will: - 0.2h
- call "$this->doGetEncryptionKey($key_override)" method and assign result to "$ret" variable
- if "$ret" variable value is empty ('') or missing (false) throw an exception
- if "$ret" variable value length is self::ENCRYPTION_KEY_LENGTH, then return it
- return $this->derivateKey($ret, self::ENCRYPTION_KEY_LENGTH)
- add public final "SecurityEncryptor::encrypt($plaintext, $key_override = null)" and public final "SecurityEncryptor::decrypt($ciphertext, $key_override = null)" methods like so: - 1.5h
- take implementation from https://paragonie.com/blog/2015/05/using-encryption-and-authentication-correctly page (the "setLessUnsafeCookie" and "getLessUnsafeCookie" functions)
- 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)
- use "SecurityEncryptor::ENCRYPTION_METHOD" instead of "aes-256-cbc" cipher
- use "SecurityEncryptor::ENCRYPTION_KEY_LENGTH" key length instead of 32 bytes
- use "$this->createSignature" instead of calling "hash_hmac" directly
- call "$this->_getEncryptionKey($key_override)" to get encryption key (externally provided key can be used for re-encrypting data with different key)
- when decryption fails throw an exception right away
- 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
- add public final "SecurityEncryptor::cipherAvailable()" method, that will: - 0.5h
- call "openssl_get_cipher_methods" function
- return true, when "self::ENCRYPTION_METHOD" is present in above obtained array
- return false otherwise
Quote: 8.5h*1.4=12h
- is blocked by
-
INP-1422 Store class map on disk
- Resolved
- implemented in
-
[Diffusion] rINP16691 Fixes INP-1756 - Create "Security*" classes for security-related jobs
-
[Diffusion] rINP16725 Fixes INP-1756 - Create "Security*" classes for security-related jobs
-
D354 INP-1756 - Create "Security*" classes for security-related jobs
-
D426 INP-1756 - Create "Security*" classes for security-related jobs
(2 mentioned in)