From 795731235a9ee6b4253b8ab60652665d4f67dad4 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 4 Aug 2026 14:39:01 +0700 Subject: [PATCH 1/2] [devices] public key operations --- go.mod | 2 +- go.sum | 6 +++-- .../handlers/converters/devices.go | 4 +++ .../handlers/converters/devices_test.go | 21 ++++++++++++++++ .../sms-gateway/handlers/messages/3rdparty.go | 10 +++----- internal/sms-gateway/handlers/mobile.go | 12 ++++++--- .../20260728000000_add_device_e2e_keys.sql | 25 +++++++++++++++++++ .../sms-gateway/modules/devices/domain.go | 13 +++++++++- .../sms-gateway/modules/devices/errors.go | 4 ++- .../sms-gateway/modules/devices/models.go | 12 +++++++-- .../sms-gateway/modules/devices/repository.go | 16 ++++++------ internal/sms-gateway/openapi/docs.go | 13 +++++++++- 12 files changed, 112 insertions(+), 26 deletions(-) create mode 100644 internal/sms-gateway/models/migrations/mysql/20260728000000_add_device_e2e_keys.sql diff --git a/go.mod b/go.mod index 7d5b64cd..ad7a85c4 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.8 require ( firebase.google.com/go/v4 v4.21.0 - github.com/android-sms-gateway/client-go v1.14.5 + github.com/android-sms-gateway/client-go v1.14.6-0.20260818010249-27de6c9d42cf github.com/ansrivas/fiberprometheus/v2 v2.17.0 github.com/capcom6/go-helpers v0.4.0 github.com/capcom6/go-infra-fx v0.5.9 diff --git a/go.sum b/go.sum index c1dc690c..5d1c572b 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,10 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw= -github.com/android-sms-gateway/client-go v1.14.5 h1:CtyXAHPdyDtCFTzeVPt5EUfnWhQ2RsIWbCUk52tbGjw= -github.com/android-sms-gateway/client-go v1.14.5/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.14.5-0.20260812005854-c3143ac48b54 h1:i4pOpObty9ZHkQWE5JmJMrx4hAB6hmQ6elmgc/+3oKs= +github.com/android-sms-gateway/client-go v1.14.5-0.20260812005854-c3143ac48b54/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= +github.com/android-sms-gateway/client-go v1.14.6-0.20260818010249-27de6c9d42cf h1:nNxdXmF6OL7H67X3XeY/kTa+ymJf8HtRFpo5qonR9Rk= +github.com/android-sms-gateway/client-go v1.14.6-0.20260818010249-27de6c9d42cf/go.mod h1:DQsReciU1xcaVW3T5Z2bqslNdsAwCFCtghawmA6g6L4= github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/ansrivas/fiberprometheus/v2 v2.17.0 h1:p0gqs5LsSCWGoSFF44fCJkyU+XcE6TLRqEMu80b2iCo= diff --git a/internal/sms-gateway/handlers/converters/devices.go b/internal/sms-gateway/handlers/converters/devices.go index 40ad9192..07545cb9 100644 --- a/internal/sms-gateway/handlers/converters/devices.go +++ b/internal/sms-gateway/handlers/converters/devices.go @@ -16,6 +16,10 @@ func DeviceToDTO(device devices.Device) smsgateway.Device { DeletedAt: device.DeletedAt, LastSeen: device.LastSeen, SimCards: mapSimCards(device.SimCards), + VersionedPublicKey: smsgateway.VersionedPublicKey{ + PublicKey: device.PublicKey, + KeyVersion: device.KeyVersion, + }, } } diff --git a/internal/sms-gateway/handlers/converters/devices_test.go b/internal/sms-gateway/handlers/converters/devices_test.go index ff2c005c..972a71f7 100644 --- a/internal/sms-gateway/handlers/converters/devices_test.go +++ b/internal/sms-gateway/handlers/converters/devices_test.go @@ -63,6 +63,27 @@ func TestDeviceToDTO(t *testing.T) { Name: "", }, }, + { + name: "device with versioned public key", + device: devices.Device{ + DeviceInput: devices.DeviceInput{ + DeviceInfo: devices.DeviceInfo{ + DeviceUpdate: devices.DeviceUpdate{ + PublicKey: lo.ToPtr("test-public-key"), + KeyVersion: lo.ToPtr(1), + }, + }, + ID: "test-id", + }, + }, + expected: smsgateway.Device{ + ID: "test-id", + VersionedPublicKey: smsgateway.VersionedPublicKey{ + PublicKey: lo.ToPtr("test-public-key"), + KeyVersion: lo.ToPtr(1), + }, + }, + }, { name: "device with sim cards", device: devices.Device{ diff --git a/internal/sms-gateway/handlers/messages/3rdparty.go b/internal/sms-gateway/handlers/messages/3rdparty.go index eb9735b1..382f3614 100644 --- a/internal/sms-gateway/handlers/messages/3rdparty.go +++ b/internal/sms-gateway/handlers/messages/3rdparty.go @@ -327,13 +327,9 @@ func (h *ThirdPartyController) errorHandler(c *fiber.Ctx) error { case errors.Is(err, messages.ErrMessageNotPending): return fiber.NewError(fiber.StatusConflict, err.Error()) - case errors.Is(err, devices.ErrNotFound): - fallthrough - case errors.Is(err, devices.ErrInvalidFilter): - fallthrough - case errors.Is(err, devices.ErrInvalidUser): - fallthrough - case errors.Is(err, devices.ErrMoreThanOne): + case errors.Is(err, devices.ErrNotFound), + errors.Is(err, devices.ErrInvalidFilter), + errors.Is(err, devices.ErrMoreThanOne): return fiber.NewError(fiber.StatusBadRequest, err.Error()) } diff --git a/internal/sms-gateway/handlers/mobile.go b/internal/sms-gateway/handlers/mobile.go index dbda34b2..39de8911 100644 --- a/internal/sms-gateway/handlers/mobile.go +++ b/internal/sms-gateway/handlers/mobile.go @@ -195,8 +195,10 @@ func (h *mobileHandler) postDevice(c *fiber.Ctx) error { userID, devices.DeviceInfo{ DeviceUpdate: devices.DeviceUpdate{ - PushToken: req.PushToken, - SimCards: h.simCardsToDomain(req.SimCards), + PushToken: req.PushToken, + SimCards: h.simCardsToDomain(req.SimCards), + PublicKey: req.PublicKey, + KeyVersion: req.KeyVersion, }, Name: req.Name, }, @@ -239,8 +241,10 @@ func (h *mobileHandler) patchDevice(device devices.Device, c *fiber.Ctx) error { } err := h.devicesSvc.Update(c.Context(), req.Id, devices.DeviceUpdate{ - PushToken: lo.EmptyableToPtr(req.PushToken), - SimCards: h.simCardsToDomain(req.SimCards), + PushToken: req.PushToken, + SimCards: h.simCardsToDomain(req.SimCards), + PublicKey: req.PublicKey, + KeyVersion: req.KeyVersion, }) if err != nil { return fmt.Errorf("failed to update device: %w", err) diff --git a/internal/sms-gateway/models/migrations/mysql/20260728000000_add_device_e2e_keys.sql b/internal/sms-gateway/models/migrations/mysql/20260728000000_add_device_e2e_keys.sql new file mode 100644 index 00000000..8aaacfa7 --- /dev/null +++ b/internal/sms-gateway/models/migrations/mysql/20260728000000_add_device_e2e_keys.sql @@ -0,0 +1,25 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE `devices` +ADD `public_key` text NULL; +-- +goose StatementEnd +-- +goose StatementBegin +ALTER TABLE `devices` +ADD `key_version` int NULL DEFAULT NULL; +-- +goose StatementEnd +-- +goose StatementBegin +ALTER TABLE `message_recipients` +MODIFY COLUMN `phone_number` varchar(512) NOT NULL; +-- +goose StatementEnd +--- +-- +goose Down +-- +goose StatementBegin +ALTER TABLE `message_recipients` +MODIFY COLUMN `phone_number` varchar(128) NOT NULL; +-- +goose StatementEnd +-- +goose StatementBegin +ALTER TABLE `devices` DROP `key_version`; +-- +goose StatementEnd +-- +goose StatementBegin +ALTER TABLE `devices` DROP `public_key`; +-- +goose StatementEnd \ No newline at end of file diff --git a/internal/sms-gateway/modules/devices/domain.go b/internal/sms-gateway/modules/devices/domain.go index b9c4de8e..e4697be7 100644 --- a/internal/sms-gateway/modules/devices/domain.go +++ b/internal/sms-gateway/modules/devices/domain.go @@ -1,6 +1,8 @@ package devices -import "time" +import ( + "time" +) type DeviceInput struct { DeviceInfo @@ -20,6 +22,15 @@ type DeviceInfo struct { type DeviceUpdate struct { PushToken *string SimCards []SimCard + // PublicKey is a base64-encoded RSA public key (nil if no E2E). + // Setting a new key together with KeyVersion overwrites the previous key; + // clearing an existing key is intentionally unsupported. On insert, nil + // means the device is created without E2E; on update, a both-nil pair is + // a no-op that leaves the existing key unchanged. + PublicKey *string + // KeyVersion is the key version used for rotation tracking (nil if no + // E2E). + KeyVersion *int } type Device struct { diff --git a/internal/sms-gateway/modules/devices/errors.go b/internal/sms-gateway/modules/devices/errors.go index 80e9e4e5..a3dba734 100644 --- a/internal/sms-gateway/modules/devices/errors.go +++ b/internal/sms-gateway/modules/devices/errors.go @@ -3,5 +3,7 @@ package devices import "errors" var ( - ErrInvalidUser = errors.New("invalid user") + ErrNotFound = errors.New("record not found") + ErrInvalidFilter = errors.New("invalid filter") + ErrMoreThanOne = errors.New("more than one record") ) diff --git a/internal/sms-gateway/modules/devices/models.go b/internal/sms-gateway/modules/devices/models.go index 5d720d7e..f36dcb1b 100644 --- a/internal/sms-gateway/modules/devices/models.go +++ b/internal/sms-gateway/modules/devices/models.go @@ -23,6 +23,9 @@ type DeviceModel struct { UserID string `gorm:"not null;type:varchar(32)"` SimCards datatypes.JSONSlice[simCardModel] `gorm:"serializer:json;type:json"` + + PublicKey *string `gorm:"type:text"` + KeyVersion *int `gorm:"default:null"` } func newDeviceModel(device DeviceInput) *DeviceModel { @@ -46,6 +49,9 @@ func newDeviceModel(device DeviceInput) *DeviceModel { device.SimCards, func(simCard SimCard, _ int) simCardModel { return newSimCardModel(simCard) }, ), + + PublicKey: device.PublicKey, + KeyVersion: device.KeyVersion, } } @@ -62,8 +68,10 @@ func (m *DeviceModel) toDomain() *Device { DeviceInput: DeviceInput{ DeviceInfo: DeviceInfo{ DeviceUpdate: DeviceUpdate{ - PushToken: m.PushToken, - SimCards: lo.Map(m.SimCards, func(m simCardModel, _ int) SimCard { return m.toDomain() }), + PushToken: m.PushToken, + SimCards: lo.Map(m.SimCards, func(m simCardModel, _ int) SimCard { return m.toDomain() }), + PublicKey: m.PublicKey, + KeyVersion: m.KeyVersion, }, Name: m.Name, diff --git a/internal/sms-gateway/modules/devices/repository.go b/internal/sms-gateway/modules/devices/repository.go index 55601ba6..80cdceb8 100644 --- a/internal/sms-gateway/modules/devices/repository.go +++ b/internal/sms-gateway/modules/devices/repository.go @@ -12,12 +12,6 @@ import ( "gorm.io/gorm" ) -var ( - ErrNotFound = errors.New("record not found") - ErrInvalidFilter = errors.New("invalid filter") - ErrMoreThanOne = errors.New("more than one record") -) - type Repository struct { db *gorm.DB } @@ -93,7 +87,7 @@ func (r *Repository) Update(ctx context.Context, id string, device DeviceUpdate) updates := map[string]any{} if device.PushToken != nil { - updates["push_token"] = device.PushToken + updates["push_token"] = lo.EmptyableToPtr(*device.PushToken) } if device.SimCards != nil { @@ -103,6 +97,14 @@ func (r *Repository) Update(ctx context.Context, id string, device DeviceUpdate) )) } + if device.PublicKey != nil { + updates["public_key"] = device.PublicKey + } + + if device.KeyVersion != nil { + updates["key_version"] = *device.KeyVersion + } + if len(updates) == 0 { return nil } diff --git a/internal/sms-gateway/openapi/docs.go b/internal/sms-gateway/openapi/docs.go index bee63823..96d0389b 100644 --- a/internal/sms-gateway/openapi/docs.go +++ b/internal/sms-gateway/openapi/docs.go @@ -1431,6 +1431,12 @@ const docTemplate = `{ "type": "string", "example": "PyDmBQZZXYmyxMwED8Fzy" }, + "keyVersion": { + "description": "Key version for rotation tracking", + "type": "integer", + "minimum": 1, + "example": 1 + }, "lastSeen": { "description": "Time at which the device was last seen, read only.", "type": "string", @@ -1441,6 +1447,11 @@ const docTemplate = `{ "type": "string", "example": "My Device" }, + "publicKey": { + "description": "Base64-encoded RSA public key for E2E encryption (nullable)", + "type": "string", + "example": "MIIBIjANBgkqh..." + }, "simCards": { "description": "List of SIM cards in the device.", "type": "array", @@ -2210,7 +2221,7 @@ const docTemplate = `{ "phoneNumber": { "description": "Phone number or first 16 symbols of SHA256 hash", "type": "string", - "maxLength": 128, + "maxLength": 512, "minLength": 1, "example": "79990001234" }, From a237aeae5cf24f1416184433082bdd208bdddeff Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Thu, 20 Aug 2026 09:20:04 +0700 Subject: [PATCH 2/2] [lint] ignore FCM Fid migration --- internal/sms-gateway/modules/push/fcm/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/sms-gateway/modules/push/fcm/client.go b/internal/sms-gateway/modules/push/fcm/client.go index d99a893a..a862421d 100644 --- a/internal/sms-gateway/modules/push/fcm/client.go +++ b/internal/sms-gateway/modules/push/fcm/client.go @@ -69,7 +69,7 @@ func (c *Client) Send(ctx context.Context, messages []client.Message) ([]error, Android: &messaging.AndroidConfig{ Priority: "high", }, - Token: message.Token, + Token: message.Token, //nolint:staticcheck //migration to Fid is planned }) if err != nil { errs[i] = fmt.Errorf("failed to send message: %w", err)