A fork of the Cocoon PDS but being made more distributed.
0

Configure Feed

Select the types of activity you want to include in your feed.

create a totp style nonce generator

Signed-off-by: Will <did:plc:dadhhalkfcq3gucaq25hjqon>

author willdot.net committer
Tangled
date (Jun 1, 2026, 9:32 PM UTC) commit 81d11c13 parent a214086a change-id kstuunxy
+194 -9
+6
cmd/cocoon/main.go
··· 177 177 Name: "push-based-events", 178 178 EnvVars: []string{"PUSH_BASED_EVENTS"}, 179 179 }, 180 + &cli.StringFlag{ 181 + Name: "nonce-secret", 182 + Usage: "To set a nonce secret", 183 + EnvVars: []string{"COCOON_NONCE_SECRET"}, 184 + }, 180 185 }, 181 186 Commands: []*cli.Command{ 182 187 runServe, ··· 241 246 Relays: cmd.StringSlice("relays"), 242 247 AdminPassword: cmd.String("admin-password"), 243 248 RequireInvite: cmd.Bool("require-invite"), 249 + NonceSecret: cmd.String("nonce-secret"), 244 250 SmtpUser: cmd.String("smtp-user"), 245 251 SmtpPass: cmd.String("smtp-pass"), 246 252 SmtpHost: cmd.String("smtp-host"),
+4
go.mod
··· 29 29 github.com/multiformats/go-multihash v0.2.3 30 30 github.com/prometheus/client_golang v1.23.2 31 31 github.com/samber/slog-echo v1.16.1 32 + github.com/stretchr/testify v1.11.1 32 33 github.com/tursodatabase/libsql-client-go v0.0.0-20251219100830-236aa1ff8acc 33 34 github.com/urfave/cli/v2 v2.27.6 34 35 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e ··· 47 48 github.com/cespare/xxhash/v2 v2.3.0 // indirect 48 49 github.com/coder/websocket v1.8.12 // indirect 49 50 github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 51 + github.com/davecgh/go-spew v1.1.1 // indirect 50 52 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 51 53 github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 52 54 github.com/felixge/httpsnoop v1.0.4 // indirect ··· 107 109 github.com/multiformats/go-varint v0.0.7 // indirect 108 110 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 109 111 github.com/opentracing/opentracing-go v1.2.0 // indirect 112 + github.com/pmezard/go-difflib v1.0.0 // indirect 110 113 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 111 114 github.com/prometheus/client_model v0.6.2 // indirect 112 115 github.com/prometheus/common v0.66.1 // indirect ··· 137 140 google.golang.org/protobuf v1.36.9 // indirect 138 141 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 139 142 gopkg.in/inf.v0 v0.9.1 // indirect 143 + gopkg.in/yaml.v3 v3.0.1 // indirect 140 144 lukechampine.com/blake3 v1.2.1 // indirect 141 145 )
+7 -5
oauth/dpop/manager.go
··· 21 21 ) 22 22 23 23 type Manager struct { 24 - nonce *Nonce 24 + nonce *TotpNonce 25 25 jtiCache *jtiCache 26 26 logger *slog.Logger 27 27 hostname string ··· 54 54 } 55 55 56 56 return &Manager{ 57 - nonce: NewNonce(NonceArgs{ 58 - RotationInterval: args.NonceRotationInterval, 59 - Secret: args.NonceSecret, 60 - OnSecretCreated: args.OnNonceSecretCreated, 57 + nonce: NewTotpNonce(TotpNonceArgs{ 58 + // TODO: pass this in from the args 59 + // timeRoundDuration: args.NonceRotationInterval, 60 + timeRoundDuration: time.Minute * 2, 61 + Secret: args.NonceSecret, 62 + OnSecretCreated: args.OnNonceSecretCreated, 61 63 }), 62 64 jtiCache: newJTICache(args.JTICacheSize), 63 65 logger: args.Logger,
+99
oauth/dpop/nonce_totp.go
··· 1 + package dpop 2 + 3 + import ( 4 + "crypto/hmac" 5 + "crypto/sha256" 6 + "encoding/base64" 7 + "encoding/binary" 8 + "sync" 9 + "time" 10 + 11 + "github.com/haileyok/cocoon/internal/helpers" 12 + "github.com/haileyok/cocoon/oauth/constants" 13 + ) 14 + 15 + type TotpNonce struct { 16 + secret []byte 17 + 18 + mu sync.RWMutex 19 + 20 + currentTimePeriodStart time.Time 21 + timeRoundDuration time.Duration 22 + 23 + prev string 24 + curr string 25 + next string 26 + } 27 + 28 + type TotpNonceArgs struct { 29 + timeRoundDuration time.Duration 30 + Secret []byte 31 + OnSecretCreated func([]byte) 32 + } 33 + 34 + func NewTotpNonce(args TotpNonceArgs) *TotpNonce { 35 + if args.timeRoundDuration == 0 { 36 + args.timeRoundDuration = time.Minute * 2 37 + } 38 + 39 + if args.timeRoundDuration > constants.DpopNonceMaxAge { 40 + args.timeRoundDuration = constants.DpopNonceMaxAge 41 + } 42 + 43 + if args.Secret == nil { 44 + args.Secret = helpers.RandomBytes(constants.NonceSecretByteLength) 45 + args.OnSecretCreated(args.Secret) 46 + } 47 + 48 + n := &TotpNonce{ 49 + secret: args.Secret, 50 + mu: sync.RWMutex{}, 51 + timeRoundDuration: time.Minute * 15, 52 + } 53 + 54 + n.currentTimePeriodStart = time.Now().Truncate(n.timeRoundDuration) 55 + n.prev = n.compute(n.currentTimePeriodStart.Add(-n.timeRoundDuration)) 56 + n.curr = n.compute(n.currentTimePeriodStart) 57 + n.next = n.compute(n.currentTimePeriodStart.Add(n.timeRoundDuration)) 58 + 59 + return n 60 + } 61 + 62 + func (n *TotpNonce) currentTruncatedTime(now time.Time) time.Time { 63 + return now.Truncate(n.timeRoundDuration) 64 + } 65 + 66 + func (n *TotpNonce) compute(ti time.Time) string { 67 + h := hmac.New(sha256.New, n.secret) 68 + unixBytes := make([]byte, 8) 69 + binary.BigEndian.PutUint64(unixBytes, uint64(ti.UnixNano())) 70 + h.Write(unixBytes) 71 + return base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 72 + } 73 + 74 + func (n *TotpNonce) rotate(now time.Time) { 75 + currentTruncated := n.currentTruncatedTime(now) 76 + 77 + if currentTruncated == n.currentTimePeriodStart { 78 + return 79 + } 80 + 81 + n.currentTimePeriodStart = currentTruncated 82 + n.prev = n.curr 83 + n.curr = n.next 84 + n.next = n.compute(currentTruncated.Add(n.timeRoundDuration)) 85 + } 86 + 87 + func (n *TotpNonce) NextNonce() string { 88 + n.mu.Lock() 89 + defer n.mu.Unlock() 90 + n.rotate(time.Now()) 91 + return n.next 92 + } 93 + 94 + func (n *TotpNonce) Check(nonce string) bool { 95 + n.mu.Lock() 96 + defer n.mu.Unlock() 97 + n.rotate(time.Now()) 98 + return nonce == n.prev || nonce == n.curr || nonce == n.next 99 + }
+69
oauth/dpop/nonce_totp_test.go
··· 1 + package dpop 2 + 3 + import ( 4 + "testing" 5 + "time" 6 + 7 + "github.com/stretchr/testify/assert" 8 + ) 9 + 10 + func TestRoundtime(t *testing.T) { 11 + tt := map[string]struct { 12 + input time.Time 13 + truncateTo time.Duration 14 + expected time.Time 15 + }{ 16 + "between 15:00:00 and 15:15:00 - rounds to 15:00:00": { 17 + input: time.Date(2026, time.May, 01, 15, 14, 59, 0, time.UTC), 18 + truncateTo: time.Minute * 15, 19 + expected: time.Date(2026, time.May, 01, 15, 00, 0, 0, time.UTC), 20 + }, 21 + "between 15:15:01 - rounds to 15:15:00": { 22 + input: time.Date(2026, time.May, 01, 15, 15, 01, 0, time.UTC), 23 + truncateTo: time.Minute * 15, 24 + expected: time.Date(2026, time.May, 01, 15, 15, 0, 0, time.UTC), 25 + }, 26 + "between 15:15:00 - rounds to 15:15:00": { 27 + input: time.Date(2026, time.May, 01, 15, 15, 0, 0, time.UTC), 28 + truncateTo: time.Minute * 15, 29 + expected: time.Date(2026, time.May, 01, 15, 15, 0, 0, time.UTC), 30 + }, 31 + } 32 + 33 + for name, tc := range tt { 34 + t.Run(name, func(t *testing.T) { 35 + res := tc.input.Truncate(tc.truncateTo) 36 + assert.Equal(t, tc.expected, res) 37 + }) 38 + } 39 + } 40 + 41 + func TestTotpNonce(t *testing.T) { 42 + startTime := time.Date(2026, time.May, 23, 15, 0, 0, 0, time.UTC) 43 + args := TotpNonceArgs{ 44 + timeRoundDuration: time.Minute * 2, 45 + Secret: []byte("secret"), 46 + } 47 + nonce := NewTotpNonce(args) 48 + nonce.currentTimePeriodStart = startTime 49 + 50 + assert.Equal(t, "ntNYQtG1F3h1U5OKz8Rs4yMJf08GUAtrGU9qg58Rt1o", nonce.curr) 51 + assert.Equal(t, "aIHUJrjFeadCwoZQmd4aJ_g-Pm4ehkwckeXmqz3_42g", nonce.prev) 52 + assert.Equal(t, "ON5oeN1v2NnseUWoUgK1CIU__qQn4mL9xbxJSu-ifUY", nonce.next) 53 + 54 + // try and rotate after a simulated 16 minutes to make it go into the next time period 55 + // and so will rotate the nonces 56 + nonce.rotate(nonce.currentTimePeriodStart.Add(time.Minute * 16)) 57 + 58 + assert.Equal(t, "ON5oeN1v2NnseUWoUgK1CIU__qQn4mL9xbxJSu-ifUY", nonce.curr) 59 + assert.Equal(t, "ntNYQtG1F3h1U5OKz8Rs4yMJf08GUAtrGU9qg58Rt1o", nonce.prev) 60 + assert.Equal(t, "HTvHm6VQP7cBbTZKWT_zhJ09bxD6B6JfhX-kArn1Hvo", nonce.next) 61 + 62 + // try and rotate after a simulated 5 minutes to which won't make it go into 63 + // the next time period and so won't rotate 64 + nonce.rotate(nonce.currentTimePeriodStart.Add(time.Minute * 5)) 65 + 66 + assert.Equal(t, "ON5oeN1v2NnseUWoUgK1CIU__qQn4mL9xbxJSu-ifUY", nonce.curr) 67 + assert.Equal(t, "ntNYQtG1F3h1U5OKz8Rs4yMJf08GUAtrGU9qg58Rt1o", nonce.prev) 68 + assert.Equal(t, "HTvHm6VQP7cBbTZKWT_zhJ09bxD6B6JfhX-kArn1Hvo", nonce.next) 69 + }
+9 -4
server/server.go
··· 127 127 128 128 PushBasedEvents bool 129 129 SubscribeReposServiceURL string 130 + NonceSecret string 130 131 } 131 132 132 133 type config struct { ··· 420 421 } 421 422 422 423 var nonceSecret []byte 423 - maybeSecret, err := os.ReadFile("nonce.secret") 424 - if err != nil && !os.IsNotExist(err) { 425 - logger.Error("error attempting to read nonce secret", "error", err) 424 + if args.NonceSecret != "" { 425 + nonceSecret = []byte(args.NonceSecret) 426 426 } else { 427 - nonceSecret = maybeSecret 427 + maybeSecret, err := os.ReadFile("nonce.secret") 428 + if err != nil && !os.IsNotExist(err) { 429 + logger.Error("error attempting to read nonce secret", "error", err) 430 + } else { 431 + nonceSecret = maybeSecret 432 + } 428 433 } 429 434 430 435 evtPersister, err := NewDbPersister(gdb, 72*time.Hour)