From 9f178fe9130ded8af6b2edf836c5674094504ca9 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Mon, 8 Jan 2024 15:55:14 +0700 Subject: [PATCH 1/4] Added: support for encrypted messages --- api/local.http | 18 +++++- api/requests.http | 16 +++++ .../mysql/20240108142043_encryption.sql | 18 ++++++ internal/sms-gateway/models/models.go | 5 +- internal/sms-gateway/services/messages.go | 26 +++++--- pkg/smsgateway/domain.go | 16 ++--- web/mkdocs/docs/privacy/encryption.md | 62 +++++++++++++++++++ .../docs/{privacy.md => privacy/policy.md} | 9 +-- web/mkdocs/mkdocs.yml | 6 +- 9 files changed, 153 insertions(+), 23 deletions(-) create mode 100644 internal/sms-gateway/models/migrations/mysql/20240108142043_encryption.sql create mode 100644 web/mkdocs/docs/privacy/encryption.md rename web/mkdocs/docs/{privacy.md => privacy/policy.md} (53%) diff --git a/api/local.http b/api/local.http index ebf49327..85d3c6ab 100644 --- a/api/local.http +++ b/api/local.http @@ -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}} diff --git a/api/requests.http b/api/requests.http index 0c6b7dac..ade3f74f 100644 --- a/api/requests.http +++ b/api/requests.http @@ -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}} diff --git a/internal/sms-gateway/models/migrations/mysql/20240108142043_encryption.sql b/internal/sms-gateway/models/migrations/mysql/20240108142043_encryption.sql new file mode 100644 index 00000000..adaacd97 --- /dev/null +++ b/internal/sms-gateway/models/migrations/mysql/20240108142043_encryption.sql @@ -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 \ No newline at end of file diff --git a/internal/sms-gateway/models/models.go b/internal/sms-gateway/models/models.go index 180394cf..d8951657 100644 --- a/internal/sms-gateway/models/models.go +++ b/internal/sms-gateway/models/models.go @@ -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)"` } diff --git a/internal/sms-gateway/services/messages.go b/internal/sms-gateway/services/messages.go index 362fae30..08a8fe9e 100644 --- a/internal/sms-gateway/services/messages.go +++ b/internal/sms-gateway/services/messages.go @@ -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), } } diff --git a/pkg/smsgateway/domain.go b/pkg/smsgateway/domain.go index 96226dbf..46727def 100644 --- a/pkg/smsgateway/domain.go +++ b/pkg/smsgateway/domain.go @@ -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"` // Ошибка } diff --git a/web/mkdocs/docs/privacy/encryption.md b/web/mkdocs/docs/privacy/encryption.md new file mode 100644 index 00000000..d98adc1f --- /dev/null +++ b/web/mkdocs/docs/privacy/encryption.md @@ -0,0 +1,62 @@ +# 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. + +## 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 an iteration count of 300,000. +4. Encrypt the message text and recipients' phone numbers using the AES-256-CBC algorithm and encode the result as Base64. +5. Add the Base64-encoded salt to the front of the encrypted data, separated by a dot. + +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; + + public function __construct( + string $passphrase + ) { + $this->passphrase = $passphrase; + } + + public function Encrypt(string $data): string { + $salt = $this->generateSalt(); + $secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt); + + return base64_encode($salt) . '.' . openssl_encrypt($data, 'aes-256-cbc', $secretKey, 0, $salt); + } + + public function Decrypt(string $data): string { + list($saltBase64, $encryptedBase64) = explode('.', $data, 2); + + $salt = base64_decode($saltBase64); + $secretKey = $this->generateSecretKeyFromPassphrase($this->passphrase, $salt); + + 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); + } +} +``` \ No newline at end of file diff --git a/web/mkdocs/docs/privacy.md b/web/mkdocs/docs/privacy/policy.md similarity index 53% rename from web/mkdocs/docs/privacy.md rename to web/mkdocs/docs/privacy/policy.md index 3e4fc6cc..e9bd7e74 100644 --- a/web/mkdocs/docs/privacy.md +++ b/web/mkdocs/docs/privacy/policy.md @@ -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**: Message content and recipient phone numbers are stored on the server in encrypted form and are only accessible to your device. After your device confirms receipt, this information is converted into a SHA256 hash within 15 minutes, ensuring it is not stored in clear form and further enhancing privacy. - **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. \ No newline at end of file diff --git a/web/mkdocs/mkdocs.yml b/web/mkdocs/mkdocs.yml index 6b1d8686..5d9217d9 100644 --- a/web/mkdocs/mkdocs.yml +++ b/web/mkdocs/mkdocs.yml @@ -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 From 47e156d768b5a83f696f5ffe1160080d9a6d000f Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 16 Jan 2024 20:56:21 +0700 Subject: [PATCH 2/4] Fixed: do not hash encrypted messages --- internal/sms-gateway/repositories/messages.go | 2 +- web/mkdocs/docs/privacy/policy.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/sms-gateway/repositories/messages.go b/internal/sms-gateway/repositories/messages.go index 7163988b..f4c55a5e 100644 --- a/internal/sms-gateway/repositories/messages.go +++ b/internal/sms-gateway/repositories/messages.go @@ -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 { diff --git a/web/mkdocs/docs/privacy/policy.md b/web/mkdocs/docs/privacy/policy.md index e9bd7e74..28aff2ab 100644 --- a/web/mkdocs/docs/privacy/policy.md +++ b/web/mkdocs/docs/privacy/policy.md @@ -11,7 +11,7 @@ We believe in transparency and the importance of privacy. Here's how we handle i - **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**: Message content and recipient phone numbers are stored on the server in encrypted form and are only accessible to your device. After your device confirms receipt, this information is converted into a SHA256 hash within 15 minutes, ensuring it is not stored in clear form and further enhancing privacy. +- **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. ## No Collection of Usage Statistics From edd437bc5fdf5f9c428d1eb6bfd4815cb1b452ad Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Wed, 17 Jan 2024 09:59:08 +0700 Subject: [PATCH 3/4] Updated: encrypted string format --- web/mkdocs/docs/privacy/encryption.md | 53 +++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/web/mkdocs/docs/privacy/encryption.md b/web/mkdocs/docs/privacy/encryption.md index d98adc1f..1019bcd3 100644 --- a/web/mkdocs/docs/privacy/encryption.md +++ b/web/mkdocs/docs/privacy/encryption.md @@ -2,6 +2,8 @@ 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. @@ -12,9 +14,9 @@ The application supports end-to-end encryption by encrypting message text and re 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 an iteration count of 300,000. +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. Add the Base64-encoded salt to the front of the encrypted data, separated by a dot. +5. Format result as `$aes-256-cbc/pbkdf2-sha1$i=$$`. 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: @@ -23,25 +25,47 @@ Or use one of the following realization: ```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 + 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); - - return base64_encode($salt) . '.' . openssl_encrypt($data, 'aes-256-cbc', $secretKey, 0, $salt); + $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($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); } @@ -58,5 +82,18 @@ class Encryptor { ): 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 From 1a9a84264d2e9cea6fc86428ff2f071c4d5fbfa6 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 23 Jan 2024 09:01:24 +0700 Subject: [PATCH 4/4] Fixed: phone number length validation --- pkg/smsgateway/domain.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/smsgateway/domain.go b/pkg/smsgateway/domain.go index 46727def..aca591e2 100644 --- a/pkg/smsgateway/domain.go +++ b/pkg/smsgateway/domain.go @@ -12,13 +12,13 @@ const ( // Сообщение type Message struct { - ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Идентификатор - Message string `json:"message" validate:"required,max=65535" example:"Hello World!"` // Текст сообщения - 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"` // Получатели + ID string `json:"id,omitempty" validate:"omitempty,max=36" example:"PyDmBQZZXYmyxMwED8Fzy"` // Идентификатор + Message string `json:"message" validate:"required,max=65535" example:"Hello World!"` // Текст сообщения + 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,max=128" example:"79990001234"` // Получатели } // Состояние сообщения @@ -32,7 +32,7 @@ type MessageState struct { // Детализация состояния type RecipientState struct { - 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"` // Ошибка + PhoneNumber string `json:"phoneNumber" validate:"required,min=10,max=128" example:"79990001234"` // Номер телефона или первые 16 символов SHA256 + State ProcessState `json:"state" validate:"required" example:"Pending"` // Состояние + Error *string `json:"error,omitempty" example:"timeout"` // Ошибка }