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 creds, err := c.CreateDidCredentials(sigkey, recovery, handle)
59 if err != nil {
60 return "", nil, err
61 }
62
63 op := Operation{
64 Type: "plc_operation",
65 VerificationMethods: creds.VerificationMethods,
66 RotationKeys: creds.RotationKeys,
67 AlsoKnownAs: creds.AlsoKnownAs,
68 Services: creds.Services,
69 Prev: nil,
70 }
71
72 if err := c.SignOp(sigkey, &op); err != nil {
73 return "", nil, err
74 }
75
76 did, err := DidFromOp(&op)
77 if err != nil {
78 return "", nil, err
79 }
80
81 return did, &op, nil
82}
83
84func (c *Client) CreateDidCredentials(sigkey *atcrypto.PrivateKeyK256, recovery string, handle string) (*DidCredentials, error) {
85 pubsigkey, err := sigkey.PublicKey()
86 if err != nil {
87 return nil, err
88 }
89
90 pubrotkey, err := c.rotationKey.PublicKey()
91 if err != nil {
92 return nil, err
93 }
94
95 // todo
96 rotationKeys := []string{pubrotkey.DIDKey()}
97 if recovery != "" {
98 rotationKeys = func(recovery string) []string {
99 newRotationKeys := []string{recovery}
100 for _, k := range rotationKeys {
101 newRotationKeys = append(newRotationKeys, k)
102 }
103 return newRotationKeys
104 }(recovery)
105 }
106
107 creds := DidCredentials{
108 VerificationMethods: map[string]string{
109 "atproto": pubsigkey.DIDKey(),
110 },
111 RotationKeys: rotationKeys,
112 AlsoKnownAs: []string{
113 "at://" + handle,
114 },
115 Services: map[string]identity.OperationService{
116 "atproto_pds": {
117 Type: "AtprotoPersonalDataServer",
118 Endpoint: "https://" + c.pdsHostname,
119 },
120 },
121 }
122
123 return &creds, nil
124}
125
126func (c *Client) SignOp(sigkey *atcrypto.PrivateKeyK256, op *Operation) error {
127 b, err := op.MarshalCBOR()
128 if err != nil {
129 return err
130 }
131
132 sig, err := c.rotationKey.HashAndSign(b)
133 if err != nil {
134 return err
135 }
136
137 op.Sig = base64.RawURLEncoding.EncodeToString(sig)
138
139 return nil
140}
141
142func (c *Client) SendOperation(ctx context.Context, did string, op *Operation) error {
143 b, err := json.Marshal(op)
144 if err != nil {
145 return err
146 }
147
148 req, err := http.NewRequestWithContext(ctx, "POST", c.service+"/"+url.QueryEscape(did), bytes.NewBuffer(b))
149 if err != nil {
150 return err
151 }
152
153 req.Header.Add("content-type", "application/json")
154
155 resp, err := c.h.Do(req)
156 if err != nil {
157 return err
158 }
159 defer resp.Body.Close()
160
161 b, err = io.ReadAll(resp.Body)
162 if err != nil {
163 return fmt.Errorf("error sending operation. status code: %d, response: %s", resp.StatusCode, string(b))
164 }
165
166 return nil
167}
168
169func DidFromOp(op *Operation) (string, error) {
170 b, err := op.MarshalCBOR()
171 if err != nil {
172 return "", err
173 }
174 s := sha256.Sum256(b)
175 b32 := strings.ToLower(base32.StdEncoding.EncodeToString(s[:]))
176 return "did:plc:" + b32[0:24], nil
177}