diff --git a/src/api/account_identifier_test.go b/src/api/account_identifier_test.go index 2464d3d..cbf3884 100644 --- a/src/api/account_identifier_test.go +++ b/src/api/account_identifier_test.go @@ -55,3 +55,20 @@ func TestAccountIdentifierFromParam(t *testing.T) { } }) } + +// An ACI has no phone number to send a code to, so it can only be registered with a recovery key and never verified. +func TestRegisterByACIRequiresRecoveryKey(t *testing.T) { + gin.SetMode(gin.TestMode) + aci := gin.Params{{Key: "number", Value: "11111111-1111-4111-8111-111111111111"}, {Key: "token", Value: "123456"}} + + for name, handler := range map[string]gin.HandlerFunc{"register": NewApi(nil).RegisterNumber, "verify": NewApi(nil).VerifyRegisteredNumber} { + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = aci + c.Request = httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}")) + handler(c) + if w.Code != 400 || !strings.Contains(w.Body.String(), "recovery_key") { + t.Fatalf("%s: status = %d, body %s", name, w.Code, w.Body.String()) + } + } +} diff --git a/src/api/api.go b/src/api/api.go index 1ab2478..8062d4a 100644 --- a/src/api/api.go +++ b/src/api/api.go @@ -84,8 +84,11 @@ type Configuration struct { } type RegisterNumberRequest struct { - UseVoice bool `json:"use_voice,omitempty"` - Captcha string `json:"captcha,omitempty"` + UseVoice bool `json:"use_voice,omitempty"` + Captcha string `json:"captcha,omitempty"` + RecoveryKey string `json:"recovery_key,omitempty"` // 64-character Recovery Key shown by Signal Android. Recovers the account without SMS verification. + Totp string `json:"totp,omitempty"` // Six-digit TOTP token, only needed if recovery asks for it. + Reregister bool `json:"reregister,omitempty"` // Register even if the account is still registered locally. } type UnregisterNumberRequest struct { @@ -290,7 +293,7 @@ func (a *Api) About(c *gin.Context) { // @Summary Register a phone number. // @Tags Devices -// @Description Register a phone number with the signal network. +// @Description Register a phone number with the signal network. An existing account can instead be recovered with its Recovery Key (recovery_key), in which case no verification is needed. Accounts without a phone number can only be recovered this way, using their ACI (Account Key) as identifier. If the response asks for a TOTP token, repeat the request with totp. // @Accept json // @Produce json // @Success 201 @@ -299,7 +302,7 @@ func (a *Api) About(c *gin.Context) { // @Param data body RegisterNumberRequest false "Additional Settings" // @Router /v1/register/{number} [post] func (a *Api) RegisterNumber(c *gin.Context) { - _, number, err := accountIdentifierFromParam(c) + typ, number, err := accountIdentifierFromParam(c) if err != nil { c.JSON(400, Error{Msg: err.Error()}) return @@ -326,7 +329,13 @@ func (a *Api) RegisterNumber(c *gin.Context) { return } - err = a.signalClient.RegisterNumber(number, req.UseVoice, req.Captcha) + // without a phone number there is nothing to send a verification code to + if typ == utils.IdentifierACI && req.RecoveryKey == "" { + c.JSON(400, Error{Msg: "Couldn't process request - registering by ACI requires a recovery_key"}) + return + } + + err = a.signalClient.RegisterNumber(number, req.UseVoice, req.Captcha, req.RecoveryKey, req.Totp, req.Reregister) if err != nil { switch err.(type) { case *client.InvalidTransportError: @@ -430,11 +439,15 @@ func (a *Api) DeleteLocalAccountData(c *gin.Context) { // @Param token path string true "Verification Code" // @Router /v1/register/{number}/verify/{token} [post] func (a *Api) VerifyRegisteredNumber(c *gin.Context) { - _, number, err := accountIdentifierFromParam(c) + typ, number, err := accountIdentifierFromParam(c) if err != nil { c.JSON(400, Error{Msg: err.Error()}) return } + if typ == utils.IdentifierACI { + c.JSON(400, Error{Msg: "Couldn't process request - only phone numbers can be verified, accounts registered with a recovery_key need no verification"}) + return + } token := c.Param("token") pin := "" diff --git a/src/client/client.go b/src/client/client.go index e28da34..e5de145 100644 --- a/src/client/client.go +++ b/src/client/client.go @@ -774,16 +774,21 @@ func (s *SignalClient) About() About { return about } -func (s *SignalClient) RegisterNumber(number string, useVoice bool, captcha string) error { +// RegisterNumber registers an account. With a recoveryKey an existing account is recovered instead +// (no verification needed), which is the only way to register an account without a phone number. +func (s *SignalClient) RegisterNumber(number string, useVoice bool, captcha string, recoveryKey string, totp string, reregister bool) error { var err error var jsonRpc2Client *JsonRpc2Client if s.signalCliMode == JsonRpc { type Request struct { - UseVoice bool `json:"voice,omitempty"` - Captcha string `json:"captcha,omitempty"` - Account string `json:"account,omitempty"` + UseVoice bool `json:"voice,omitempty"` + Captcha string `json:"captcha,omitempty"` + Account string `json:"account,omitempty"` + RecoveryKey string `json:"recoveryKey,omitempty"` + Totp string `json:"totp,omitempty"` + Reregister bool `json:"reregister,omitempty"` } - request := Request{Account: number} + request := Request{Account: number, RecoveryKey: recoveryKey, Totp: totp, Reregister: reregister} if useVoice { request.UseVoice = useVoice @@ -809,6 +814,18 @@ func (s *SignalClient) RegisterNumber(number string, useVoice bool, captcha stri command = append(command, []string{"--captcha", captcha}...) } + if recoveryKey != "" { + command = append(command, []string{"--recovery-key", recoveryKey}...) + } + + if totp != "" { + command = append(command, []string{"--totp", totp}...) + } + + if reregister { + command = append(command, "--reregister") + } + _, err = s.cliClient.Execute(true, command, "") } diff --git a/src/docs/docs.go b/src/docs/docs.go index db26180..5864bf2 100644 --- a/src/docs/docs.go +++ b/src/docs/docs.go @@ -282,6 +282,18 @@ const docTemplate = `{ "captcha": { "type": "string" }, + "recovery_key": { + "description": "64-character Recovery Key shown by Signal Android. Recovers the account without SMS verification.", + "type": "string" + }, + "reregister": { + "description": "Register even if the account is still registered locally.", + "type": "boolean" + }, + "totp": { + "description": "Six-digit TOTP token, only needed if recovery asks for it.", + "type": "string" + }, "use_voice": { "type": "boolean" } @@ -4656,7 +4668,7 @@ const docTemplate = `{ "consumes": [ "application/json" ], - "description": "Register a phone number with the signal network.", + "description": "Register a phone number with the signal network. An existing account can instead be recovered with its Recovery Key (recovery_key), in which case no verification is needed. Accounts without a phone number can only be recovered this way, using their ACI (Account Key) as identifier. If the response asks for a TOTP token, repeat the request with totp.", "parameters": [ { "description": "Account identifier (E.164 phone number or ACI UUID)", diff --git a/src/docs/swagger.json b/src/docs/swagger.json index c8e910b..aaf7527 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -277,6 +277,18 @@ "captcha": { "type": "string" }, + "recovery_key": { + "description": "64-character Recovery Key shown by Signal Android. Recovers the account without SMS verification.", + "type": "string" + }, + "reregister": { + "description": "Register even if the account is still registered locally.", + "type": "boolean" + }, + "totp": { + "description": "Six-digit TOTP token, only needed if recovery asks for it.", + "type": "string" + }, "use_voice": { "type": "boolean" } @@ -4651,7 +4663,7 @@ "consumes": [ "application/json" ], - "description": "Register a phone number with the signal network.", + "description": "Register a phone number with the signal network. An existing account can instead be recovered with its Recovery Key (recovery_key), in which case no verification is needed. Accounts without a phone number can only be recovered this way, using their ACI (Account Key) as identifier. If the response asks for a TOTP token, repeat the request with totp.", "parameters": [ { "description": "Account identifier (E.164 phone number or ACI UUID)",