Skip to content

Commit

Permalink
Implement client pairing protocol
Browse files Browse the repository at this point in the history
Also fixes some mistakes in the DB schema
and in the model (table names were wrong)
  • Loading branch information
gbl08ma committed Jul 14, 2017
1 parent 36bca37 commit b07c8ae
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 18 deletions.
26 changes: 25 additions & 1 deletion api.go
@@ -1,8 +1,14 @@
package main

import (
"encoding/pem"
"net/http"

"crypto/x509"
"io/ioutil"

"crypto/ecdsa"

"github.com/gbl08ma/disturbancesmlx/resource"
"github.com/yarf-framework/yarf"
)
Expand All @@ -27,7 +33,7 @@ func (r *Static) Get(c *yarf.Context) error {
return nil
}

func APIserver() {
func APIserver(trustedClientCertPath string) {
y := yarf.New()

v1 := yarf.RouteGroup("/v1")
Expand Down Expand Up @@ -59,8 +65,26 @@ func APIserver() {

v1.Add("/stationkb/*", new(Static).WithPath("stationkb/", "/v1/stationkb/"))

pubkey := getTrustedClientPublicKey(trustedClientCertPath)

v1.Add("/pair", new(resource.Pair).WithNode(rootSqalxNode).WithPublicKey(pubkey))

y.AddGroup(v1)

y.Logger = webLog
y.Start(":12000")
}

func getTrustedClientPublicKey(trustedClientCertPath string) *ecdsa.PublicKey {
certBytes, err := ioutil.ReadFile(trustedClientCertPath)
if err != nil {
panic("Error reading trusted client certificate")
}
block, _ := pem.Decode([]byte(certBytes))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
panic("Error parsing client certificate: " + err.Error())
}

return cert.PublicKey.(*ecdsa.PublicKey)
}
49 changes: 35 additions & 14 deletions dataobjects/androidpairrequest.go
Expand Up @@ -29,7 +29,7 @@ func getAndroidPairRequestsWithSelect(node sqalx.Node, sbuilder sq.SelectBuilder
defer tx.Commit() // read-only tx

rows, err := sbuilder.Columns("nonce", "request_time", "android_id", "ip_address").
From("line_disturbance").
From("android_pair_request").
RunWith(tx).Query()
if err != nil {
return requests, fmt.Errorf("getAndroidPairRequestsWithSelect: %s", err)
Expand Down Expand Up @@ -78,7 +78,7 @@ func NewAndroidPairRequest(nonce string, androidID string, ipAddress net.IP) *An
func (request *AndroidPairRequest) Store(node sqalx.Node) error {
tx, err := node.Beginx()
if err != nil {
return errors.New("NewAndroidPairRequest: " + err.Error())
return errors.New("Store: " + err.Error())
}
defer tx.Rollback()

Expand All @@ -91,11 +91,11 @@ func (request *AndroidPairRequest) Store(node sqalx.Node) error {
RunWith(tx).Exec()

if err != nil {
return errors.New("NewAndroidPairRequest: " + err.Error())
return errors.New("Store: " + err.Error())
}
err = tx.Commit()
if err != nil {
return errors.New("NewAndroidPairRequest: " + err.Error())
return errors.New("Store: " + err.Error())
}
return nil
}
Expand All @@ -118,52 +118,73 @@ func (request *AndroidPairRequest) CalculateActivationTime(node sqalx.Node, maxT
})

requests, err := getAndroidPairRequestsWithSelect(tx, s)
if err != nil {
return time.Time{}, err
}

activation := time.Now().UTC()
for _, pastRequest := range requests {
// let's find out in which way this one is bad
diff := request.RequestTime.Sub(pastRequest.RequestTime)
diff = maxDuration(diff, -diff)

if request.Nonce == pastRequest.Nonce {
if diff < maxTimestampSkew {
// nope, this nonce was used too recently
return time.Time{}, nil
}
}

if request.AndroidID == pastRequest.AndroidID {
switch {
case diff < 5*time.Minute:
activation.Add(2 * time.Hour)
activation = activation.Add(2 * time.Hour)
case diff < 30*time.Minute:
activation.Add(1 * time.Hour)
activation = activation.Add(1 * time.Hour)
case diff < 2*time.Hour:
activation.Add(30 * time.Minute)
activation = activation.Add(30 * time.Minute)
case diff < 24*time.Hour:
activation.Add(10 * time.Minute)
activation = activation.Add(10 * time.Minute)
default:
// probably an honest reinstall. Penalize just a bit
activation.Add(1 * time.Minute)
activation = activation.Add(1 * time.Minute)
}
}

if request.IPaddress.Equal(pastRequest.IPaddress) {
switch {
case diff < 5*time.Minute:
activation.Add(1 * time.Hour)
activation = activation.Add(1 * time.Hour)
case diff < 30*time.Minute:
activation.Add(30 * time.Minute)
activation = activation.Add(30 * time.Minute)
case diff < 2*time.Hour:
activation.Add(15 * time.Minute)
activation = activation.Add(15 * time.Minute)
case diff < 24*time.Hour:
activation.Add(5 * time.Hour)
activation = activation.Add(5 * time.Minute)
default:
// probably just two people installing from the same location
// or a user with two devices
activation.Add(1 * time.Minute)
activation = activation.Add(1 * time.Minute)
}
}
}
if activation.Sub(time.Now().UTC()) > 24*time.Hour {
// don't make anyone wait more than 24 hours

// TODO FIXME this makes the whole thing mostly pointless
// an attacker can just "spawn" a thousand devices
// (and they can even all have the same IP address and Android ID)
// despite the fact that the activation time should then be extremely high,
// since we're setting this upper bound, the attacker can just wait 24 hours
// and by then, all of the attacker's pairs will be activated
// without this upper bound, if someone requests a thousand pairings from the same IP
// they'll have to wait 1000 hours

// situations where IP addresses might not be so unique:
// - people using the free Go Wifi at subway stations (whoops)

// conclusion:
// remove this check once we're sure IP addresses and Android IDs are unique enough
activation = time.Now().UTC().Add(24 * time.Hour)
}
return activation, nil
Expand Down
7 changes: 7 additions & 0 deletions genkey.sh
@@ -0,0 +1,7 @@
#!/bin/sh

openssl ecparam -genkey -name secp521r1 -noout -out privkey1.pem
openssl pkcs8 -topk8 -nocrypt -in privkey1.pem -outform DER -out trusted_client_priv.der
openssl req -new -x509 -key privkey1.pem -out trusted_client_cert.pem -days 36500
rm privkey1.pem

7 changes: 6 additions & 1 deletion main.go
Expand Up @@ -247,7 +247,12 @@ func main() {
defer mlxscr.End()

go WebServer()
go APIserver()

certPath := "trusted_client_cert.pem"
if len(os.Args) > 1 {
certPath = os.Args[1]
}
go APIserver(certPath)

fcmServerKey, present := secrets.Get("firebaseServerKey")
if !present {
Expand Down
32 changes: 32 additions & 0 deletions resource/helpers.go
@@ -1,7 +1,10 @@
package resource

import (
"encoding/json"
"net/http"
"strings"
"time"

msgpack "gopkg.in/vmihailenco/msgpack.v2"

Expand All @@ -21,6 +24,28 @@ func (r *resource) Beginx() (sqalx.Node, error) {
return r.node.Beginx()
}

func (r *resource) DecodeRequest(c *yarf.Context, v interface{}) error {
contentType := c.Request.Header.Get("Content-Type")
var err error
switch {
case strings.Contains(contentType, "msgpack"):
err = msgpack.NewDecoder(c.Request.Body).Decode(v)
case strings.Contains(contentType, "json"):
default:
err = json.NewDecoder(c.Request.Body).Decode(v)
}

if err != nil {
return &yarf.CustomError{
HTTPCode: http.StatusBadRequest,
ErrorMsg: "Failed to decode request",
ErrorBody: err.Error(),
}
}
return nil

}

// RenderData takes a interface{} object and writes the encoded representation of it.
// Encoding used will be idented JSON, non-idented JSON, Msgpack or XML
func RenderData(c *yarf.Context, data interface{}) {
Expand Down Expand Up @@ -52,3 +77,10 @@ func RenderMsgpack(c *yarf.Context, data interface{}) {
c.Response.Write(encoded)
}
}

func maxDuration(d1 time.Duration, d2 time.Duration) time.Duration {
if d1 > d2 {
return d1
}
return d2
}

0 comments on commit b07c8ae

Please sign in to comment.