+1
-3
api/atyo/atyoping.go
+1
-3
api/atyo/atyoping.go
···
17
17
// RECORDTYPE: Ping
18
18
type Ping struct {
19
19
LexiconTypeID string `json:"$type,const=app.atyo.ping" cborgen:"$type,const=app.atyo.ping"`
20
-
// contents: A message encrypted for a specific recipient, possibly including an encrypted payload. After decryption, this will deserialize as `app.atyo.ping#contents`.
20
+
// contents: A message encrypted for specific recipient(s), possibly including an encrypted payload. After decryption, this will deserialize as `app.atyo.ping#contents`.
21
21
Contents util.LexBytes `json:"contents,omitempty" cborgen:"contents,omitempty"`
22
22
// createdAt: The time of record creation.
23
23
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
···
25
25
Nonce util.LexBytes `json:"nonce,omitempty" cborgen:"nonce,omitempty"`
26
26
// publicKey: One-time key used to encrypt the message contents
27
27
PublicKey util.LexBytes `json:"publicKey,omitempty" cborgen:"publicKey,omitempty"`
28
-
// targets: The one-time private key to decrypt `contents`, which has been encrypted for its recipient(s).
29
-
Targets util.LexBytes `json:"targets,omitempty" cborgen:"targets,omitempty"`
30
28
}
31
29
32
30
// Ping_Contents is a "contents" in the app.atyo.ping schema.
+1
-56
api/atyo/cbor_gen.go
+1
-56
api/atyo/cbor_gen.go
···
189
189
}
190
190
191
191
cw := cbg.NewCborWriter(w)
192
-
fieldCount := 6
192
+
fieldCount := 5
193
193
194
194
if t.Contents == nil {
195
195
fieldCount--
···
200
200
}
201
201
202
202
if t.PublicKey == nil {
203
-
fieldCount--
204
-
}
205
-
206
-
if t.Targets == nil {
207
203
fieldCount--
208
204
}
209
205
···
253
249
}
254
250
255
251
if _, err := cw.Write(t.Nonce); err != nil {
256
-
return err
257
-
}
258
-
259
-
}
260
-
261
-
// t.Targets (util.LexBytes) (slice)
262
-
if t.Targets != nil {
263
-
264
-
if len("targets") > 1000000 {
265
-
return xerrors.Errorf("Value in field \"targets\" was too long")
266
-
}
267
-
268
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("targets"))); err != nil {
269
-
return err
270
-
}
271
-
if _, err := cw.WriteString(string("targets")); err != nil {
272
-
return err
273
-
}
274
-
275
-
if len(t.Targets) > 2097152 {
276
-
return xerrors.Errorf("Byte array in field t.Targets was too long")
277
-
}
278
-
279
-
if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Targets))); err != nil {
280
-
return err
281
-
}
282
-
283
-
if _, err := cw.Write(t.Targets); err != nil {
284
252
return err
285
253
}
286
254
···
439
407
}
440
408
441
409
if _, err := io.ReadFull(cr, t.Nonce); err != nil {
442
-
return err
443
-
}
444
-
445
-
// t.Targets (util.LexBytes) (slice)
446
-
case "targets":
447
-
448
-
maj, extra, err = cr.ReadHeader()
449
-
if err != nil {
450
-
return err
451
-
}
452
-
453
-
if extra > 2097152 {
454
-
return fmt.Errorf("t.Targets: byte array too large (%d)", extra)
455
-
}
456
-
if maj != cbg.MajByteString {
457
-
return fmt.Errorf("expected byte array")
458
-
}
459
-
460
-
if extra > 0 {
461
-
t.Targets = make([]uint8, extra)
462
-
}
463
-
464
-
if _, err := io.ReadFull(cr, t.Targets); err != nil {
465
410
return err
466
411
}
467
412
+32
-25
appview/ping.go
+32
-25
appview/ping.go
···
1
1
package appview
2
2
3
3
import (
4
+
"bytes"
4
5
"crypto/rand"
6
+
"encoding/hex"
5
7
"encoding/json"
6
8
"fmt"
7
9
"net/http"
···
39
41
return
40
42
}
41
43
42
-
fmt.Fprintln(os.Stderr, "would write ping to PDS: "+string(jsonPing))
44
+
cborBuf := bytes.NewBuffer([]byte{})
45
+
err = ping.MarshalCBOR(cborBuf)
46
+
if err == nil {
47
+
fmt.Fprintln(os.Stderr, "ping length as CBOR:", cborBuf.Len())
48
+
} else {
49
+
fmt.Fprintln(os.Stderr, "failed to marshal as CBOR", err)
50
+
}
51
+
52
+
fmt.Fprintf(os.Stderr, hex.EncodeToString(cborBuf.Bytes())+"\n")
53
+
54
+
fmt.Fprintln(
55
+
os.Stderr,
56
+
"would write ping to PDS: "+string(jsonPing),
57
+
)
43
58
44
59
// TODO write records to our PDS
45
60
···
49
64
}
50
65
}
51
66
67
+
// Build an app.atyo.ping record. Notes about the format of `contents`:
68
+
//
69
+
// Roughly based on the Scuttlebutt protocol for pivate messages:
70
+
// https://ssbc.github.io/scuttlebutt-protocol-guide/#private-messages
52
71
func (s *Server) buildPing(target *identity.Identity) (*atyo.Ping, *httpError) {
53
-
const nonceLen = 24
54
-
var nonce [nonceLen]byte
55
-
if len, err := rand.Read(nonce[:]); len != nonceLen || err != nil {
56
-
return nil, &httpError{
57
-
http.StatusInternalServerError,
58
-
"rand.Read returned unexpected result",
59
-
}
60
-
}
72
+
var nonce [24]byte
73
+
_, _ = rand.Read(nonce[:])
61
74
62
75
sharedPubKey, sharedSecretKey, err := box.GenerateKey(rand.Reader)
63
76
if err != nil {
···
68
81
}
69
82
70
83
// TODO: this is where e.g. geo contents would go
71
-
message := padMessage(nil)
84
+
// Maybe we should do padding after calculating shared key len
85
+
message := padMessage([]byte("Hello secret msg"))
72
86
73
87
targetPubkey, err := s.keys.fetchUserPubKey(target)
74
88
if err != nil {
···
78
92
}
79
93
}
80
94
81
-
encryptedMessage, err := box.SealAnonymous(nil, message, sharedPubKey, rand.Reader)
82
-
if err != nil {
83
-
return nil, &httpError{http.StatusInternalServerError,
84
-
"Failed to encrypt message payload",
85
-
}
95
+
// NOTE: sealing this way effectively makes sharedSecret a symmetric key, I think?
96
+
// Or maybe just makes the message unencrypted, tbh not completely sure.
97
+
encryptedMessage := box.Seal(nil, message, &nonce, sharedPubKey, sharedSecretKey)
86
98
87
-
}
99
+
encSharedKey := box.Seal(nil, sharedSecretKey[:], &nonce, &targetPubkey, sharedSecretKey)
88
100
89
-
encSharedKey, err := box.SealAnonymous(nil, sharedSecretKey[:], &targetPubkey, rand.Reader)
90
-
if err != nil {
91
-
return nil, &httpError{
92
-
http.StatusInternalServerError,
93
-
"Failed to encrypt shared secret for recipient",
94
-
}
95
-
}
101
+
contents := append(encSharedKey, encryptedMessage...)
102
+
103
+
fmt.Fprintln(os.Stderr, "contents total len: ", len(contents))
96
104
97
105
return &atyo.Ping{
98
-
Contents: encryptedMessage,
106
+
Contents: contents,
99
107
CreatedAt: syntax.DatetimeNow().String(),
100
108
Nonce: nonce[:],
101
109
PublicKey: sharedPubKey[:],
102
-
Targets: encSharedKey,
103
110
}, nil
104
111
}
105
112
+1
-5
lexicons/ping.json
+1
-5
lexicons/ping.json
···
9
9
"type": "object",
10
10
"required": ["contents", "createdAt", "nonce", "publicKey"],
11
11
"properties": {
12
-
"targets": {
13
-
"type": "bytes",
14
-
"description": "The one-time private key to decrypt `contents`, which has been encrypted for its recipient(s)."
15
-
},
16
12
"contents": {
17
13
"type": "bytes",
18
-
"description": "A message encrypted for a specific recipient, possibly including an encrypted payload. After decryption, this will deserialize as `app.atyo.ping#contents`."
14
+
"description": "A message encrypted for specific recipient(s), possibly including an encrypted payload. After decryption, this will deserialize as `app.atyo.ping#contents`."
19
15
},
20
16
"createdAt": {
21
17
"type": "string",
+1
-1
public/index.html
+1
-1
public/index.html
···
66
66
<h1>ATYo</h1>
67
67
<form action="/login" id="login-form">
68
68
<label for="user">Username:</label>
69
-
<input type="text" name="username" placeholder="@alice.example.com"><br>
69
+
<input type="text" name="usernane" placeholder="@alice.example.com"><br>
70
70
<label for="password">
71
71
App password (create <a href="https://bsky.app/settings/app-passwords">here</a>):
72
72
</label>