Skip to content

[Encryption] New component #39344

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

Closed
wants to merge 14 commits into from
Closed

[Encryption] New component #39344

wants to merge 14 commits into from

Conversation

Nyholm
Copy link
Member

@Nyholm Nyholm commented Dec 5, 2020

Q A
Branch? 6.0
Bug fix? no
New feature? yes
Deprecations? no
Tickets
License MIT
Doc PR

This PR introduces a new Encryption component. But wait, NO. That is not what we are doing.

We are not doing encryption

This component does not do encryption. We do not invent a way to protect your data. This is an encryption abstraction component. So the only thing we do is to define two interfaces and provide a sodium implementation of them.

The interfaces

There is an KeyInterface and a EncryptionInterface. They look something like this:

interface KeyInterface
{
    public function extractPublicKey(): self;
}
interface EncryptionInterface
{
    public function generateKey(string $secret = null): KeyInterface;
    public function encrypt(string $message, KeyInterface $myKey): string;
    public function encryptFor(string $message, KeyInterface $recipientKey): string;
    public function decrypt(string $message, KeyInterface $key, KeyInterface $senderPublicKey = null): string;
}

There are some documentation on the interfaces, they can be good to read if one is not sure what the methods are doing.

Keys

The idea behind these interfaces is that you create a Key. That key can be used for both symmetric and asymmetric encryption. That key can be serialized and stored on disk or in database. You can extract the public part of your Key.

You as a user need to know what type of key to have. You cannot load a random key from disk and use it with encryptFor(). You need to know that it was Alice's private key. Ie, Key management is out of scope of the component.

Keys cannot be transfered to a different implementation. (Just like PSR-6). So you cannot use SodiumEncryption to create a Key and then use it with PhpseclibEncryption.

Encryption

The encryption interface provides an explicit way how to handle encryption. There are 3 scenarios:

  • encrypt(): When you don't share the encrypt/decrypt keys
  • encryptFor(): When you want to send a message to Bob

For someone who worked with encryption they know that this is no rocket science. But for someone who takes their first step into encrypting in PHP, they probably look at PHP's Sodium "documentation" and have no idea where to start. This EncryptionInterface will be super helpful for them.

Portability

( This section is where I accept all kinds of "you are doing it wrong" 😄 )

The output of all EncryptionInterface::encrypt*() methods is a string. That string has a specific format.
The format is implemented in the Ciphertext class. Here is how the final string look like:

{"alg": "sodium"} . encryption-output . hash

With some psudo-code:

$nonce = random_bytes(16);
$ciphertext = sodium_encrypt('my message', $nonce);
$headers = json_encode(['alg'=>'sodium_encrypt', 'ver'=>'1', 'nonce' => $nonce]);
$hash = hash('sha256', $headers.$ciphertext);

$output = sprintf('%s.%s.%s',
           base64_encode(headers),
           base64_encode($ciphertext),
           base64_encode($hash)
        );

But why do we need this format?

We do need a format. We need to store some meta data, the ciphertext and the nonce somewhere. To concatenate these together seams like the simplest solution.

The alg header is basically a routing key to tell the difference between “Algorithm not supported” and “Wrong key was used to decrypt message”. Also to make the keys somewhat portable. Ie, it is possible for a non-encryption-component-application to decrypt the key. Note that there is no algorithm negotiation. Note also that the hash here is not to make things "more secure", it is just for an integrity check.

This format will also fall under or BC policy.

I have looked at using JWE, but I was not able to follow the specification to 100% to create the Authentication Tag (Im not sure it is possible in PHP...) and I've also seen posts about not to use JWE. JWE also made things much more complex.

The name of the component

I am not a big fan of the name of this component. What I've noticed that just because it is called "Encryption", then people think that we are doing advanced security stuff. This component will never compete with Sodium, Phpseclib or Halite. It will only make a very thin abstraction on top of it. This abstraction will work for 80% of your encryption needs. For the other 20% you need more flexibility or customisation. That is where you use Sodium, Phpseclib, Halite etc directly.

Ie, this component is not a good fit for "That other team's Java application sends us encrypted data, can we decrypt it?".

@Nyholm Nyholm added the Security label Dec 5, 2020
@carsonbot carsonbot changed the title Adding symmetric and asymmetric encryption [Security] Adding symmetric and asymmetric encryption Dec 5, 2020
Copy link
Member

@fabpot fabpot left a comment

Choose a reason for hiding this comment

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

Looks georgous and simple! Thank you for doing it. Sorry for the not so useful comments, but I've focused on the text instead of the code for this first review.

Two questions that come to mind:

Would it make sense to have a dedicated component instead of having it in security? I can see many usages for which I would not need anything else from the Security components.

We are using "encryption" in the secret feature (part of framework bundle) and in the Mime component. Any code that could be moved to this new hypothetical encryption component?

@Nyholm
Copy link
Member Author

Nyholm commented Dec 6, 2020

Thank you for the review. I've updated the PR accordingly.

Would it make sense to have a dedicated component instead of having it in security?

Yes, it will be a small component, but I do think so.

We are using "encryption" in the secret feature (part of framework bundle) and in the Mime component. Any code that could be moved to this new hypothetical encryption component?

Looking at the SodiumVault: Yes, we can use these abstractions. They authors have used the sodium keypair as "decryption key" which I fail to understand why, But we can discuss that in a separate issue.

In the MimeComponent we have a DkimSigner, SMimeEncryoter and SMimeSigner, these cannot be leveraged by the SymmetricEncryptionInterface or AsymmetricEncryptionInterface.

The current implementation is an abstractionSymmetricEncryptionInterface or AsymmetricEncryptionInterface, you are not able to chose algorithm. The output of encrypt() is following the pattern of cipher.algorithm.nonce, so it is possible for a non-php application to decrypt it, but they need to know our arbitrary values of algorithm.

@fabpot
Copy link
Member

fabpot commented Dec 6, 2020

There is no such thing as a "small component" :) History showed us that most small components grew over time with new needs are discovered.
So, I'm 👍 for a new component.

@wouterj
Copy link
Member

wouterj commented Dec 6, 2020

👍 for a new component (I would even prefer a component outside the Security namespace) and moving its framework integration to the FrameworkBundle.

There already are a couple features in the framework bundle that can benefit from this new component I think, e.g. encrypting cache values (refs #35019) and encrypting session data (refs #35804).

@jderusse jderusse added this to the 5.x milestone Dec 6, 2020
@Nyholm Nyholm removed the Security label Dec 6, 2020
@Nyholm Nyholm changed the title [Security] Adding symmetric and asymmetric encryption [Encryption] Adding symmetric and asymmetric encryption Dec 6, 2020
@Nyholm
Copy link
Member Author

Nyholm commented Dec 7, 2020

I need some more work to support JWE. Using JWE has the benefit that we more easily can share the encrypted strings with other applications.

JWE exists of 5 parts:

header: a json object defining what algorithms we use
CEK: The encoded key for the symmetric encryption
Initialization Vector: Nonce for symmetric encryption
Cipher text: The encrypted payload
Authentication tag: used for integrity checks

So we use symmetric encryption for the payload and asymmetric to encrypt the symmetric key.

@Nyholm
Copy link
Member Author

Nyholm commented Dec 7, 2020

So, we are compatible with the JWE format.. almost. I cannot figure out the JWE Authentication Tag.

Here is an example for RFC7516:

Perform authenticated encryption on the plaintext with the AES GCM algorithm using the CEK as the encryption key, the JWE Initialization Vector, and the Additional Authenticated Data value above, requesting a 128-bit Authentication Tag output.

So basically: "Take a bunch of data, encrypt it with AES GCM and from that encryption you get ciphertext X and Authentication Tag Y".

But when I look at ie sodium or libsecphp, I find no encryption algoritm where I can get two outputs.. Am I missing something?

However, We are following the same protocol and the ciphertext is secure, but it is not according to the spec (not so good for portability)

@Nyholm
Copy link
Member Author

Nyholm commented Dec 8, 2020

I liked JWE because it mean that our payload could be portable. However, JWE introduced a lot of complexity and I didn't manage to add a Authentication Tag according to the spec. So.. just complexity and no probability. I changed back to a more simple implementation.

@Nyholm
Copy link
Member Author

Nyholm commented Dec 8, 2020

Im trying out something new. Im making the API more explicit by introducing a KeyInterface. See 12804bb

@sstok
Copy link
Contributor

sstok commented Dec 9, 2020

Sorry for this harsh comment, but it's noting personal: DON'T EVER ROLL YOUR OWN ENCRYPTION!

I'm no crypto expert but I already see some things that are potentially dangerous.

Why do you think Sodium was created? 😄 Cryptographic is a complex topic and with my limited knowledge I can already spot some things I'm not entirely sure of, but I know have caused major problems in the past. If you have sodium, use it. PHP has it bundled already, and it is the most secure standard of today. RSA and DSA should only used for legacy systems or by experts who know how the handle these complexities.

ping @paragonie-scott

@jvasseur
Copy link
Contributor

jvasseur commented Dec 9, 2020

Sorry for this harsh comment, but it's noting personal: DON'T EVER ROLL YOUR OWN ENCRYPTION!

We are not creating our own encryption here but just creating a wrapper that relies on other lower level encryption libraries (including Sodium).

….php

Co-authored-by: Robin Chalas <chalasr@users.noreply.github.com>
return $key;
}

public static function fromPrivateAndPublicKeys(string $privateKey, string $publicKey): self
Copy link
Member

Choose a reason for hiding this comment

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

not used and class is internal

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe these method will be very useful when we use the encryption component for the "symfony secrets".

Copy link
Member

Choose a reason for hiding this comment

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

I have the feeling that having some usages of this new component in core would help moving forward here.
@Nyholm Maybe could you open a PR doing so based on this PR? Or commit here directly?

return $key;
}

public static function fromKeypair(string $keypair): self
Copy link
Member

Choose a reason for hiding this comment

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

same

Copy link

@paragonie-security paragonie-security left a comment

Choose a reason for hiding this comment

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

The current implementation needs work before it can be a general-purpose encryption component for all Symfony users.

$key->secret = $secret;
} else {
// Trim the key to a good size
$key->secret = substr(sha1($secret), 0, \SODIUM_CRYPTO_SECRETBOX_KEYBYTES);

Choose a reason for hiding this comment

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

First, sha1() is not an appropriate function to use here.

But even worse, this is hex-encoded so you're getting roughly half of the expected entropy out of your secret from this sha1 invocation.

- $key->secret = substr(sha1($secret), 0, \SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
+ $key->secret = mb_substr(hash('sha256', $secret, true), 0, \SODIUM_CRYPTO_SECRETBOX_KEYBYTES, '8bit');


// Check if data has been modified
$hash = hash('sha256', $headersString.$ciphertext);
if (!hash_equals($hash, $hashSignature)) {

Choose a reason for hiding this comment

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

Misnomer: $hashSignature isn't, at all, a hash signature.

If you were doing the right thing, it would be a MAC, not a signature, but this just a SHA256 hash without any secrets. This is trivially bypassed: Just calculate hash('sha256', $headersString.$ciphertext) for any arbitrary data you want to feed in.

You're probably better off calling this a checksum, or equivalent, so as to not over-sell the security properties you're shooting for.

Copy link

Choose a reason for hiding this comment

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

This setup also looks susceptible to canonicalization attacks. If the intent here is to have something like HMAC, you'll want to also revisit this.

@fabpot
Copy link
Member

fabpot commented Dec 11, 2021

@Nyholm What do we do here?

@Nyholm
Copy link
Member Author

Nyholm commented Dec 11, 2021

Im gathering courage to make this mergable. I will not stall more than another month.

@OskarStark
Copy link
Contributor

Must target 6.1 then, right?

throw new DecryptionException(sprintf('Failed to decrypt message with algorithm "%s".', $algorithm), $exception);
}

if (false === $output) {
Copy link

Choose a reason for hiding this comment

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

!is_string() would be a better condition for static analysis.

@ro0NL
Copy link
Contributor

ro0NL commented Dec 21, 2021

s/encryption/encryptor/ everywhere? 😅

@fabpot
Copy link
Member

fabpot commented Jul 21, 2022

Let's close for now

@fabpot fabpot closed this Jul 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.