Skip to content

Commit

Permalink
Fix yiisoft#4: Add CookieEncryptor and CookieSigner classes
Browse files Browse the repository at this point in the history
  • Loading branch information
devanych authored Mar 10, 2021
1 parent e4b19d0 commit cc6711b
Show file tree
Hide file tree
Showing 7 changed files with 528 additions and 5 deletions.
7 changes: 3 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# Yii Cookies Change Log


## 1.0.1 under development
## 1.1.0 under development

- no changes in this release.
- Add #19: Add the `Yiisoft\Cookies\CookieEncryptor` class to encrypt the value of the cookie and verify that it is tampered (devanych)
- Add #19: Add the `Yiisoft\Cookies\CookieSigner` class to sign the value of the cookie and verify that it is tampered (devanych)

## 1.0.0 December 02, 2020


- Initial release.

44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ The package helps in working with HTTP cookies in a [PSR-7](https://www.php-fig.
- provides a handy abstraction representing a cookie
- allows dealing with many cookies at once
- forms and adds `Set-Cookie` headers to response
- signs a cookie to prevent its value from being tampered with
- encrypts a cookie to prevent its value from being tampered with

## Requirements

Expand Down Expand Up @@ -63,6 +65,48 @@ Getting request cookies:
$cookies = \Yiisoft\Cookies\CookieCollection::fromArray($request->getCookieParams());
```

Signing a cookie to prevent its value from being tampered with:

```php
$cookie = new \Yiisoft\Cookies\Cookie('identity', 'identityValue');

// The secret key used to sign and validate cookies.
$key = '0my1xVkjCJnD_q1yr6lUxcAdpDlTMwiU';
$signer = new \Yiisoft\Cookies\CookieSigner($key);

// Prefixes unique hash based on the value of the cookie and a secret key.
$signedCookie = $signer->sign($cookie);

// Validates and get backs the cookie with clean value.
$cookie = $signer->validate($signedCookie);

// Before validation, check if the cookie is signed.
if ($signer->isSigned($cookie)) {
$cookie = $signer->validate($cookie);
}
```

Encrypting a cookie to prevent its value from being tampered with:

```php
$cookie = new \Yiisoft\Cookies\Cookie('identity', 'identityValue');

// The secret key used to sign and validate cookies.
$key = '0my1xVkjCJnD_q1yr6lUxcAdpDlTMwiU';
$encryptor = new \Yiisoft\Cookies\CookieEncryptor($key);

// Encrypts cookie value based on the secret key.
$encryptedCookie = $encryptor->encrypt($cookie);

// Validates, decrypts and get backs the cookie with clean value.
$cookie = $encryptor->decrypt($encryptedCookie);

// Before decryption, check if the cookie is encrypted.
if ($encryptor->isEncrypted($cookie)) {
$cookie = $encryptor->decrypt($cookie);
}
```

See [Yii guide to cookies](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/cookies.md) for more info.

## Testing
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"php": "^7.4|^8.0",
"psr/http-message": "^1.0",
"psr/http-message-implementation": "1.0",
"yiisoft/http": "^1.0"
"yiisoft/http": "^1.0",
"yiisoft/security": "^1.0"
},
"require-dev": {
"nyholm/psr7": "^1.3",
Expand Down
110 changes: 110 additions & 0 deletions src/CookieEncryptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Cookies;

use RuntimeException;
use Yiisoft\Security\AuthenticationException;
use Yiisoft\Security\Crypt;

use function md5;
use function rawurldecode;
use function rawurlencode;
use function strlen;
use function strpos;
use function substr;

/**
* A CookieEncryptor encrypts the cookie value and validates whether the encrypted cookie value has been tampered with.
*
* @see Cookie
*/
final class CookieEncryptor
{
/**
* @var Crypt The Crypt instance.
*/
private Crypt $crypt;

/**
* @var string The secret key used to encrypt and decrypt cookie values.
*/
private string $key;

/**
* @param string $key The secret key used to encrypt and decrypt cookie values.
*/
public function __construct(string $key)
{
$this->crypt = new Crypt();
$this->key = $key;
}

/**
* Returns a new cookie instance with the encrypted cookie value.
*
* @param Cookie $cookie The cookie with clean value.
*
* @throws RuntimeException If the cookie value is already encrypted.
*
* @return Cookie The cookie with encrypted value.
*/
public function encrypt(Cookie $cookie): Cookie
{
if ($this->isEncrypted($cookie)) {
throw new RuntimeException("The \"{$cookie->getName()}\" cookie value is already encrypted.");
}

$value = $this->crypt->encryptByKey($cookie->getValue(), $this->key, $cookie->getName());
return $cookie->withValue($this->prefix($cookie) . rawurlencode($value));
}

/**
* Returns a new cookie instance with the decrypted cookie value.
*
* @param Cookie $cookie The cookie with encrypted value.
*
* @throws RuntimeException If the cookie value is tampered with or not validly encrypted. If you are not sure
* that the value of the cookie file was encrypted earlier, then first use the {@see isEncrypted()}.
*
* @return Cookie The cookie with decrypted value.
*/
public function decrypt(Cookie $cookie): Cookie
{
if (!$this->isEncrypted($cookie)) {
throw new RuntimeException("The \"{$cookie->getName()}\" cookie value is not validly encrypted.");
}

try {
$value = rawurldecode(substr($cookie->getValue(), 32));
return $cookie->withValue($this->crypt->decryptByKey($value, $this->key, $cookie->getName()));
} catch (AuthenticationException $e) {
throw new RuntimeException("The \"{$cookie->getName()}\" cookie value was tampered with.");
}
}

/**
* Checks whether the cookie value is validly encrypted.
*
* @param Cookie $cookie The cookie to check.
*
* @return bool Whether the cookie value is validly encrypted.
*/
public function isEncrypted(Cookie $cookie): bool
{
return strlen($cookie->getValue()) > 32 && strpos($cookie->getValue(), $this->prefix($cookie)) === 0;
}

/**
* Returns a prefix for cookie.
*
* @param Cookie $cookie The cookie to prefix.
*
* @return string The prefix for cookie.
*/
private function prefix(Cookie $cookie): string
{
return md5(self::class . $cookie->getName());
}
}
109 changes: 109 additions & 0 deletions src/CookieSigner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Cookies;

use RuntimeException;
use Yiisoft\Security\DataIsTamperedException;
use Yiisoft\Security\Mac;

use function md5;
use function strpos;
use function strlen;
use function substr;

/**
* A CookieSigner signs the cookie value and validates whether the signed cookie value has been tampered with.
*
* @see Cookie
*/
final class CookieSigner
{
/**
* @var Mac The Mac instance.
*/
private Mac $mac;

/**
* @var string The secret key used to sign and validate cookie values.
*/
private string $key;

/**
* @param string $key The secret key used to sign and validate cookie values.
*/
public function __construct(string $key)
{
$this->mac = new Mac();
$this->key = $key;
}

/**
* Returns a new cookie instance with the signed cookie value.
*
* @param Cookie $cookie The cookie with clean value.
*
* @throws RuntimeException If the cookie value is already signed.
*
* @return Cookie The cookie with signed value.
*/
public function sign(Cookie $cookie): Cookie
{
if ($this->isSigned($cookie)) {
throw new RuntimeException("The \"{$cookie->getName()}\" cookie value is already signed.");
}

$prefix = $this->prefix($cookie);
$value = $this->mac->sign($prefix . $cookie->getValue(), $this->key);
return $cookie->withValue($prefix . $value);
}

/**
* Returns a new cookie instance with the clean cookie value or throws an exception if signature is not valid.
*
* @param Cookie $cookie The cookie with signed value.
*
* @throws RuntimeException If the cookie value is tampered with or not validly signed.
* If you are not sure that the value of the cookie file was signed earlier, then first use the {@see isSigned()}.
*
* @return Cookie The cookie with unsigned value.
*/
public function validate(Cookie $cookie): Cookie
{
if (!$this->isSigned($cookie)) {
throw new RuntimeException("The \"{$cookie->getName()}\" cookie value is not validly signed.");
}

try {
$value = $this->mac->getMessage(substr($cookie->getValue(), 32), $this->key);
return $cookie->withValue(substr($value, 32));
} catch (DataIsTamperedException $e) {
throw new RuntimeException("The \"{$cookie->getName()}\" cookie value was tampered with.");
}
}

/**
* Checks whether the cookie value is validly signed.
*
* @param Cookie $cookie The cookie to check.
*
* @return bool Whether the cookie value is validly signed.
*/
public function isSigned(Cookie $cookie): bool
{
return strlen($cookie->getValue()) > 32 && strpos($cookie->getValue(), $this->prefix($cookie)) === 0;
}

/**
* Returns a prefix for cookie.
*
* @param Cookie $cookie The cookie to prefix.
*
* @return string The prefix for cookie.
*/
private function prefix(Cookie $cookie): string
{
return md5(self::class . $cookie->getName());
}
}
Loading

0 comments on commit cc6711b

Please sign in to comment.