Skip to content

Conversation

@patel-vansh
Copy link
Contributor

Description
This PR adds a new feature to the encryption service. Currently encryption service only supports single key decryption. This feature adds a feature to enable fallback keys which can be used if data can't be decrypted by the main key.

For more detailed use cases and motivation, please take a look at this forum post.

For equivalent Laravel implementation, please look at this page.

Note:
This PR is not complete yet. I need help in testing as well as user guide update according to this new change. I've done basic logic implementation and hence I am creating this PR to let others have a look at the change and suggest some changes. Testing and User guide updation is still remaining.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

* If you want to enable decryption using previous keys, set them here.
* See the user guide for more info.
*/
public array $previousKeys = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The encryption config can take in values from the .env file and having array properties can be hard to be populated. I suggest this takes a string of comma separated app keys instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that. So, this would be changed to comma separated string, right? So, we will have to do like, convert hex2bin or base64_decode on the fly when needed, or maybe, convert all previous keys and implode them as comma separated keys?

Which approach would be more better?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've currently set the second approach as for now (while initializing in the BaseConfig, do the parsing stuff and then implode as comma-separated string for future use), but we can use the other approach as well.

@patel-vansh
Copy link
Contributor Author

I don't know why this static analysis action is failing. The $result variable is initialized to false, maybe that would be the case.
But I don't think the problem is that its initializd to false, there must be something that I'm missing.

Thanks for help.

Copy link
Member

@michalsn michalsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice start. Some thoughts: I think we should only fall back to the previous keys if the $params do not contain a key. If a key is provided in the $params, it suggests we are dealing with a custom encryption key, and the previous keys are meant to work only with the key from the config file, which would be overridden.

I would explore the option to wrap decrypt() methods content into a callback.

Comment on lines 87 to 114
$result = false;

try {
$result = $this->decryptWithKey($data, $this->key);
sodium_memzero($this->key);
} catch (EncryptionException $e) {
$exception = $e;
sodium_memzero($this->key);
}

if ($result === false && $this->previousKeys !== '') {
foreach (explode(',', $this->previousKeys) as $previousKey) {
try {
$result = $this->decryptWithKey($data, $previousKey);
if (isset($result)) {
return $result;
}
} catch (EncryptionException) {
// Try next key
}
}
}

if (isset($exception)) {
throw $exception;
}

return $result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this hard to read in its current form. Wrapping the logic in a callback method might make it clearer and easier to maintain, especially since the existing method logic won't change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to make it a more readable, please check now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of adding this method to the BaseHandler:

protected function tryDecryptWithFallback($data, #[SensitiveParameter] $params, callable $decryptCallback)
{
   try {
       return $decryptCallback($data, $params);
   } catch (EncryptionException $e) {
       if ($this->previousKeys === []) {
           throw $e;
       }

       if (is_string($params) || (is_array($params) && isset($params['key']))) {
           throw $e;
       }

       foreach ($this->previousKeys as $previousKey) {
           try {
               $previousParams = is_array($params)
                   ? array_merge($params, ['key' => $previousKey])
                   : $previousKey;

               return $decryptCallback($data, $previousParams);
           } catch (EncryptionException) {
               continue;
           }
       }

       throw $e;
   }
}

and then using it like:

public function decrypt($data, #[SensitiveParameter] $params = null)
{
    return $this->tryDecryptWithFallback($data, $params, function ($data, $params): string {
        // original method content
    });
}

From my perspective, this is clearer, as the original methods remain largely unchanged and a single shared method handles both handlers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, adding it in BaseHandler.php was also my first thought, but I thought that anyone that has created their own encryption handler would have to implement this method after update.

So, will it be okay to introduce a new method in base handler?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as long as it's universal and can be used with any handler.

@patel-vansh
Copy link
Contributor Author

One note:
I know that this might cause Copy/Paste Detection action to fail as both handlers essentially have same inner logic for the decrypt method.

Just committed it to get reviews on the working and logic of that function.

I think we can solve this by introducing method inside BaseHandler.php, but again, it would be kind of a breaking change so, we need to think about it.

}

// Only use fallback keys if no custom key was provided in params
$useFallback = ! isset($params['key']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A string value (is_string($params)) should also be treated as a key.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants