Skip to content

Instantly share code, notes, and snippets.

@gnat42
Last active June 20, 2017 18:36
Show Gist options
  • Save gnat42/8104a7f23fc08d4ed9d59609aeea70f2 to your computer and use it in GitHub Desktop.
Save gnat42/8104a7f23fc08d4ed9d59609aeea70f2 to your computer and use it in GitHub Desktop.
<?php
/**
* Created by PhpStorm.
* User: gnat
* Date: 2017-06-19
* Time: 11:18 PM
*/
namespace NS\CoreDomain\Practice\Credentials;
use NS\CoreDomain\Practice\User;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
class CredentialEncoder
{
/**
* Symfony provided service
* Given a user instance, it returns an encoder that will transform their password into a hash
* Currently we've configured pbkdf2, with algo: sha256 and iterations: 1000
*
* @var EncoderFactoryInterface
*/
private $encoderFactory;
/**
* Currently we use aes-256-cbc
* @var string
*/
private $method;
/**
* System encryption key ( initially generated via base64_encode(random_bytes(128)) )
* @var string
*/
private $key;
/**
* CredentialEncoder constructor.
* @param EncoderFactoryInterface $encoderFactory
* @param string $method
* @param string $key
*/
public function __construct(EncoderFactoryInterface $encoderFactory, $method, $key)
{
$this->encoderFactory = $encoderFactory;
$this->method = $method;
$this->key = $key;
}
/**
* @param User $user
* @param CredentialStore $credentialStore
* @param $password
* @return string
*/
public function encodeUserCredentials(User $user, CredentialStore $credentialStore, $password)
{
$encoder = $this->encoderFactory->getEncoder($user);
// Resets the User's stored salt via bin2hex(random_bytes(8)); The salt is used as the IV for openssl
$user->resetSalt();
// generates key based off of pbkdf2 hash of provided password
$key = $encoder->encodePassword($password, $user->getSalt());
return openssl_encrypt(serialize($credentialStore), $this->method, $key, false, $user->getSalt());
}
/**
* @param User $user
* @param $currentPassword
* @param $newPassword
* @return string
*/
public function reEncodeUserCredential(User $user, $currentPassword, $newPassword)
{
$decoded = $this->decodeUserCredentials($user, $currentPassword);
return $this->encodeUserCredentials($user, $decoded, $newPassword);
}
/**
* @param User $user
* @param $password
* @return CredentialStore|false
*/
public function decodeUserCredentials(User $user, $password)
{
$encoder = $this->encoderFactory->getEncoder($user);
$key = $encoder->encodePassword($password, $user->getSalt());
$data = openssl_decrypt($user->getCredentialStore(), $this->method, $key, false, $user->getSalt());
if ($data === false) {
throw new \RuntimeException("Unable to decode via password");
}
return unserialize($data);
}
/**
* Given unencrypted user credential data, encrypt using system key.
* This allows us access to the credential data we're storing on behalf of the user
* in background processes initiated by the user on the front end.
*
* TODO: Investigate changing this to a libsodium create_box where we encrypt it for
* the backend system specifically. Only backend servers could decrypt it then. Thus
* requiring two systems to be compromised. This of course is a moot point if someone gains
* root on this system as they could modify this code and output the decrypted credentials.
* Should these be stored using something like Vault instead??
*
* @param $credential
* @return string
*/
public function encodeSystemCredentials($credential)
{
$iv = random_bytes(16);
return base64_encode("$iv::" . openssl_encrypt(serialize($credential), $this->method, $this->key, false, $iv));
}
/**
* @param string $encryptedStr
* @return mixed
*/
public function decodeSystemCredentials(string $encryptedStr)
{
if (strpos($encryptedStr, '::') === false) {
$encryptedStr = base64_decode($encryptedStr);
if (strpos($encryptedStr, '::') === false) {
throw new \RuntimeException('Unable to decrypt');
}
}
$data = explode('::', $encryptedStr);
return unserialize(openssl_decrypt($data[1], $this->method, $this->key, false, $data[0]));
}
}
<?php
/**
* Created by PhpStorm.
* User: gnat
* Date: 19/06/17
* Time: 1:48 PM
*/
namespace NS\SiteBundle\Authentication;
use NS\CoreDomain\Practice\Credentials\CredentialEncoder;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
/** @var CredentialEncoder */
private $credentialEncoder;
/** @var string */
private $loginUrl;
/** @var string */
private $successUrl;
/**
* FormLoginAuthenticator constructor.
* @param CredentialEncoder $credentialEncoder
* @param string $loginUrl
* @param string $successUrl
*/
public function __construct(CredentialEncoder $credentialEncoder, string $loginUrl, string $successUrl)
{
$this->credentialEncoder = $credentialEncoder;
$this->loginUrl = $loginUrl;
$this->successUrl = $successUrl;
}
/**
* @inheritDoc
*/
protected function getLoginUrl()
{
return $this->loginUrl;
}
/**
* @inheritDoc
*/
public function getCredentials(Request $request)
{
if ($request->request->has('_username')) {
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
return null;
}
/**
* @inheritDoc
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = $userProvider->loadUserByUsername($credentials['username']);
return $user;
}
/**
* @var string|null
*/
private $credentials = false;
/**
* @inheritDoc
*/
public function checkCredentials($credentials, UserInterface $user)
{
$this->credentials = $this->credentialEncoder->decodeUserCredentials($user, $credentials['password']);
// If the user's password was correct, take the decrypted data and re-encrypt using system key
// For use in background non-session based access
if ($this->credentials !== false) {
$this->credentials = $this->credentialEncoder->encodeSystemCredentials($this->credentials);
return true;
}
return false;
}
/**
* @inheritDoc
*/
public function createAuthenticatedToken(UserInterface $user, $providerKey)
{
$token = new EncryptedStoreToken($user, $providerKey, $user->getRoles());
// System key encrypted data
$token->setCredentials($this->credentials);
$this->credentials = false;
return $token;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$targetPath = null;
// if the user hit a secure page and start() was called, this was
// the URL they were on, and probably where you want to redirect to
if ($request->getSession() instanceof SessionInterface) {
$targetPath = $this->getTargetPath($request->getSession(), $providerKey);
}
if (!$targetPath) {
$targetPath = $this->successUrl;
}
return new RedirectResponse($targetPath);
}
}
@gnat42
Copy link
Author

