Skip to content

Commit 40c1e19

Browse files
committed
:octocat: move some common methods to Utilities
1 parent 7f16768 commit 40c1e19

File tree

8 files changed

+255
-114
lines changed

8 files changed

+255
-114
lines changed

examples/create-description.php

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
use chillerlan\OAuth\Core\{
1111
ClientCredentials, CSRFToken, OAuth1Interface, OAuth2Interface,
12-
OAuthInterface, PKCE, TokenInvalidate, TokenRefresh, UserInfo
12+
PKCE, TokenInvalidate, TokenRefresh, UserInfo, Utilities
1313
};
1414

1515
/**
@@ -31,7 +31,7 @@
3131
'|----------|------|--------|-----|------|------|------|----|----|----|',
3232
];
3333

34-
foreach(getProviders(__DIR__.'/../src/Providers') as $p){
34+
foreach(Utilities::getProviders() as $p){
3535
/** @var \OAuthExampleProviderFactory $factory */
3636
$provider = $factory->getProvider($p['fqcn'], OAuthExampleProviderFactory::STORAGE_MEMORY);
3737

@@ -85,34 +85,3 @@
8585
}
8686

8787
exit;
88-
89-
function getProviders(string $providerDir):array{
90-
$providerDir = realpath($providerDir);
91-
$providers = [];
92-
93-
/** @var \SplFileInfo $e */
94-
foreach(new IteratorIterator(new DirectoryIterator($providerDir)) as $e){
95-
96-
if($e->getExtension() !== 'php'){
97-
continue;
98-
}
99-
100-
$class = 'chillerlan\\OAuth\\Providers\\'.substr($e->getFilename(), 0, -4);
101-
102-
try{
103-
$r = new ReflectionClass($class);
104-
105-
if(!$r->implementsInterface(OAuthInterface::class) || $r->isAbstract()){
106-
continue;
107-
}
108-
109-
$providers[hash('crc32b', $r->getShortName())] = ['name' => $r->getShortName(), 'fqcn' => $class];
110-
}
111-
catch(Throwable){
112-
continue;
113-
}
114-
115-
}
116-
117-
return $providers;
118-
}

