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