Skip to content

Commit ee8390e

Browse files
authored
Added: end-to-end encryption support (#5)
* Added: encryption support
1 parent 468fb3f commit ee8390e

7 files changed

Lines changed: 214 additions & 13 deletions

File tree

README.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ composer require capcom6/android-sms-gateway
1717

1818
## Usage
1919

20-
Here is a simple example of how to send a message using the client:
20+
Here is a simple example of how to send a message using the library:
2121

2222

2323
```php
@@ -26,33 +26,43 @@ Here is a simple example of how to send a message using the client:
2626
require 'vendor/autoload.php';
2727

2828
use AndroidSmsGateway\Client;
29+
use AndroidSmsGateway\Encryptor;
2930
use AndroidSmsGateway\Domain\Message;
3031

3132
$login = 'your_login';
3233
$password = 'your_password';
3334

3435
$client = new Client($login, $password);
36+
// or
37+
// $encryptor = new Encryptor('your_passphrase');
38+
// $client = new Client($login, $password, Client::DEFAULT_URL, $httpClient, $encryptor);
3539

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

3842
try {
3943
$messageState = $client->Send($message);
40-
echo "Message sent with ID: " . $messageState->ID();
44+
echo "Message sent with ID: " . $messageState->ID() . PHP_EOL;
4145
} catch (Exception $e) {
42-
echo "Error sending message: " . $e->getMessage();
46+
echo "Error sending message: " . $e->getMessage() . PHP_EOL;
47+
die(1);
4348
}
4449

4550
try {
4651
$messageState = $client->GetState($messageState->ID());
47-
echo "Message state: " . $messageState->State();
52+
echo "Message state: " . $messageState->State() . PHP_EOL;
4853
} catch (Exception $e) {
49-
echo "Error getting message state: " . $e->getMessage();
54+
echo "Error getting message state: " . $e->getMessage() . PHP_EOL;
55+
die(1);
5056
}
5157
```
5258

53-
## Methods
59+
## Client
5460

55-
The `Client` class provides the following methods:
61+
The `Client` is used for sending SMS messages in plain text, but can also be used for sending encrypted messages by providing an `Encryptor`.
62+
63+
### Methods
64+
65+
The `Client` class has the following methods:
5666

5767
* `Send(Message $message)`: Send a new SMS message.
5868
* `GetState(string $id)`: Retrieve the state of a previously sent message by its ID.

src/Client.php

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Client {
1919
protected string $baseUrl;
2020

2121
protected HttpClient $client;
22+
protected ?Encryptor $encryptor;
2223

2324
protected RequestFactoryInterface $requestFactory;
2425
protected StreamFactoryInterface $streamFactory;
@@ -27,11 +28,13 @@ public function __construct(
2728
string $login,
2829
string $password,
2930
string $serverUrl = self::DEFAULT_URL,
30-
?HttpClient $client = null
31+
?HttpClient $client = null,
32+
?Encryptor $encryptor = null
3133
) {
3234
$this->basicAuth = base64_encode($login . ':' . $password);
3335
$this->baseUrl = $serverUrl;
3436
$this->client = $client ?? HttpClientDiscovery::find();
37+
$this->encryptor = $encryptor;
3538

3639
$this->requestFactory = Psr17FactoryDiscovery::findRequestFactory();
3740
$this->streamFactory = Psr17FactoryDiscovery::findStreamFactory();
@@ -40,6 +43,10 @@ public function __construct(
4043
public function Send(Message $message): MessageState {
4144
$path = '/message';
4245

46+
if (isset($this->encryptor)) {
47+
$message = $message->Encrypt($this->encryptor);
48+
}
49+
4350
$response = $this->sendRequest(
4451
'POST',
4552
$path,
@@ -49,7 +56,13 @@ public function Send(Message $message): MessageState {
4956
throw new \RuntimeException('Invalid response');
5057
}
5158

52-
return MessageState::FromObject($response);
59+
$state = MessageState::FromObject($response);
60+
61+
if (isset($this->encryptor)) {
62+
$state = $state->Decrypt($this->encryptor);
63+
}
64+
65+
return $state;
5366
}
5467

5568
public function GetState(string $id): MessageState {
@@ -63,7 +76,13 @@ public function GetState(string $id): MessageState {
6376
throw new \RuntimeException('Invalid response');
6477
}
6578

66-
return MessageState::FromObject($response);
79+
$state = MessageState::FromObject($response);
80+
81+
if (isset($this->encryptor)) {
82+
$state = $state->Decrypt($this->encryptor);
83+
}
84+
85+
return $state;
6786
}
6887

6988
/**

src/Domain/Message.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace AndroidSmsGateway\Domain;
44

5+
use AndroidSmsGateway\Encryptor;
56
use AndroidSmsGateway\Interfaces\SerializableInterface;
67

78
/**
@@ -30,6 +31,10 @@ class Message implements SerializableInterface {
3031
* Request delivery report, `true` by default
3132
*/
3233
private bool $withDeliveryReport;
34+
/**
35+
* Is message and phones encrypted, `false` by default
36+
*/
37+
private bool $isEncrypted = false;
3338
/**
3439
* Phone numbers in E164 format
3540
* @var array<string>
@@ -39,13 +44,35 @@ class Message implements SerializableInterface {
3944
/**
4045
* @param array<string> $phoneNumbers
4146
*/
42-
public function __construct(string $message, array $phoneNumbers, ?string $id = null, ?int $ttl = null, ?int $simNumber = null, bool $withDeliveryReport = true) {
47+
public function __construct(
48+
string $message,
49+
array $phoneNumbers,
50+
?string $id = null,
51+
?int $ttl = null,
52+
?int $simNumber = null,
53+
bool $withDeliveryReport = true
54+
) {
4355
$this->id = $id;
4456
$this->message = $message;
4557
$this->ttl = $ttl;
4658
$this->simNumber = $simNumber;
4759
$this->withDeliveryReport = $withDeliveryReport;
4860
$this->phoneNumbers = $phoneNumbers;
61+
$this->isEncrypted = false;
62+
}
63+
64+
public function Encrypt(Encryptor $encryptor): self {
65+
if ($this->isEncrypted) {
66+
return $this;
67+
}
68+
69+
$this->isEncrypted = true;
70+
$this->message = $encryptor->Encrypt($this->message);
71+
$this->phoneNumbers = array_map(
72+
fn(string $phoneNumber) => $encryptor->Encrypt($phoneNumber),
73+
$this->phoneNumbers
74+
);
75+
return $this;
4976
}
5077

5178
public function ToObject(): object {
@@ -55,6 +82,7 @@ public function ToObject(): object {
5582
'ttl' => $this->ttl,
5683
'simNumber' => $this->simNumber,
5784
'withDeliveryReport' => $this->withDeliveryReport,
85+
'isEncrypted' => $this->isEncrypted,
5886
'phoneNumbers' => $this->phoneNumbers
5987
];
6088
}

src/Domain/MessageState.php

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace AndroidSmsGateway\Domain;
44

5+
use AndroidSmsGateway\Encryptor;
56
use AndroidSmsGateway\Enums\ProcessState;
67

78
/**
@@ -24,38 +25,96 @@ class MessageState {
2425
*/
2526
private array $recipients;
2627

28+
/**
29+
* Is message and phones hashed
30+
* @var bool
31+
*/
32+
private bool $isHashed;
33+
34+
/**
35+
* Is message and phones encrypted
36+
* @var bool
37+
*/
38+
private bool $isEncrypted;
39+
2740
/**
2841
* @param array<RecipientState> $recipients
2942
*/
30-
public function __construct(string $id, ProcessState $state, array $recipients) {
43+
public function __construct(
44+
string $id,
45+
ProcessState $state,
46+
array $recipients,
47+
bool $isHashed = false,
48+
bool $isEncrypted = false
49+
) {
3150
$this->id = $id;
3251
$this->state = $state;
3352
$this->recipients = $recipients;
53+
$this->isHashed = $isHashed;
54+
$this->isEncrypted = $isEncrypted;
3455
}
3556

57+
/**
58+
* Get message ID
59+
* @return string
60+
*/
3661
public function ID(): string {
3762
return $this->id;
3863
}
3964

65+
/**
66+
* Get message state
67+
* @return ProcessState
68+
*/
4069
public function State(): ProcessState {
4170
return $this->state;
4271
}
4372

4473
/**
74+
* Is message and phones hashed
75+
* @return bool
76+
*/
77+
public function IsHashed(): bool {
78+
return $this->isHashed;
79+
}
80+
81+
/**
82+
* Get recipient states
4583
* @return array<RecipientState>
4684
*/
4785
public function Recipients(): array {
4886
return $this->recipients;
4987
}
5088

89+
public function Decrypt(Encryptor $encryptor): self {
90+
if ($this->isHashed) {
91+
return $this;
92+
}
93+
94+
if (!$this->isEncrypted) {
95+
return $this;
96+
}
97+
98+
$this->recipients = array_map(
99+
static fn(RecipientState $recipient) => $recipient->Decrypt($encryptor),
100+
$this->recipients
101+
);
102+
103+
$this->isEncrypted = false;
104+
105+
return $this;
106+
}
107+
51108
public static function FromObject(object $obj): self {
52109
return new self(
53110
$obj->id,
54111
ProcessState::FromValue($obj->state),
55112
array_map(
56113
static fn($obj) => RecipientState::FromObject($obj),
57114
$obj->recipients
58-
)
115+
),
116+
$obj->isHashed ?? false,
117+
$obj->isEncrypted ?? false
59118
);
60119
}
61120
}

src/Domain/RecipientState.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace AndroidSmsGateway\Domain;
44

5+
use AndroidSmsGateway\Encryptor;
56
use AndroidSmsGateway\Enums\ProcessState;
67

78
/**
@@ -39,6 +40,12 @@ public function Error(): ?string {
3940
return $this->error;
4041
}
4142

43+
public function Decrypt(Encryptor $encryptor): self {
44+
$this->phoneNumber = $encryptor->Decrypt($this->phoneNumber);
45+
46+
return $this;
47+
}
48+
4249
public static function FromObject(object $obj): self {
4350
return new self(
4451
$obj->phoneNumber,

src/Encryptor.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace AndroidSmsGateway;
4+
5+
class Encryptor {
6+
protected string $passphrase;
7+
protected int $iterationCount;
8+
9+
/**
10+
* Encryptor constructor.
11+
* @param string $passphrase Passphrase to use for encryption
12+
* @param int $iterationCount Iteration count
13+
*/
14+
public function __construct(
15+
string $passphrase,
16+
int $iterationCount = 75000
17+
) {
18+
$this->passphrase = $passphrase;
19+
$this->iterationCount = $iterationCount;
20+
}
21+
22+
public function Encrypt(string $data): string {
23+
$salt = $this->generateSalt();
24+
$secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt, 32, $this->iterationCount);
25+
26+
return sprintf(
27+
'$aes-256-cbc/pbkdf2-sha1$i=%d$%s$%s',
28+
$this->iterationCount,
29+
base64_encode($salt),
30+
openssl_encrypt($data, 'aes-256-cbc', $secretKey, 0, $salt)
31+
);
32+
}
33+
34+
public function Decrypt(string $data): string {
35+
list($_, $algo, $paramsStr, $saltBase64, $encryptedBase64) = explode('$', $data);
36+
37+
if ($algo !== 'aes-256-cbc/pbkdf2-sha1') {
38+
throw new \RuntimeException('Unsupported algorithm');
39+
}
40+
41+
$params = $this->parseParams($paramsStr);
42+
if (empty($params['i'])) {
43+
throw new \RuntimeException('Missing iteration count');
44+
}
45+
46+
$salt = base64_decode($saltBase64);
47+
$secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt, 32, intval($params['i']));
48+
49+
return openssl_decrypt($encryptedBase64, 'aes-256-cbc', $secretKey, 0, $salt);
50+
}
51+
52+
protected function generateSalt(int $size = 16): string {
53+
return random_bytes($size);
54+
}
55+
56+
protected function generateSecretKeyFromPassphrase(
57+
string $passphrase,
58+
string $salt,
59+
int $keyLength = 32,
60+
int $iterationCount = 75000
61+
): string {
62+
return hash_pbkdf2('sha1', $passphrase, $salt, $iterationCount, $keyLength, true);
63+
}
64+
65+
/**
66+
* @return array<string, string>
67+
*/
68+
protected function parseParams(string $params): array {
69+
$keyValuePairs = explode(',', $params);
70+
$result = [];
71+
foreach ($keyValuePairs as $pair) {
72+
list($key, $value) = explode('=', $pair, 2);
73+
$result[$key] = $value;
74+
}
75+
return $result;
76+
}
77+
}

0 commit comments

Comments
 (0)