mirror of
https://github.com/makayabou/asg-server.git
synced 2026-05-02 17:43:36 +02:00
Added: support for encrypted messages
This commit is contained in:
parent
b8fd478afa
commit
86d59aabfc
@ -18,5 +18,21 @@ Authorization: Basic {{localCredentials}}
|
||||
}
|
||||
|
||||
###
|
||||
GET {{localUrl}}/message/hr8HV_KE0kehtBCApEaIn HTTP/1.1
|
||||
POST {{localUrl}}/message HTTP/1.1
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
{
|
||||
"message": "17wc9/ZRf1l84LHkEK3hgA==.aH1XrMHAeMyF4PeiavV3dk8o2fP0nSo92IqseLQfg14=",
|
||||
"ttl": 600,
|
||||
"phoneNumbers": [
|
||||
"xkQeXzSDFj2xP6JBUMK0pA==.PfUHEa9QZv8h7JnUoBlmWw=="
|
||||
],
|
||||
"simNumber": 1,
|
||||
"withDeliveryReport": true,
|
||||
"isEncrypted": true
|
||||
}
|
||||
|
||||
###
|
||||
GET {{localUrl}}/message/2a1hOxM1zuZVygvE3uX0j HTTP/1.1
|
||||
Authorization: Basic {{localCredentials}}
|
||||
|
||||
@ -27,6 +27,22 @@ Authorization: Basic {{credentials}}
|
||||
"withDeliveryReport": true
|
||||
}
|
||||
|
||||
###
|
||||
POST {{baseUrl}}/api/3rdparty/v1/message HTTP/1.1
|
||||
Content-Type: application/json
|
||||
Authorization: Basic {{credentials}}
|
||||
|
||||
{
|
||||
"message": "17wc9/ZRf1l84LHkEK3hgA==.aH1XrMHAeMyF4PeiavV3dk8o2fP0nSo92IqseLQfg14=",
|
||||
"ttl": 600,
|
||||
"phoneNumbers": [
|
||||
"xkQeXzSDFj2xP6JBUMK0pA==.PfUHEa9QZv8h7JnUoBlmWw=="
|
||||
],
|
||||
"simNumber": 1,
|
||||
"withDeliveryReport": true,
|
||||
"isEncrypted": true
|
||||
}
|
||||
|
||||
###
|
||||
GET {{baseUrl}}/api/3rdparty/v1/message/-rnEaUz7KObDdokPrzKpM HTTP/1.1
|
||||
Authorization: Basic {{credentials}}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `messages`
|
||||
ADD `is_encrypted` tinyint(1) unsigned NOT NULL DEFAULT false;
|
||||
-- +goose StatementEnd
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `message_recipients`
|
||||
MODIFY COLUMN `phone_number` varchar(128) NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
---
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `message_recipients`
|
||||
MODIFY COLUMN `phone_number` varchar(16) NOT NULL;
|
||||
-- +goose StatementEnd
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE `messages` DROP `is_encrypted`;
|
||||
-- +goose StatementEnd
|
||||
@ -51,7 +51,8 @@ type Message struct {
|
||||
SimNumber *uint8 `gorm:"type:tinyint(1) unsigned"`
|
||||
WithDeliveryReport bool `gorm:"not null;type:tinyint(1) unsigned"`
|
||||
|
||||
IsHashed bool `gorm:"not null;type:tinyint(1) unsigned;default:0"`
|
||||
IsHashed bool `gorm:"not null;type:tinyint(1) unsigned;default:0"`
|
||||
IsEncrypted bool `gorm:"not null;type:tinyint(1) unsigned;default:0"`
|
||||
|
||||
Device Device `gorm:"foreignKey:DeviceID;constraint:OnDelete:CASCADE"`
|
||||
Recipients []MessageRecipient `gorm:"foreignKey:MessageID;constraint:OnDelete:CASCADE"`
|
||||
@ -61,7 +62,7 @@ type Message struct {
|
||||
|
||||
type MessageRecipient struct {
|
||||
MessageID uint64 `gorm:"primaryKey;type:BIGINT UNSIGNED"`
|
||||
PhoneNumber string `gorm:"primaryKey;type:varchar(16)"`
|
||||
PhoneNumber string `gorm:"primaryKey;type:varchar(128)"`
|
||||
State MessageState `gorm:"not null;type:enum('Pending','Sent','Processed','Delivered','Failed');default:Pending"`
|
||||
Error *string `gorm:"type:varchar(256)"`
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ func (r *MessagesRepository) HashProcessed() error {
|
||||
defer tx.Exec("SELECT RELEASE_LOCK(?)", HashingLockName)
|
||||
|
||||
err = tx.Model(&models.MessageRecipient{}).
|
||||
Where("message_id IN (?)", tx.Model(&models.Message{}).Select("id").Where("is_hashed = ? AND state <> ?", false, models.MessageStatePending)).
|
||||
Where("message_id IN (?)", tx.Model(&models.Message{}).Select("id").Where("is_hashed = ? AND is_encrypted = ? AND state <> ?", false, false, models.MessageStatePending)).
|
||||
Update("phone_number", gorm.Expr("LEFT(SHA2(phone_number, 256), 16)")).
|
||||
Error
|
||||
if err != nil {
|
||||
|
||||
@ -69,9 +69,10 @@ func (s *MessagesService) SelectPending(deviceID string) ([]smsgateway.Message,
|
||||
ID: v.ExtID,
|
||||
Message: v.Message,
|
||||
TTL: ttl,
|
||||
PhoneNumbers: s.recipientsToDomain(v.Recipients),
|
||||
SimNumber: v.SimNumber,
|
||||
WithDeliveryReport: types.AsPointer[bool](v.WithDeliveryReport),
|
||||
IsEncrypted: v.IsEncrypted,
|
||||
PhoneNumbers: s.recipientsToDomain(v.Recipients),
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,10 +115,15 @@ func (s *MessagesService) Enqeue(device models.Device, message smsgateway.Messag
|
||||
Recipients: make([]smsgateway.RecipientState, len(message.PhoneNumbers)),
|
||||
}
|
||||
|
||||
var phone string
|
||||
var err error
|
||||
for i, v := range message.PhoneNumbers {
|
||||
phone, err := cleanPhoneNumber(v)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("can't use phone in row %d: %w", i+1, err)
|
||||
if message.IsEncrypted {
|
||||
phone = v
|
||||
} else {
|
||||
if phone, err = cleanPhoneNumber(v); err != nil {
|
||||
return state, fmt.Errorf("can't use phone in row %d: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
message.PhoneNumbers[i] = phone
|
||||
@ -140,7 +146,10 @@ func (s *MessagesService) Enqeue(device models.Device, message smsgateway.Messag
|
||||
ValidUntil: validUntil,
|
||||
SimNumber: message.SimNumber,
|
||||
WithDeliveryReport: types.OrDefault[bool](message.WithDeliveryReport, true),
|
||||
IsEncrypted: message.IsEncrypted,
|
||||
Device: device,
|
||||
Recipients: s.recipientsToModel(message.PhoneNumbers),
|
||||
TimedModel: models.TimedModel{},
|
||||
}
|
||||
if msg.ExtID == "" {
|
||||
msg.ExtID = s.idgen()
|
||||
@ -240,10 +249,11 @@ func (s *MessagesService) recipientsStateToModel(input []smsgateway.RecipientSta
|
||||
|
||||
func modelToMessageState(input models.Message) smsgateway.MessageState {
|
||||
return smsgateway.MessageState{
|
||||
ID: input.ExtID,
|
||||
State: smsgateway.ProcessState(input.State),
|
||||
IsHashed: input.IsHashed,
|
||||
Recipients: slices.Map(input.Recipients, modelToRecipientState),
|
||||
ID: input.ExtID,
|
||||
State: smsgateway.ProcessState(input.State),
|
||||
IsHashed: input.IsHashed,
|
||||
IsEncrypted: input.IsEncrypted,
|
||||
Recipients: slices.Map(input.Recipients, modelToRecipientState),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,20 +17,22 @@ type Message struct {
|
||||
TTL *uint64 `json:"ttl,omitempty" validate:"omitempty,min=5" example:"86400"` // Время жизни сообщения в секундах
|
||||
SimNumber *uint8 `json:"simNumber,omitempty" validate:"omitempty,max=3" example:"1"` // Номер сим-карты
|
||||
WithDeliveryReport *bool `json:"withDeliveryReport,omitempty" example:"true"` // Запрашивать отчет о доставке
|
||||
IsEncrypted bool `json:"isEncrypted,omitempty" example:"true"` // Зашифровано
|
||||
PhoneNumbers []string `json:"phoneNumbers" validate:"required,min=1,max=100,dive,required,min=10" example:"79990001234"` // Получатели
|
||||
}
|
||||
|
||||
// Состояние сообщения
|
||||
type MessageState struct {
|
||||
ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Идентификатор
|
||||
State ProcessState `json:"state" validate:"required" example:"Pending"` // Состояние
|
||||
IsHashed bool `json:"isHashed" example:"false"` // Хэшировано
|
||||
Recipients []RecipientState `json:"recipients" validate:"required,min=1,dive"` // Детализация состояния по получателям
|
||||
ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Идентификатор
|
||||
State ProcessState `json:"state" validate:"required" example:"Pending"` // Состояние
|
||||
IsHashed bool `json:"isHashed" example:"false"` // Хэшировано
|
||||
IsEncrypted bool `json:"isEncrypted" example:"false"` // Зашифровано
|
||||
Recipients []RecipientState `json:"recipients" validate:"required,min=1,dive"` // Детализация состояния по получателям
|
||||
}
|
||||
|
||||
// Детализация состояния
|
||||
type RecipientState struct {
|
||||
PhoneNumber string `json:"phoneNumber" validate:"required,min=10" example:"79990001234"` // Номер телефона или первые 16 символов SHA256
|
||||
State ProcessState `json:"state" validate:"required" example:"Pending"` // Состояние
|
||||
Error *string `json:"error,omitempty" example:"timeout"` // Ошибка
|
||||
PhoneNumber string `json:"phoneNumber" validate:"required,min=10,max=64" example:"79990001234"` // Номер телефона или первые 16 символов SHA256
|
||||
State ProcessState `json:"state" validate:"required" example:"Pending"` // Состояние
|
||||
Error *string `json:"error,omitempty" example:"timeout"` // Ошибка
|
||||
}
|
||||
|
||||
99
web/mkdocs/docs/privacy/encryption.md
Normal file
99
web/mkdocs/docs/privacy/encryption.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Encryption
|
||||
|
||||
The application supports end-to-end encryption by encrypting message text and recipients' phone numbers before sending them to the API and decrypting them on the device. This ensures that no one – including us as the service provider, the hosting provider, or any third parties – can access the content and recipients of the messages.
|
||||
|
||||
Please note that using encryption will increase device battery usage.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. Fields `message` and every value in the `phoneNumbers` field must be encrypted.
|
||||
2. The `isEncrypted` field of the message object must be set to `true`.
|
||||
3. On the device, the same passphrase must be specified as in step 1.
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. Select a passphrase that will be used for encryption and specify it on the device.
|
||||
2. Generate a random salt, with 16 bytes being the recommended size.
|
||||
3. Create a secret key using the PBKDF2 algorithm with SHA1 hash function, key size of 256 bits, and recommended iteration count of 75,000.
|
||||
4. Encrypt the message text and recipients' phone numbers using the AES-256-CBC algorithm and encode the result as Base64.
|
||||
5. Format result as `$aes-256-cbc/pbkdf2-sha1$i=<iteration count>$<base64 encoded salt>$<base 64 encoded encrypted data>`. The format is inspired by [PHC](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md).
|
||||
|
||||
Or use one of the following realization:
|
||||
|
||||
### [PHP](https://github.com/capcom6/android-sms-gateway-php/blob/master/src/Encryptor.php)
|
||||
|
||||
```php
|
||||
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 = 300000
|
||||
): 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;
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -9,10 +9,11 @@ We believe in transparency and the importance of privacy. Here's how we handle i
|
||||
|
||||
## Cloud Mode
|
||||
|
||||
- **Encrypted Communication**: Communication between the app and the cloud server is encrypted.
|
||||
- **Encrypted Communication**: Communication between the app and the cloud server is encrypted using secure protocols to protect your data in transit.
|
||||
- **End-to-End Encryption**: We have implemented optional AES-based end-to-end encryption to ensure that all messages and phone numbers can be encrypted before being sent to the API. This means that data is encrypted before transmission and decrypted on the user's device before sending the SMS, ensuring that no one – including us as the service provider, the hosting provider, or any other party – can access the content and recipients of the messages.
|
||||
- **Message Handling**: If end-to-end encryption is not used, after your device confirms receipt, message content and recipients are converted into a SHA256 hash within 15 minutes, ensuring it is not stored in clear form.
|
||||
- **Limited Data Sharing**: Only essential data such as the device manufacturer, model, app version, and Firebase Cloud Messaging (FCM) token is sent to the server to enable cloud functionality.
|
||||
- **Message Handling**: Message content and recipient phone numbers are stored on the server only until your device confirms receipt. Afterwards, this information is converted into a SHA256 hash within 15 minutes, ensuring it is not stored in clear form.
|
||||
|
||||
|
||||
## No Collection of Usage Statistics
|
||||
|
||||
- **No Tracking**: We do not collect any usage statistics, including crash reports. Your usage of the app remains private and untracked.
|
||||
- **No Tracking**: We do not collect any usage statistics, including crash reports. Your usage of the app remains private and untracked.
|
||||
@ -3,6 +3,8 @@ site_url: https://sms.capcom.me
|
||||
repo_url: https://github.com/capcom6/android-sms-gateway
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- content.code.copy
|
||||
palette:
|
||||
# Palette toggle for automatic mode
|
||||
- media: "(prefers-color-scheme)"
|
||||
@ -30,7 +32,9 @@ nav:
|
||||
- Getting Started: getting-started.md
|
||||
- API: api.md
|
||||
- Pricing: pricing.md
|
||||
- Privacy: privacy.md
|
||||
- Privacy:
|
||||
- Policy: privacy/policy.md
|
||||
- Encryption: privacy/encryption.md
|
||||
- FAQ: faq.md
|
||||
- Contributing: contributing.md
|
||||
- License: license.md
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user