1package plc
2
3import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/base32"
8 "encoding/base64"
9 "encoding/json"
10 "fmt"
11 "io"
12 "net/http"
13 "net/url"
14 "strings"
15
16 "github.com/bluesky-social/indigo/atproto/crypto"
17 "github.com/bluesky-social/indigo/util"
18)
19
20type Client struct {
21 h *http.Client
22 service string
23 pdsHostname string
24 rotationKey *crypto.PrivateKeyK256
25}
26
27type ClientArgs struct {
28 Service string
29 RotationKey []byte
30 PdsHostname string
31}
32
33func NewClient(args *ClientArgs) (*Client, error) {
34 if args.Service == "" {
35 args.Service = "https://plc.directory"
36 }
37
38 rk, err := crypto.ParsePrivateBytesK256([]byte(args.RotationKey))
39 if err != nil {
40 return nil, err
41 }
42
43 return &Client{
44 h: util.RobustHTTPClient(),
45 service: args.Service,
46 rotationKey: rk,
47 pdsHostname: args.PdsHostname,
48 }, nil
49}
50
51func (c *Client) CreateDID(ctx context.Context, sigkey *crypto.PrivateKeyK256, recovery string, handle string) (string, *Operation, error) {
52 pubsigkey, err := sigkey.PublicKey()
53 if err != nil {
54 return "", nil, err
55 }
56
57 pubrotkey, err := c.rotationKey.PublicKey()
58 if err != nil {
59 return "", nil, err
60 }
61
62 // todo
63 rotationKeys := []string{pubrotkey.DIDKey()}
64 if recovery != "" {
65 rotationKeys = func(recovery string) []string {
66 newRotationKeys := []string{recovery}
67 for _, k := range rotationKeys {
68 newRotationKeys = append(newRotationKeys, k)
69 }
70 return newRotationKeys
71 }(recovery)
72 }
73
74 op := Operation{
75 Type: "plc_operation",
76 VerificationMethods: map[string]string{
77 "atproto": pubsigkey.DIDKey(),
78 },
79 RotationKeys: rotationKeys,
80 AlsoKnownAs: []string{
81 "at://" + handle,
82 },
83 Services: map[string]OperationService{
84 "atproto_pds": {
85 Type: "AtprotoPersonalDataServer",
86 Endpoint: "https://" + c.pdsHostname,
87 },
88 },
89 Prev: nil,
90 }
91
92 if err := c.SignOp(sigkey, &op); err != nil {
93 return "", nil, err
94 }
95
96 did, err := didFromOp(&op)
97 if err != nil {
98 return "", nil, err
99 }
100
101 return did, &op, nil
102}
103
104func didFromOp(op *Operation) (string, error) {
105 b, err := op.MarshalCBOR()
106 if err != nil {
107 return "", err
108 }
109 s := sha256.Sum256(b)
110 b32 := strings.ToLower(base32.StdEncoding.EncodeToString(s[:]))
111 return "did:plc:" + b32[0:24], nil
112}
113
114func (c *Client) SignOp(sigkey *crypto.PrivateKeyK256, op *Operation) error {
115 b, err := op.MarshalCBOR()
116 if err != nil {
117 return err
118 }
119
120 sig, err := c.rotationKey.HashAndSign(b)
121 if err != nil {
122 return err
123 }
124
125 op.Sig = base64.RawURLEncoding.EncodeToString(sig)
126
127 return nil
128}
129
130func (c *Client) SendOperation(ctx context.Context, did string, op *Operation) error {
131 b, err := json.Marshal(op)
132 if err != nil {
133 return err
134 }
135
136 req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b))
137 if err != nil {
138 return err
139 }
140
141 req.Header.Add("content-type", "application/json")
142
143 resp, err := c.h.Do(req)
144 if err != nil {
145 return err
146 }
147 defer resp.Body.Close()
148
149 b, err = io.ReadAll(resp.Body)
150 if err != nil {
151 return fmt.Errorf("error sending operation. status code: %d, response: %s", resp.StatusCode, string(b))
152 }
153
154 return nil
155}