Skip to content

Commit

Permalink
Added: encryption support
Browse files Browse the repository at this point in the history
  • Loading branch information
capcom6 committed Jan 18, 2024
1 parent c34345b commit 0c0e145
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 12 deletions.
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,33 +26,46 @@ 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;

$login = 'your_login';
$password = 'your_password';

$client = new Client($login, $password);
// or
// $encryptor = new Encryptor('your_passphrase');
// $client = new EncryptedClient($login, $password, Client::DEFAULT_URL, $httpClient, $encryptor);

$message = new Message('Your message text here.', ['+1234567890']);

try {
$messageState = $client->Send($message);
echo "Message sent with ID: " . $messageState->ID();
echo "Message sent with ID: " . $messageState->ID() . PHP_EOL;
} catch (Exception $e) {
echo "Error sending message: " . $e->getMessage();
echo "Error sending message: " . $e->getMessage() . PHP_EOL;
die(1);
}

try {
$messageState = $client->GetState($messageState->ID());
echo "Message state: " . $messageState->State();
echo "Message state: " . $messageState->State() . PHP_EOL;
} catch (Exception $e) {
echo "Error getting message state: " . $e->getMessage();
echo "Error getting message state: " . $e->getMessage() . PHP_EOL;
die(1);
}
```

## Methods
## Clients

The `Client` class provides the following methods:
There are two clients available:

- `Client` is used for sending SMS messages in plain text, but can also be used for sending encrypted messages by providing an `Encryptor`.

### 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.
Expand Down
25 changes: 22 additions & 3 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Client {
protected string $baseUrl;

protected HttpClient $client;
protected ?Encryptor $encryptor;

protected RequestFactoryInterface $requestFactory;
protected StreamFactoryInterface $streamFactory;
Expand All @@ -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();
Expand All @@ -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,
Expand All @@ -49,7 +56,13 @@ public function Send(Message $message): 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;
}

public function GetState(string $id): MessageState {
Expand All @@ -63,7 +76,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;
}

/**
Expand Down
30 changes: 29 additions & 1 deletion src/Domain/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace AndroidSmsGateway\Domain;

use AndroidSmsGateway\Encryptor;
use AndroidSmsGateway\Interfaces\SerializableInterface;

/**
Expand Down Expand Up @@ -30,6 +31,10 @@ class Message implements SerializableInterface {
* Request delivery report, `true` by default
*/
private bool $withDeliveryReport;
/**
* Is message and phones encrypted, `false` by default
*/
private bool $isEncrypted = false;
/**
* Phone numbers in E164 format
* @var array<string>
Expand All @@ -39,13 +44,35 @@ class Message implements SerializableInterface {
/**
* @param array<string> $phoneNumbers
*/
public function __construct(string $message, array $phoneNumbers, ?string $id = null, ?int $ttl = null, ?int $simNumber = null, bool $withDeliveryReport = true) {
public function __construct(
string $message,
array $phoneNumbers,
?string $id = null,
?int $ttl = null,
?int $simNumber = null,
bool $withDeliveryReport = true
) {
$this->id = $id;
$this->message = $message;
$this->ttl = $ttl;
$this->simNumber = $simNumber;
$this->withDeliveryReport = $withDeliveryReport;
$this->phoneNumbers = $phoneNumbers;
$this->isEncrypted = false;
}

public function Encrypt(Encryptor $encryptor): self {
if ($this->isEncrypted) {
return $this;
}

$this->isEncrypted = true;
$this->message = $encryptor->Encrypt($this->message);
$this->phoneNumbers = array_map(
fn(string $phoneNumber) => $encryptor->Encrypt($phoneNumber),
$this->phoneNumbers
);
return $this;
}

public function ToObject(): object {
Expand All @@ -55,6 +82,7 @@ public function ToObject(): object {
'ttl' => $this->ttl,
'simNumber' => $this->simNumber,
'withDeliveryReport' => $this->withDeliveryReport,
'isEncrypted' => $this->isEncrypted,
'phoneNumbers' => $this->phoneNumbers
];
}
Expand Down
63 changes: 61 additions & 2 deletions src/Domain/MessageState.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace AndroidSmsGateway\Domain;

use AndroidSmsGateway\Encryptor;
use AndroidSmsGateway\Enums\ProcessState;

/**
Expand All @@ -24,38 +25,96 @@ class MessageState {
*/
private array $recipients;

/**
* Is message and phones hashed
* @var bool
*/
private bool $isHashed;

/**
* Is message and phones encrypted
* @var bool
*/
private bool $isEncrypted;

/**
* @param array<RecipientState> $recipients
*/
public function __construct(string $id, ProcessState $state, array $recipients) {
public function __construct(
string $id,
ProcessState $state,
array $recipients,
bool $isHashed = false,
bool $isEncrypted = false
) {
$this->id = $id;
$this->state = $state;
$this->recipients = $recipients;
$this->isHashed = $isHashed;
$this->isEncrypted = $isEncrypted;
}

/**
* Get message ID
* @return string
*/
public function ID(): string {
return $this->id;
}

/**
* Get message state
* @return ProcessState
*/
public function State(): ProcessState {
return $this->state;
}

/**
* Is message and phones hashed
* @return bool
*/
public function IsHashed(): bool {
return $this->isHashed;
}

/**
* Get recipient states
* @return array<RecipientState>
*/
public function Recipients(): array {
return $this->recipients;
}

public function Decrypt(Encryptor $encryptor): self {
if ($this->isHashed) {
return $this;
}

if (!$this->isEncrypted) {
return $this;
}

$this->recipients = array_map(
static fn(RecipientState $recipient) => $recipient->Decrypt($encryptor),
$this->recipients
);

$this->isEncrypted = false;

return $this;
}

public static function FromObject(object $obj): self {
return new self(
$obj->id,
ProcessState::FromValue($obj->state),
array_map(
static fn($obj) => RecipientState::FromObject($obj),
$obj->recipients
)
),
$obj->isHashed ?? false,
$obj->isEncrypted ?? false
);
}
}
7 changes: 7 additions & 0 deletions src/Domain/RecipientState.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace AndroidSmsGateway\Domain;

use AndroidSmsGateway\Encryptor;
use AndroidSmsGateway\Enums\ProcessState;

/**
Expand Down Expand Up @@ -39,6 +40,12 @@ public function Error(): ?string {
return $this->error;
}

public function Decrypt(Encryptor $encryptor): self {
$this->phoneNumber = $encryptor->Decrypt($this->phoneNumber);

return $this;
}

public static function FromObject(object $obj): self {
return new self(
$obj->phoneNumber,
Expand Down
77 changes: 77 additions & 0 deletions src/Encryptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace AndroidSmsGateway;

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,
int $iterationCount = 75000
) {
$this->passphrase = $passphrase;
$this->iterationCount = $iterationCount;
}

public function Encrypt(string $data): string {
$salt = $this->generateSalt();
$secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt, 32, $this->iterationCount);

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($_, $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, 32, intval($params['i']));

return openssl_decrypt($encryptedBase64, 'aes-256-cbc', $secretKey, 0, $salt);
}

protected function generateSalt(int $size = 16): string {
return random_bytes($size);
}

protected function generateSecretKeyFromPassphrase(
string $passphrase,
string $salt,
int $keyLength = 32,
int $iterationCount = 75000
): string {
return hash_pbkdf2('sha1', $passphrase, $salt, $iterationCount, $keyLength, true);
}

/**
* @return array<string, string>
*/
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;
}
}
1 change: 1 addition & 0 deletions tests/Domain/MessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public function testCanSerializeToObject(): void {
'ttl' => $ttl,
'simNumber' => $simNumber,
'withDeliveryReport' => $withDeliveryReport,
'isEncrypted' => false,
'phoneNumbers' => $phoneNumbers
];

Expand Down

0 comments on commit 0c0e145

Please sign in to comment.