package main import ( "bytes" "crypto/hmac" "crypto/sha1" "encoding/hex" "encoding/json" "errors" "log/slog" "net/http" "net/url" "os" ) // regEndpoint is the endpoint for shared secret registration const regEndpoint = "/_synapse/admin/v1/register" // RegistrationReq represents the JSON body of a registration request type RegistrationReq struct { Nonce string `json:"nonce"` Username string `json:"username"` DisplayName string `json:"displayname"` Password string `json:"password"` Admin bool `json:"admin"` MAC string `json:"mac"` } // registerUser registers the given user with the matrix server func registerUser(username, displayName, password string, u url.URL) error { u.Path = regEndpoint req, err := generateRegRequest(username, displayName, password, u) if err != nil { return err } data, err := json.Marshal(req) if err != nil { return err } log.Info("Issuing registration request", slog.String("username", req.Username), slog.String("nonce", req.Nonce), slog.String("mac", req.MAC), ) res, err := http.Post(u.String(), "application/json", bytes.NewReader(data)) if err != nil { return err } defer res.Body.Close() if res.StatusCode != 200 { var errRes struct { Message string `json:"message"` ErrCode string `json:"errcode"` Error string `json:"error"` } err = json.NewDecoder(res.Body).Decode(&errRes) if err != nil || (errRes.Message == "" && errRes.Error == "") { return errors.New("http: " + res.Status) } else if errRes.Error != "" { return errors.New(u.Host + ": " + errRes.ErrCode + ": " + errRes.Error) } else { return errors.New(u.Host + ": " + errRes.Message) } } return nil } // generateRegRequest generates the JSON struct that will serve as the body of a use registration request func generateRegRequest(username, displayName, password string, u url.URL) (*RegistrationReq, error) { secret := os.Getenv("SHARED_SECRET") nonce, err := getNonce(u) if err != nil { return nil, err } mac, err := calculateMAC(secret, username, password, nonce) if err != nil { return nil, err } return &RegistrationReq{ nonce, username, displayName, password, false, mac, }, nil } // getNonce requests and returns a nonce from the matrix server func getNonce(u url.URL) (string, error) { u.Path = regEndpoint res, err := http.Get(u.String()) if err != nil { return "", err } defer res.Body.Close() if res.StatusCode != 200 { return "", errors.New("http: " + res.Status) } var resp struct { Nonce string `json:"nonce"` } err = json.NewDecoder(res.Body).Decode(&resp) if err != nil { return "", err } return resp.Nonce, nil } // calculateMAC calculates a returns an HMAC-SHA1 for a new non-admin user func calculateMAC(secret, username, password, nonce string) (string, error) { h := hmac.New(sha1.New, []byte(secret)) b := &bytes.Buffer{} b.WriteString(nonce) b.WriteByte(0) b.WriteString(username) b.WriteByte(0) b.WriteString(password) b.WriteByte(0) b.WriteString("notadmin") b.WriteTo(h) return hex.EncodeToString(h.Sum(nil)), nil }