diff --git a/README.md b/README.md index d0e64fe..02fa4db 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Here is a simple example of how to send a message using the client: require 'vendor/autoload.php'; use AndroidSmsGateway\Client; +use AndroidSmsGateway\Encryptor; use AndroidSmsGateway\EncryptedClient; use AndroidSmsGateway\Domain\Message; @@ -34,7 +35,8 @@ $password = 'your_password'; $client = new Client($login, $password); // or -// $client = new EncryptedClient('your_passphrase', $login, $password); +// $encryptor = new Encryptor('your_passphrase'); +// $client = new EncryptedClient($login, $password, Client::DEFAULT_URL, $httpClient, $encryptor); $message = new Message('Your message text here.', ['+1234567890']); @@ -59,12 +61,11 @@ try { There are two clients available: -- `Client` is used for sending SMS messages in plain text. -- `EncryptedClient` is used for sending AES encrypted SMS messages. You need to provide the same passphrase in Android app. +- `Client` is used for sending SMS messages in plain text, but can also be used for sending encrypted messages by providing an `Encryptor`. ### Methods -Each client has the following methods: +Client has the following methods: * `Send(Message $message)`: Send a new SMS message. * `GetState(string $id)`: Retrieve the state of a previously sent message by its ID. diff --git a/src/Client.php b/src/Client.php index 2c511fa..6bf14a2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -19,6 +19,7 @@ class Client { protected string $baseUrl; protected HttpClient $client; + protected ?Encryptor $encryptor; protected RequestFactoryInterface $requestFactory; protected StreamFactoryInterface $streamFactory; @@ -27,11 +28,13 @@ public function __construct( string $login, string $password, string $serverUrl = self::DEFAULT_URL, - ?HttpClient $client = null + ?HttpClient $client = null, + ?Encryptor $encryptor = null ) { $this->basicAuth = base64_encode($login . ':' . $password); $this->baseUrl = $serverUrl; $this->client = $client ?? HttpClientDiscovery::find(); + $this->encryptor = $encryptor; $this->requestFactory = Psr17FactoryDiscovery::findRequestFactory(); $this->streamFactory = Psr17FactoryDiscovery::findStreamFactory(); @@ -40,6 +43,10 @@ public function __construct( public function Send(Message $message): MessageState { $path = '/message'; + if (isset($this->encryptor)) { + $message = $message->Encrypt($this->encryptor); + } + $response = $this->sendRequest( 'POST', $path, @@ -63,7 +70,13 @@ public function GetState(string $id): MessageState { throw new \RuntimeException('Invalid response'); } - return MessageState::FromObject($response); + $state = MessageState::FromObject($response); + + if (isset($this->encryptor)) { + $state = $state->Decrypt($this->encryptor); + } + + return $state; } /** diff --git a/src/EncryptedClient.php b/src/EncryptedClient.php deleted file mode 100644 index 38bc795..0000000 --- a/src/EncryptedClient.php +++ /dev/null @@ -1,32 +0,0 @@ -encryptor = new Encryptor($passphrase); - } - - public function Send(Message $message): MessageState { - $message = $message->Encrypt($this->encryptor); - return parent::Send($message)->Decrypt($this->encryptor); - } - - public function GetState(string $id): MessageState { - return parent::GetState($id)->Decrypt($this->encryptor); - } -} \ No newline at end of file diff --git a/src/Encryptor.php b/src/Encryptor.php index aaa977d..bf58269 100644 --- a/src/Encryptor.php +++ b/src/Encryptor.php @@ -4,25 +4,47 @@ class Encryptor { protected string $passphrase; + protected int $iterationCount; + /** + * Encryptor constructor. + * @param string $passphrase Passphrase to use for encryption + * @param int $iterationCount Iteration count + */ public function __construct( - string $passphrase + string $passphrase, + int $iterationCount = 75000 ) { $this->passphrase = $passphrase; + $this->iterationCount = $iterationCount; } public function Encrypt(string $data): string { $salt = $this->generateSalt(); - $secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt); + $secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt, 32, $this->iterationCount); - return base64_encode($salt) . '.' . openssl_encrypt($data, 'aes-256-cbc', $secretKey, 0, $salt); + return sprintf( + '$aes-256-cbc/pbkdf2-sha1$i=%d$%s$%s', + $this->iterationCount, + base64_encode($salt), + openssl_encrypt($data, 'aes-256-cbc', $secretKey, 0, $salt) + ); } public function Decrypt(string $data): string { - list($saltBase64, $encryptedBase64) = explode('.', $data, 2); + list($_, $algo, $paramsStr, $saltBase64, $encryptedBase64) = explode('$', $data); + + if ($algo !== 'aes-256-cbc/pbkdf2-sha1') { + throw new \RuntimeException('Unsupported algorithm'); + } + + $params = $this->parseParams($paramsStr); + if (empty($params['i'])) { + throw new \RuntimeException('Missing iteration count'); + } $salt = base64_decode($saltBase64); - $secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt); + $secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt, 32, intval($params['i'])); return openssl_decrypt($encryptedBase64, 'aes-256-cbc', $secretKey, 0, $salt); } @@ -39,4 +61,17 @@ protected function generateSecretKeyFromPassphrase( ): string { return hash_pbkdf2('sha1', $passphrase, $salt, $iterationCount, $keyLength, true); } + + /** + * @return array + */ + protected function parseParams(string $params): array { + $keyValuePairs = explode(',', $params); + $result = []; + foreach ($keyValuePairs as $pair) { + list($key, $value) = explode('=', $pair, 2); + $result[$key] = $value; + } + return $result; + } } \ No newline at end of file