Uploaded image for project: 'In-Portal CMS'
  1. In-Portal CMS
  2. INP-1756

Create "Security*" classes for security-related jobs

    XMLWordPrintable

    Details

    • 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:

      Patches must be submitted through Phabricator.

      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. 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
      1. 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
      2. 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"
      3. add protected "SecurityGeneratorPromise::resolveNumber($min, $max)" method, that will return result of calling "random_int" function - 0.3h
      4. 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

        Attachments

          Issue Links

            Activity

              People

              • Assignee:
                alex Alex
                Reporter:
                alex Alex
                Developer:
                Alex
                Reviewer:
                Erik Snarski [Intechnic]
              • Votes:
                0 Vote for this issue
                Watchers:
                2 Start watching this issue

                Dates

                • Created:
                  Updated:
                  Resolved:

                  Time Tracking

                  Estimated:
                  Original Estimate - 12h Original Estimate - 12h
                  12h
                  Remaining:
                  Time Spent - 6.7h Remaining Estimate - 5h 20m
                  5h 20m
                  Logged:
                  Time Spent - 6.7h Remaining Estimate - 5h 20m
                  6.7h