gnat42 commented Jun 20, 2017

We have a situation where we are interacting with API's that don't use OAuth, but expect username/passwords. They were designed to be interacted via desktop applications not web applications. Since most of our interactions with these services will be handled by background processes without access to the session data. We need to

  1. Store these plaintext usernames + passwords securely.
  2. Have a way of maintaining some level of security but access them from the system level once a user has logged in.

The pseudo implementation is as follows:

  1. I'm using PBKDF2 to hash the user password into the key used to decrypt the 'blob' of serialized credential data. The password hash is never stored.
  2. The user object maintains a salt that is changed whenever the password is changed. This is fed into the IV parameter of openssl_encrypt/decrypt. From what I've read this is similar to 'regular' salt used for simpler password hashing+salt and can be safely stored and transmitted with the encrypted data. It mainly ensures more entropy in the encrypted output?
  3. Once the user is authenticated by virtue of their password successfully decrypting their serialized credential data, we re-encrypt with AES-256-CBC using a system key so that this can be stored in the user's session (not cookie) but server side data. That way we can pass that around to background processes.

Thoughts? Weaknesses? Anything you think I'm doing wrong or the cipher+mode issues I should know about?

I have considered changing my re-encode function to use lib sodium so that its encrypted such that only the backend system can read the data. Not sure how much more security that adds and what type of attack it would protect me from.

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