src/Core/Utilities.php

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
/**
3+
* Class Utilities
4+
*
5+
* @created 10.04.2024
6+
* @author smiley <smiley@chillerlan.net>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace chillerlan\OAuth\Core;
13+
14+
use DirectoryIterator;
15+
use InvalidArgumentException;
16+
use ReflectionClass;
17+
use RuntimeException;
18+
use function hash;
19+
use function random_bytes;
20+
use function realpath;
21+
use function sodium_base642bin;
22+
use function sodium_bin2base64;
23+
use function sodium_bin2hex;
24+
use function sodium_crypto_secretbox;
25+
use function sodium_crypto_secretbox_keygen;
26+
use function sodium_crypto_secretbox_open;
27+
use function sodium_hex2bin;
28+
use function sodium_memzero;
29+
use function substr;
30+
use function trim;
31+
use const SODIUM_BASE64_VARIANT_ORIGINAL;
32+
use const SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
33+
34+
/**
35+
* Common utilities for use with the OAuth providers
36+
*/
37+
class Utilities{
38+
39+
final public const ENCRYPT_FORMAT_BINARY = 0b00;
40+
final public const ENCRYPT_FORMAT_BASE64 = 0b01;
41+
final public const ENCRYPT_FORMAT_HEX = 0b10;
42+
43+
/**
44+
* Fetches a list of provider classes in the given directory
45+
*/
46+
public static function getProviders(string|null $providerDir = null, string|null $namespace = null):array{
47+
$providerDir = realpath($providerDir ?? __DIR__.'/../Providers');
48+
$namespace = trim(($namespace ?? 'chillerlan\\OAuth\\Providers'), '\\');
49+
$providers = [];
50+
51+
if($providerDir === false){
52+
throw new InvalidArgumentException('invalid $providerDir');
53+
}
54+
55+
/** @var \SplFileInfo $e */
56+
foreach(new DirectoryIterator($providerDir) as $e){
57+
58+
if($e->getExtension() !== 'php'){
59+
continue;
60+
}
61+
62+
$r = new ReflectionClass($namespace.'\\'.substr($e->getFilename(), 0, -4));
63+
64+
if(!$r->implementsInterface(OAuthInterface::class) || $r->isAbstract()){
65+
continue;
66+
}
67+
68+
$providers[hash('crc32b', $r->getShortName())] = [
69+
'name' => $r->getShortName(),
70+
'fqcn' => $r->getName(),
71+
'path' => $e->getRealPath(),
72+
];
73+
74+
}
75+
76+
return $providers;
77+
}
78+
79+
/**
80+
* Creates a new cryptographically secure random encryption key (in hexadecimal or format)
81+
*/
82+
public static function createEncryptionKey():string{
83+
return sodium_bin2hex(sodium_crypto_secretbox_keygen());
84+
}
85+
86+
/**
87+
* encrypts the given $data with $key, $format output [binary, base64, hex]
88+
*
89+
* @see \sodium_crypto_secretbox()
90+
* @see \sodium_bin2base64()
91+
* @see \sodium_bin2hex()
92+
*/
93+
public static function encrypt(string $data, string $keyHex, int $format = self::ENCRYPT_FORMAT_HEX):string{
94+
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
95+
$box = sodium_crypto_secretbox($data, $nonce, sodium_hex2bin($keyHex));
96+
97+
$out = match($format){
98+
self::ENCRYPT_FORMAT_BINARY => $nonce.$box,
99+
self::ENCRYPT_FORMAT_BASE64 => sodium_bin2base64($nonce.$box, SODIUM_BASE64_VARIANT_ORIGINAL),
100+
self::ENCRYPT_FORMAT_HEX => sodium_bin2hex($nonce.$box),
101+
};
102+
103+
sodium_memzero($data);
104+
sodium_memzero($keyHex);
105+
sodium_memzero($nonce);
106+
sodium_memzero($box);
107+
108+
return $out;
109+
}
110+
111+
/**
112+
* decrypts the given $encrypted data with $key from $format input [binary, base64, hex]
113+
*
114+
* @see \sodium_crypto_secretbox_open()
115+
* @see \sodium_base642bin()
116+
* @see \sodium_hex2bin()
117+
*/
118+
public static function decrypt(string $encrypted, string $keyHex, int $format = self::ENCRYPT_FORMAT_HEX):string{
119+
120+
$bin = match($format){
121+
self::ENCRYPT_FORMAT_BINARY => $encrypted,
122+
self::ENCRYPT_FORMAT_BASE64 => sodium_base642bin($encrypted, SODIUM_BASE64_VARIANT_ORIGINAL),
123+
self::ENCRYPT_FORMAT_HEX => sodium_hex2bin($encrypted),
124+
};
125+
126+
$nonce = substr($bin, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
127+
$box = substr($bin, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
128+
$data = sodium_crypto_secretbox_open($box, $nonce, sodium_hex2bin($keyHex));
129+
130+
sodium_memzero($encrypted);
131+
sodium_memzero($keyHex);
132+
sodium_memzero($bin);
133+
sodium_memzero($nonce);
134+
sodium_memzero($box);
135+
136+
if($data === false){
137+
throw new RuntimeException('decryption failed'); // @codeCoverageIgnore
138+
}
139+
140+
return $data;
141+
}
142+
143+
}

src/OAuthOptionsTrait.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@
1212
namespace chillerlan\OAuth;
1313

1414
use chillerlan\OAuth\Storage\OAuthStorageException;
15-
use function is_dir, is_writable, max, min, realpath, sprintf, strlen, trim;
16-
use const SODIUM_CRYPTO_SECRETBOX_KEYBYTES;
15+
use function is_dir, is_writable, max, min, preg_match, realpath, sprintf, strtolower, trim;
1716

1817
/**
1918
* The settings for the OAuth provider
@@ -43,7 +42,7 @@ trait OAuthOptionsTrait{
4342
protected bool $useStorageEncryption = false;
4443

4544
/**
46-
* The encryption key to use
45+
* The encryption key (hexadecimal) to use
4746
*
4847
* @see \sodium_crypto_secretbox_keygen()
4948
* @see \chillerlan\OAuth\Storage\FileStorage
@@ -103,8 +102,9 @@ trait OAuthOptionsTrait{
103102
* sets an encryption key
104103
*/
105104
protected function set_storageEncryptionKey(string $storageEncryptionKey):void{
105+
$storageEncryptionKey = strtolower($storageEncryptionKey);
106106

107-
if(strlen($storageEncryptionKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES){
107+
if(!preg_match('/^[a-f0-9]{64}$/', $storageEncryptionKey)){
108108
throw new OAuthStorageException('invalid encryption key');
109109
}
110110

src/Storage/FileStorage.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111

1212
namespace chillerlan\OAuth\Storage;
1313

14-
use chillerlan\OAuth\Core\AccessToken;
1514
use chillerlan\OAuth\OAuthOptions;
15+
use chillerlan\OAuth\Core\{AccessToken, Utilities};
1616
use chillerlan\Settings\SettingsContainerInterface;
17-
use DirectoryIterator;
1817
use Psr\Log\{LoggerInterface, NullLogger};
18+
use DirectoryIterator;
1919
use function dirname, file_exists, file_get_contents, file_put_contents, hash, implode,
2020
is_dir, is_file, mkdir, sprintf, str_starts_with, substr, trim, unlink;
2121
use const DIRECTORY_SEPARATOR;
@@ -31,7 +31,7 @@
3131
*/
3232
class FileStorage extends OAuthStorageAbstract{
3333

34-
final protected const ENCRYPT_FORMAT = self::ENCRYPT_FORMAT_BINARY;
34+
final protected const ENCRYPT_FORMAT = Utilities::ENCRYPT_FORMAT_BINARY;
3535

3636
/**
3737
* OAuthStorageAbstract constructor.
@@ -114,7 +114,7 @@ public function clearAllAccessTokens():static{
114114
public function storeCSRFState(string $state, string $provider):static{
115115

116116
if($this->options->useStorageEncryption === true){
117-
$state = $this->encrypt($state, $this->options->storageEncryptionKey);
117+
$state = $this->encrypt($state);
118118
}
119119

120120
$this->saveFile($state, $this::KEY_STATE, $provider);
@@ -133,7 +133,7 @@ public function getCSRFState(string $provider):string{
133133
}
134134

135135
if($this->options->useStorageEncryption === true){
136-
return $this->decrypt($state, $this->options->storageEncryptionKey);
136+
return $this->decrypt($state);
137137
}
138138

139139
return $state;
@@ -170,7 +170,7 @@ public function clearAllCSRFStates():static{
170170
public function storeCodeVerifier(string $verifier, string $provider):static{
171171

172172
if($this->options->useStorageEncryption === true){
173-
$verifier = $this->encrypt($verifier, $this->options->storageEncryptionKey);
173+
$verifier = $this->encrypt($verifier);
174174
}
175175

176176
$this->saveFile($verifier, $this::KEY_VERIFIER, $provider);
@@ -189,7 +189,7 @@ public function getCodeVerifier(string $provider):string{
189189
}
190190

191191
if($this->options->useStorageEncryption === true){
192-
return $this->decrypt($verifier, $this->options->storageEncryptionKey);
192+
return $this->decrypt($verifier);
193193
}
194194

195195
return $verifier;

src/Storage/OAuthStorageAbstract.php

Lines changed: 11 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@
1212
namespace chillerlan\OAuth\Storage;
1313

1414
use chillerlan\OAuth\OAuthOptions;
15-
use chillerlan\OAuth\Core\AccessToken;
15+
use chillerlan\OAuth\Core\{AccessToken, Utilities};
1616
use chillerlan\Settings\SettingsContainerInterface;
1717
use Psr\Log\{LoggerInterface, NullLogger};
18-
use function random_bytes, sodium_base642bin, sodium_bin2base64, sodium_bin2hex, sodium_crypto_secretbox,
19-
sodium_crypto_secretbox_open, sodium_hex2bin, sodium_memzero, substr, trim;
20-
use const SODIUM_BASE64_VARIANT_ORIGINAL, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
18+
use function trim;
2119

2220
/**
2321
* Implements an abstract OAuth storage adapter
@@ -28,11 +26,7 @@ abstract class OAuthStorageAbstract implements OAuthStorageInterface{
2826
final protected const KEY_STATE = 'STATE';
2927
final protected const KEY_VERIFIER = 'VERIFIER';
3028

31-
final protected const ENCRYPT_FORMAT_BINARY = 0b00;
32-
final protected const ENCRYPT_FORMAT_BASE64 = 0b01;
33-
final protected const ENCRYPT_FORMAT_HEX = 0b10;
34-
35-
protected const ENCRYPT_FORMAT = self::ENCRYPT_FORMAT_HEX;
29+
protected const ENCRYPT_FORMAT = Utilities::ENCRYPT_FORMAT_HEX;
3630

3731
/**
3832
* OAuthStorageAbstract constructor.
@@ -89,7 +83,7 @@ public function toStorage(AccessToken $token):mixed{
8983
$tokenJSON = $token->toJSON();
9084

9185
if($this->options->useStorageEncryption === true){
92-
return $this->encrypt($tokenJSON, $this->options->storageEncryptionKey);
86+
return $this->encrypt($tokenJSON);
9387
}
9488

9589
return $tokenJSON;
@@ -101,67 +95,24 @@ public function toStorage(AccessToken $token):mixed{
10195
public function fromStorage(mixed $data):AccessToken{
10296

10397
if($this->options->useStorageEncryption === true){
104-
$data = $this->decrypt($data, $this->options->storageEncryptionKey);
98+
$data = $this->decrypt($data);
10599
}
106100

107101
return (new AccessToken)->fromJSON($data);
108102
}
109103

110104
/**
111-
* encrypts the given $data with $key
112-
*
113-
* @see \sodium_crypto_secretbox()
114-
* @see \sodium_bin2base64()
115-
* @see \sodium_bin2hex()
105+
* encrypts the given $data
116106
*/
117-
protected function encrypt(string $data, string $key):string{
118-
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
119-
$box = sodium_crypto_secretbox($data, $nonce, $key);
120-
121-
$out = match($this::ENCRYPT_FORMAT){
122-
$this::ENCRYPT_FORMAT_BINARY => $nonce.$box,
123-
$this::ENCRYPT_FORMAT_BASE64 => sodium_bin2base64($nonce.$box, SODIUM_BASE64_VARIANT_ORIGINAL),
124-
$this::ENCRYPT_FORMAT_HEX => sodium_bin2hex($nonce.$box),
125-
};
126-
127-
sodium_memzero($data);
128-
sodium_memzero($key);
129-
sodium_memzero($nonce);
130-
sodium_memzero($box);
131-
132-
return $out;
107+
protected function encrypt(string $data):string{
108+
return Utilities::encrypt($data, $this->options->storageEncryptionKey, $this::ENCRYPT_FORMAT);
133109
}
134110

135111
/**
136-
* decrypts the given $encrypted data with $key
137-
*
138-
* @see \sodium_crypto_secretbox_open()
139-
* @see \sodium_base642bin()
140-
* @see \sodium_hex2bin()
112+
* decrypts the given $encrypted data
141113
*/
142-
protected function decrypt(string $encrypted, string $key):string{
143-
144-
$bin = match($this::ENCRYPT_FORMAT){
145-
$this::ENCRYPT_FORMAT_BINARY => $encrypted,
146-
$this::ENCRYPT_FORMAT_BASE64 => sodium_base642bin($encrypted, SODIUM_BASE64_VARIANT_ORIGINAL),
147-
$this::ENCRYPT_FORMAT_HEX => sodium_hex2bin($encrypted),
148-
};
149-
150-
$nonce = substr($bin, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
151-
$box = substr($bin, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
152-
$data = sodium_crypto_secretbox_open($box, $nonce, $key);
153-
154-
sodium_memzero($encrypted);
155-
sodium_memzero($key);
156-
sodium_memzero($bin);
157-
sodium_memzero($nonce);
158-
sodium_memzero($box);
159-
160-
if($data === false){
161-
throw new OAuthStorageException('decryption failed'); // @codeCoverageIgnore
162-
}
163-
164-
return $data;
114+
protected function decrypt(string $encrypted):string{
115+
return Utilities::decrypt($encrypted, $this->options->storageEncryptionKey, $this::ENCRYPT_FORMAT);
165116
}
166117

167118
}

0 commit comments

Comments
 (0)