Skip to content

Commit

Permalink
[report] Initial commit of /report endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
joshlf committed May 8, 2020
1 parent 2874bab commit dcd7acc
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 1 deletion.
3 changes: 2 additions & 1 deletion app/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (

func main() {
funcframework.RegisterHTTPFunction("/challenge", app.ChallengeHandler)
// Use PORT environment variable, or default to 8080.
funcframework.RegisterHTTPFunction("/report", app.ReportHandler)
// Use PORT environment variable, or default to 8088.
port := "8088"
if envPort := os.Getenv("PORT"); envPort != "" {
port = envPort
Expand Down
98 changes: 98 additions & 0 deletions app/internal/report/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package report

import (
"encoding/binary"
"time"

"app/internal/util"
)

const (
// UploadKeyLen is the length, in bytes, of an UploadKey
UploadKeyLen = 16

pendingReportsCollection = "pending_reports"

// 3 days - the period of during which a token is valid and may be verified.
validityPeriod = time.Hour * 24 * 3
// 4 days - the period of time - after the validity period has expired -
// during which the token is still allocated in order to prevent
// reallocation.
allocationPeriod = time.Hour * 24 * 4
)

// An UploadKey is used to authorize the uploading of future reports.
type UploadKey [UploadKeyLen]byte

func genUploadKey() UploadKey {
var k UploadKey
util.ReadCryptoRandBytes(k[:])
return k
}

// The layout of a pending report document in the database.
type pendingReportDoc struct {
UploadKey UploadKey
// The 9-bit key from the upload token, used to hedge against human mistakes
// in transmitting or entering the upload token.
TokenKey uint16
ReportData []byte
// The time at which this document's upload token may no longer be
// validated. Note that after the validity has expired, the document is
// still not removed from the database until the allocation expiration has
// been reached. This is in order to prevent the following scenario, which
// would be possible if the two expirations were the same duration:
// 1. User uploads a report and gets an upload token.
// 2. User knows that they have a particular amount of time before their
// token will expire, so they wait until the last minute to call their
// health authority.
// 3. Token expires, and document is remvoed from the database.
// 4. A different user uploads a report, and is allocated the same token.
// 5. Original user calls health authority, and provides token.
// 6. Health authority verifies the original user's diagnosis, and verifies
// their token.
// 7. The new report - associated with a different user - is published.
//
// We expect that a user will not wait /that/ long after they know that
// their token has expired to try verifying their report with the health
// authority, and so adding a period of time after which the token can no
// longer be verified but during which the token cannot be re-allocated for
// a new report minimizes the likelihood of this mistake happening.
ValidityExpiration time.Time
// The time at which this document is removed from the database, and the
// token becomes available for allocation again.
AllocationExpiration time.Time
}

// StorePendingReport stores the given report in the database as pending. It
// allocates a new upload token and upload key, and returns them.
func StorePendingReport(ctx *util.Context, reportData []byte) (UploadToken, UploadKey, util.StatusError) {
now := time.Now()
validityExp := now.Add(validityPeriod)
allocationExp := validityExp.Add(allocationPeriod)
doc := pendingReportDoc{
UploadKey: genUploadKey(),
TokenKey: 0, // We'll explicitly set this below
ReportData: reportData,
ValidityExpiration: validityExp,
AllocationExpiration: allocationExp,
}

pendingReports := ctx.FirestoreClient().Collection(pendingReportsCollection)

// TODO(28): Implement a token allocation algorithm which guarantees that
// the numerically smallest unallocated token is always chosen.
//
// For the time being, we simply generate a random token and hope for the
// best. With 55 bits of entropy, it's very unlikely that this will ever be
// a problem during testing before we implement the final algorithm.
var bytes [8]byte
util.ReadCryptoRandBytes(bytes[:])
t := UploadToken{token: binary.BigEndian.Uint64(bytes[:])}
doc.TokenKey = t.key()
_, err := pendingReports.Doc(t.idString()).Create(ctx, doc)
if err != nil {
return UploadToken{}, UploadKey{}, util.NewInternalServerError(err)
}
return t, doc.UploadKey, nil
}
74 changes: 74 additions & 0 deletions app/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package app

import (
"encoding/json"
"errors"
"net/http"

"app/internal/pow"
"app/internal/report"
"app/internal/util"
)

// reportRequest is the body of a POST request to the /report endpoint.
type reportRequest struct {
// Must have exactly one of Challenge or UploadKey.
Challenge *pow.ChallengeSolution `json:"challenge"`
UploadKey *report.UploadKey `json:"upload_key"`
Report reportObj `json:"report"`
}

type reportObj struct {
Data []byte `json:"data"`
}

type reportResponse struct {
UploadToken report.UploadToken `json:"upload_token"`
UploadKey report.UploadKey `json:"upload_key"`
}

// ReportHandler is a handler for the /report endpoint.
func ReportHandler(w http.ResponseWriter, r *http.Request) {
ctx, err := util.NewContext(w, r)
if err != nil {
return
}

if err = util.ValidateRequestMethod(&ctx, "POST", ""); err != nil {
return
}

var req reportRequest
if err := json.NewDecoder(ctx.HTTPRequest().Body).Decode(&req); err != nil {
err := util.JSONToStatusError(err)
ctx.WriteStatusError(err)
return
}

switch {
case req.Challenge != nil && req.UploadKey != nil:
err := util.NewBadRequestError(errors.New("can only have proof of work challenge solution or upload key, not both"))
ctx.WriteStatusError(err)
case req.Challenge != nil:
if err := pow.ValidateSolution(&ctx, req.Challenge); err != nil {
ctx.WriteStatusError(err)
return
}

token, key, err := report.StorePendingReport(&ctx, req.Report.Data)
if err != nil {
ctx.WriteStatusError(err)
return
}

json.NewEncoder(w).Encode(reportResponse{
UploadToken: token,
UploadKey: key,
})
case req.UploadKey != nil:
// TODO(joshlf): Implement this case.
default:
err := util.NewBadRequestError(errors.New("missing proof of work challenge solution"))
ctx.WriteStatusError(err)
}
}

0 comments on commit dcd7acc

Please sign in to comment.