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(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 (c *Client) SignOp(sigkey *crypto.PrivateKeyK256, op *Operation) error {
105 b, err := op.MarshalCBOR()
106 if err != nil {
107 return err
108 }
109
110 sig, err := c.rotationKey.HashAndSign(b)
111 if err != nil {
112 return err
113 }
114
115 op.Sig = base64.RawURLEncoding.EncodeToString(sig)
116
117 return nil
118}
119
120func (c *Client) SendOperation(ctx context.Context, did string, op *Operation) error {
121 b, err := json.Marshal(op)
122 if err != nil {
123 return err
124 }
125
126 req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b))
127 if err != nil {
128 return err
129 }
130
131 req.Header.Add("content-type", "application/json")
132
133 resp, err := c.h.Do(req)
134 if err != nil {
135 return err
136 }
137 defer resp.Body.Close()
138
139 b, err = io.ReadAll(resp.Body)
140 if err != nil {
141 return fmt.Errorf("error sending operation. status code: %d, response: %s", resp.StatusCode, string(b))
142 }
143
144 return nil
145}
146
147func DidFromOp(op *Operation) (string, error) {
148 b, err := op.MarshalCBOR()
149 if err != nil {
150 return "", err
151 }
152 s := sha256.Sum256(b)
153 b32 := strings.ToLower(base32.StdEncoding.EncodeToString(s[:]))
154 return "did:plc:" + b32[0:24], nil
155}