From 1c55e8fca7c792f6e737399469ac056da73d592a Mon Sep 17 00:00:00 2001 From: Kane York Date: Fri, 15 Sep 2017 16:40:40 -0700 Subject: [PATCH] Extract form sealing to a package --- socketserver/server/backend.go | 14 ++-- socketserver/server/backend_test.go | 4 +- socketserver/server/commands.go | 6 +- socketserver/server/handlecore.go | 2 +- socketserver/server/naclform/seal.go | 96 +++++++++++++++++++++++++++ socketserver/server/publisher.go | 8 +-- socketserver/server/testinfra_test.go | 8 +-- socketserver/server/types.go | 4 +- socketserver/server/utils.go | 94 -------------------------- 9 files changed, 119 insertions(+), 117 deletions(-) create mode 100644 socketserver/server/naclform/seal.go diff --git a/socketserver/server/backend.go b/socketserver/server/backend.go index 5bae575d..73b002c1 100644 --- a/socketserver/server/backend.go +++ b/socketserver/server/backend.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "github.com/FrankerFaceZ/FrankerFaceZ/socketserver/server/naclform" cache "github.com/patrickmn/go-cache" "golang.org/x/crypto/nacl/box" ) @@ -33,8 +34,7 @@ type backendInfo struct { addTopicURL string announceStartupURL string - sharedKey [32]byte - serverID int + secureForm naclform.ServerInfo lastSuccess map[string]time.Time lastSuccessLock sync.Mutex @@ -45,7 +45,7 @@ var Backend *backendInfo func setupBackend(config *ConfigFile) *backendInfo { b := new(backendInfo) Backend = b - b.serverID = config.ServerID + b.secureForm.ServerID = config.ServerID b.HTTPClient.Timeout = 60 * time.Second b.baseURL = config.BackendURL @@ -68,7 +68,7 @@ func setupBackend(config *ConfigFile) *backendInfo { copy(theirPublic[:], config.BackendPublicKey) copy(ourPrivate[:], config.OurPrivateKey) - box.Precompute(&b.sharedKey, &theirPublic, &ourPrivate) + box.Precompute(&b.secureForm.SharedKey, &theirPublic, &ourPrivate) return b } @@ -119,7 +119,7 @@ func (backend *backendInfo) SendRemoteCommand(remoteCommand, data string, auth A formData.Set("authenticated", "0") } - sealedForm, err := backend.SealRequest(formData) + sealedForm, err := backend.secureForm.Seal(formData) if err != nil { return "", err } @@ -171,7 +171,7 @@ func (backend *backendInfo) SendRemoteCommand(remoteCommand, data string, auth A // SendAggregatedData sends aggregated emote usage and following data to the backend server. func (backend *backendInfo) SendAggregatedData(form url.Values) error { - sealedForm, err := backend.SealRequest(form) + sealedForm, err := backend.secureForm.Seal(form) if err != nil { return err } @@ -228,7 +228,7 @@ func (backend *backendInfo) sendTopicNotice(topic string, added bool) error { formData.Set("added", "f") } - sealedForm, err := backend.SealRequest(formData) + sealedForm, err := backend.secureForm.Seal(formData) if err != nil { return err } diff --git a/socketserver/server/backend_test.go b/socketserver/server/backend_test.go index d1c85adb..0e9d2d6f 100644 --- a/socketserver/server/backend_test.go +++ b/socketserver/server/backend_test.go @@ -18,14 +18,14 @@ func TestSealRequest(t *testing.T) { "QuickBrownFox": []string{"LazyDog"}, } - sealedValues, err := b.SealRequest(values) + sealedValues, err := b.secureForm.Seal(values) if err != nil { t.Fatal(err) } // sealedValues.Encode() // id=0&msg=KKtbng49dOLLyjeuX5AnXiEe6P0uZwgeP_7mMB5vhP-wMAAPZw%3D%3D&nonce=-wRbUnifscisWUvhm3gBEXHN5QzrfzgV - unsealedValues, err := b.UnsealRequest(sealedValues) + unsealedValues, err := b.secureForm.Unseal(sealedValues) if err != nil { t.Fatal(err) } diff --git a/socketserver/server/commands.go b/socketserver/server/commands.go index 4d61a43b..64b1ddd7 100644 --- a/socketserver/server/commands.go +++ b/socketserver/server/commands.go @@ -184,7 +184,7 @@ func C2SPing(*websocket.Conn, *ClientInfo, ClientMessage) (ClientMessage, error) func C2SSetUser(_ *websocket.Conn, client *ClientInfo, msg ClientMessage) (ClientMessage, error) { username, err := msg.ArgumentsAsString() if err != nil { - return + return ClientMessage{}, err } username = copyString(username) @@ -221,7 +221,7 @@ func C2SReady(_ *websocket.Conn, client *ClientInfo, msg ClientMessage) (ClientM func C2SSubscribe(_ *websocket.Conn, client *ClientInfo, msg ClientMessage) (ClientMessage, error) { channel, err := msg.ArgumentsAsString() if err != nil { - return + return ClientMessage{}, err } channel = PubSubChannelPool.Intern(channel) @@ -248,7 +248,7 @@ func C2SSubscribe(_ *websocket.Conn, client *ClientInfo, msg ClientMessage) (Cli func C2SUnsubscribe(_ *websocket.Conn, client *ClientInfo, msg ClientMessage) (ClientMessage, error) { channel, err := msg.ArgumentsAsString() if err != nil { - return + return ClientMessage{}, err } channel = PubSubChannelPool.Intern(channel) diff --git a/socketserver/server/handlecore.go b/socketserver/server/handlecore.go index d23afb58..5dd515da 100644 --- a/socketserver/server/handlecore.go +++ b/socketserver/server/handlecore.go @@ -104,7 +104,7 @@ func SetupServerAndHandle(config *ConfigFile, serveMux *http.ServeMux) { serveMux.HandleFunc("/cached_pub", HTTPBackendCachedPublish) serveMux.HandleFunc("/get_sub_count", HTTPGetSubscriberCount) - announceForm, err := Backend.SealRequest(url.Values{ + announceForm, err := Backend.secureForm.Seal(url.Values{ "startup": []string{"1"}, }) if err != nil { diff --git a/socketserver/server/naclform/seal.go b/socketserver/server/naclform/seal.go new file mode 100644 index 00000000..ef21ae35 --- /dev/null +++ b/socketserver/server/naclform/seal.go @@ -0,0 +1,96 @@ +package naclform + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "errors" + "net/url" + "strconv" + "strings" + + "golang.org/x/crypto/nacl/box" +) + +var ErrorShortNonce = errors.New("Nonce too short.") +var ErrorInvalidSignature = errors.New("Invalid signature or contents") + +type ServerInfo struct { + SharedKey [32]byte + ServerID int +} + +func fillCryptoRandom(buf []byte) error { + remaining := len(buf) + for remaining > 0 { + count, err := rand.Read(buf) + if err != nil { + return err + } + remaining -= count + } + return nil +} + +func (i *ServerInfo) Seal(form url.Values) (url.Values, error) { + var nonce [24]byte + var err error + + err = fillCryptoRandom(nonce[:]) + if err != nil { + return nil, err + } + + cipherMsg := box.SealAfterPrecomputation(nil, []byte(form.Encode()), &nonce, &i.SharedKey) + + bufMessage := new(bytes.Buffer) + enc := base64.NewEncoder(base64.URLEncoding, bufMessage) + enc.Write(cipherMsg) + enc.Close() + cipherString := bufMessage.String() + + bufNonce := new(bytes.Buffer) + enc = base64.NewEncoder(base64.URLEncoding, bufNonce) + enc.Write(nonce[:]) + enc.Close() + nonceString := bufNonce.String() + + retval := url.Values{ + "nonce": []string{nonceString}, + "msg": []string{cipherString}, + "id": []string{strconv.Itoa(i.ServerID)}, + } + + return retval, nil +} + +func (i *ServerInfo) Unseal(form url.Values) (url.Values, error) { + var nonce [24]byte + + nonceString := form.Get("nonce") + dec := base64.NewDecoder(base64.URLEncoding, strings.NewReader(nonceString)) + count, err := dec.Read(nonce[:]) + if err != nil { + return nil, err + } + if count != 24 { + return nil, ErrorShortNonce + } + + cipherString := form.Get("msg") + dec = base64.NewDecoder(base64.URLEncoding, strings.NewReader(cipherString)) + cipherBuffer := new(bytes.Buffer) + cipherBuffer.ReadFrom(dec) + + message, ok := box.OpenAfterPrecomputation(nil, cipherBuffer.Bytes(), &nonce, &i.SharedKey) + if !ok { + return nil, ErrorInvalidSignature + } + + retValues, err := url.ParseQuery(string(message)) + if err != nil { + return nil, ErrorInvalidSignature + } + + return retValues, nil +} diff --git a/socketserver/server/publisher.go b/socketserver/server/publisher.go index a1815a63..669aa727 100644 --- a/socketserver/server/publisher.go +++ b/socketserver/server/publisher.go @@ -123,7 +123,7 @@ func saveLastMessage(cmd Command, channel string, expires time.Time, data string func HTTPBackendDropBacklog(w http.ResponseWriter, r *http.Request) { r.ParseForm() - formData, err := Backend.UnsealRequest(r.Form) + formData, err := Backend.secureForm.Unseal(r.Form) if err != nil { w.WriteHeader(403) fmt.Fprintf(w, "Error: %v", err) @@ -160,7 +160,7 @@ func rateLimitFromRequest(r *http.Request) (rate.Limiter, error) { // If the 'expires' parameter is not specified, the message will not expire (though it is only kept in-memory). func HTTPBackendCachedPublish(w http.ResponseWriter, r *http.Request) { r.ParseForm() - formData, err := Backend.UnsealRequest(r.Form) + formData, err := Backend.secureForm.Unseal(r.Form) if err != nil { w.WriteHeader(403) fmt.Fprintf(w, "Error: %v", err) @@ -227,7 +227,7 @@ func HTTPBackendCachedPublish(w http.ResponseWriter, r *http.Request) { // If "scope" is "global", then "channel" is not used. func HTTPBackendUncachedPublish(w http.ResponseWriter, r *http.Request) { r.ParseForm() - formData, err := Backend.UnsealRequest(r.Form) + formData, err := Backend.secureForm.Unseal(r.Form) if err != nil { w.WriteHeader(403) fmt.Fprintf(w, "Error: %v", err) @@ -292,7 +292,7 @@ func HTTPBackendUncachedPublish(w http.ResponseWriter, r *http.Request) { // A "global" option is not available, use fetch(/stats).CurrentClientCount instead. func HTTPGetSubscriberCount(w http.ResponseWriter, r *http.Request) { r.ParseForm() - formData, err := Backend.UnsealRequest(r.Form) + formData, err := Backend.secureForm.Unseal(r.Form) if err != nil { w.WriteHeader(403) fmt.Fprintf(w, "Error: %v", err) diff --git a/socketserver/server/testinfra_test.go b/socketserver/server/testinfra_test.go index 5d8a4570..529214d9 100644 --- a/socketserver/server/testinfra_test.go +++ b/socketserver/server/testinfra_test.go @@ -97,7 +97,7 @@ func (er *TExpectedBackendRequest) String() string { if MethodIsPost == "" { return er.Path } - return fmt.Sprint("%s %s: %s", MethodIsPost, er.Path, er.PostForm.Encode()) + return fmt.Sprintf("%s %s: %s", MethodIsPost, er.Path, er.PostForm.Encode()) } type TBackendRequestChecker struct { @@ -123,7 +123,7 @@ func (backend *TBackendRequestChecker) ServeHTTP(w http.ResponseWriter, r *http. r.ParseForm() - unsealedForm, err := Backend.UnsealRequest(r.PostForm) + unsealedForm, err := Backend.secureForm.Unseal(r.PostForm) if err != nil { backend.tb.Errorf("Failed to unseal backend request: %v", err) } @@ -276,7 +276,7 @@ func TSealForSavePubMsg(tb testing.TB, cmd Command, channel string, arguments in } form.Set("time", strconv.FormatInt(time.Now().Unix(), 10)) - sealed, err := Backend.SealRequest(form) + sealed, err := Backend.secureForm.Seal(form) if err != nil { tb.Error(err) return nil, err @@ -300,7 +300,7 @@ func TSealForUncachedPubMsg(tb testing.TB, cmd Command, channel string, argument form.Set("time", time.Now().Format(time.UnixDate)) form.Set("scope", scope) - sealed, err := Backend.SealRequest(form) + sealed, err := Backend.secureForm.Seal(form) if err != nil { tb.Error(err) return nil, err diff --git a/socketserver/server/types.go b/socketserver/server/types.go index 3812b045..3de13127 100644 --- a/socketserver/server/types.go +++ b/socketserver/server/types.go @@ -64,7 +64,7 @@ type ClientMessage struct { origArguments string } -func (cm ClientMessage) Reply(cmd string, args interface{}) ClientMessage { +func (cm ClientMessage) Reply(cmd Command, args interface{}) ClientMessage { return ClientMessage{ MessageID: cm.MessageID, Command: cmd, @@ -72,7 +72,7 @@ func (cm ClientMessage) Reply(cmd string, args interface{}) ClientMessage { } } -func (cm ClientMessage) ReplyJSON(cmd string, argsJSON string) ClientMessage { +func (cm ClientMessage) ReplyJSON(cmd Command, argsJSON string) ClientMessage { n := ClientMessage{ MessageID: cm.MessageID, Command: cmd, diff --git a/socketserver/server/utils.go b/socketserver/server/utils.go index a91feda4..cbbd760f 100644 --- a/socketserver/server/utils.go +++ b/socketserver/server/utils.go @@ -1,103 +1,9 @@ package server -import ( - "bytes" - "crypto/rand" - "encoding/base64" - "errors" - "net/url" - "strconv" - "strings" - - "golang.org/x/crypto/nacl/box" -) - -func FillCryptoRandom(buf []byte) error { - remaining := len(buf) - for remaining > 0 { - count, err := rand.Read(buf) - if err != nil { - return err - } - remaining -= count - } - return nil -} - func copyString(s string) string { return string([]byte(s)) } -func (backend *backendInfo) SealRequest(form url.Values) (url.Values, error) { - var nonce [24]byte - var err error - - err = FillCryptoRandom(nonce[:]) - if err != nil { - return nil, err - } - - cipherMsg := box.SealAfterPrecomputation(nil, []byte(form.Encode()), &nonce, &backend.sharedKey) - - bufMessage := new(bytes.Buffer) - enc := base64.NewEncoder(base64.URLEncoding, bufMessage) - enc.Write(cipherMsg) - enc.Close() - cipherString := bufMessage.String() - - bufNonce := new(bytes.Buffer) - enc = base64.NewEncoder(base64.URLEncoding, bufNonce) - enc.Write(nonce[:]) - enc.Close() - nonceString := bufNonce.String() - - retval := url.Values{ - "nonce": []string{nonceString}, - "msg": []string{cipherString}, - "id": []string{strconv.Itoa(Backend.serverID)}, - } - - return retval, nil -} - -var ErrorShortNonce = errors.New("Nonce too short.") -var ErrorInvalidSignature = errors.New("Invalid signature or contents") - -func (backend *backendInfo) UnsealRequest(form url.Values) (url.Values, error) { - var nonce [24]byte - - nonceString := form.Get("nonce") - dec := base64.NewDecoder(base64.URLEncoding, strings.NewReader(nonceString)) - count, err := dec.Read(nonce[:]) - if err != nil { - Statistics.BackendVerifyFails++ - return nil, err - } - if count != 24 { - Statistics.BackendVerifyFails++ - return nil, ErrorShortNonce - } - - cipherString := form.Get("msg") - dec = base64.NewDecoder(base64.URLEncoding, strings.NewReader(cipherString)) - cipherBuffer := new(bytes.Buffer) - cipherBuffer.ReadFrom(dec) - - message, ok := box.OpenAfterPrecomputation(nil, cipherBuffer.Bytes(), &nonce, &backend.sharedKey) - if !ok { - Statistics.BackendVerifyFails++ - return nil, ErrorInvalidSignature - } - - retValues, err := url.ParseQuery(string(message)) - if err != nil { - Statistics.BackendVerifyFails++ - return nil, ErrorInvalidSignature - } - - return retValues, nil -} - func AddToSliceS(ary *[]string, val string) bool { slice := *ary for _, v := range slice {