An atproto PDS written in Go
1package main 2 3import ( 4 "crypto/ecdsa" 5 "crypto/elliptic" 6 "crypto/rand" 7 "encoding/json" 8 "fmt" 9 "os" 10 "time" 11 12 "github.com/bluesky-social/indigo/atproto/crypto" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "github.com/haileyok/cocoon/internal/helpers" 15 "github.com/lestrrat-go/jwx/v2/jwk" 16 "github.com/urfave/cli/v2" 17 "golang.org/x/crypto/bcrypt" 18 "gorm.io/driver/sqlite" 19 "gorm.io/gorm" 20) 21 22func main() { 23 app := cli.App{ 24 Name: "admin", 25 Commands: cli.Commands{ 26 runCreateRotationKey, 27 runCreatePrivateJwk, 28 runCreateInviteCode, 29 runResetPassword, 30 }, 31 ErrWriter: os.Stdout, 32 } 33 34 app.Run(os.Args) 35} 36 37var runCreateRotationKey = &cli.Command{ 38 Name: "create-rotation-key", 39 Usage: "creates a rotation key for your pds", 40 Flags: []cli.Flag{ 41 &cli.StringFlag{ 42 Name: "out", 43 Required: true, 44 Usage: "output file for your rotation key", 45 }, 46 }, 47 Action: func(cmd *cli.Context) error { 48 key, err := crypto.GeneratePrivateKeyK256() 49 if err != nil { 50 return err 51 } 52 53 bytes := key.Bytes() 54 55 if err := os.WriteFile(cmd.String("out"), bytes, 0644); err != nil { 56 return err 57 } 58 59 return nil 60 }, 61} 62 63var runCreatePrivateJwk = &cli.Command{ 64 Name: "create-private-jwk", 65 Usage: "creates a private jwk for your pds", 66 Flags: []cli.Flag{ 67 &cli.StringFlag{ 68 Name: "out", 69 Required: true, 70 Usage: "output file for your jwk", 71 }, 72 }, 73 Action: func(cmd *cli.Context) error { 74 privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 75 if err != nil { 76 return err 77 } 78 79 key, err := jwk.FromRaw(privKey) 80 if err != nil { 81 return err 82 } 83 84 kid := fmt.Sprintf("%d", time.Now().Unix()) 85 86 if err := key.Set(jwk.KeyIDKey, kid); err != nil { 87 return err 88 } 89 90 b, err := json.Marshal(key) 91 if err != nil { 92 return err 93 } 94 95 if err := os.WriteFile(cmd.String("out"), b, 0644); err != nil { 96 return err 97 } 98 99 return nil 100 }, 101} 102 103var runCreateInviteCode = &cli.Command{ 104 Name: "create-invite-code", 105 Usage: "creates an invite code", 106 Flags: []cli.Flag{ 107 &cli.StringFlag{ 108 Name: "for", 109 Usage: "optional did to assign the invite code to", 110 }, 111 &cli.IntFlag{ 112 Name: "uses", 113 Usage: "number of times the invite code can be used", 114 Value: 1, 115 }, 116 }, 117 Action: func(cmd *cli.Context) error { 118 db, err := newDb() 119 if err != nil { 120 return err 121 } 122 123 forDid := "did:plc:123" 124 if cmd.String("for") != "" { 125 did, err := syntax.ParseDID(cmd.String("for")) 126 if err != nil { 127 return err 128 } 129 130 forDid = did.String() 131 } 132 133 uses := cmd.Int("uses") 134 135 code := fmt.Sprintf("%s-%s", helpers.RandomVarchar(8), helpers.RandomVarchar(8)) 136 137 if err := db.Exec("INSERT INTO invite_codes (did, code, remaining_use_count) VALUES (?, ?, ?)", forDid, code, uses).Error; err != nil { 138 return err 139 } 140 141 fmt.Printf("New invite code created with %d uses: %s\n", uses, code) 142 143 return nil 144 }, 145} 146 147var runResetPassword = &cli.Command{ 148 Name: "reset-password", 149 Usage: "resets a password", 150 Flags: []cli.Flag{ 151 &cli.StringFlag{ 152 Name: "did", 153 Usage: "did of the user who's password you want to reset", 154 }, 155 }, 156 Action: func(cmd *cli.Context) error { 157 db, err := newDb() 158 if err != nil { 159 return err 160 } 161 162 didStr := cmd.String("did") 163 did, err := syntax.ParseDID(didStr) 164 if err != nil { 165 return err 166 } 167 168 newPass := fmt.Sprintf("%s-%s", helpers.RandomVarchar(12), helpers.RandomVarchar(12)) 169 hashed, err := bcrypt.GenerateFromPassword([]byte(newPass), 10) 170 if err != nil { 171 return err 172 } 173 174 if err := db.Exec("UPDATE repos SET password = ? WHERE did = ?", hashed, did.String()).Error; err != nil { 175 return err 176 } 177 178 fmt.Printf("Password for %s has been reset to: %s", did.String(), newPass) 179 180 return nil 181 }, 182} 183 184func newDb() (*gorm.DB, error) { 185 return gorm.Open(sqlite.Open("cocoon.db"), &gorm.Config{}) 186}