+46
-9
README.md
+46
-9
README.md
···
1
-
# AProto
1
+
<img src='https://raw.githubusercontent.com/evbogue/wiredove/master/doveorange_sm.png' style='width: 75px; height: 75px;' />
2
+
3
+
# ANProto
4
+
5
+
the **A**uthenticated and **N**on-networked protocol or **AN**other protocol
6
+
7
+
ed25519 keypairs sign timestamp + hash in base64
8
+
9
+
***
10
+
11
+
[anproto.com](https://anproto.com)
12
+
13
+
+ [JavaScript implementation](https://github.com/evbogue/anproto) [by Evbogue]
14
+
+ [Golang implementation](https://github.com/vic/goan) [by Vic]
15
+
+ [Rust implementation](https://github.com/vic/anproto-rs/) [by Vic]
16
+
+ [Python implementation](https://github.com/macauleyjustin/ANproto-Python) [by Justin]
17
+
18
+
try it at [anproto.com/try](https://anproto.com/try) or use a client such as [wiredove](https://wiredove.net/)
19
+
20
+
***
21
+
22
+
### What is ANProto?
23
+
24
+
+ ANProto is the spiritual successor to [secure-scuttlebot](https://scuttlebot.io), but without all of the extra stuff that is difficult to maintain.
25
+
+ ANProto is an attempt to argue that [ATProto](https://atproto.com) is too involved in it's own networking infrastructure to be usefully decentralized.
26
+
+ ANProto operates under the working theory that [Nostr](https://fiatjaf.com/nostr.html) will never reach anyone besides Bitcoiners.
27
+
28
+
***
2
29
3
-
ed25519 keypairs sign ts + hash in base64
30
+
### Bring your own network!
31
+
32
+
ANProto works over any networking stack. Open the messages from your URL bar! Email them to your friends! Load them on a USB stick an slingshot them over a river! ANProto is non-networked, so you can send and retrieve the messages anyway you want. Try the fetch API or Websockets if you want a good place to start. But maybe dork out trying to send ANProto messages via Bluetooth, LoRa, or sync them via local wifi like you did with Scuttlebot!
4
33
5
-
### code
34
+
***
35
+
36
+
### the JavaScript library!
6
37
7
38
use Deno or your browser
8
39
9
40
```
10
-
import { a } from './a.js'
41
+
import { an } from './an.js'
11
42
12
-
console.log(await a.gen())
13
-
// NtrdkXob+epH2c/k3GVtdw8lnJzMt+eEWQQ5VYgaARc=XgGOj8lESOXzfmoN6tA5fca+c5fXukw1LJFJYVhf38U22t2Rehv56kfZz+TcZW13DyWcnMy354RZBDlViBoBFw==
43
+
console.log(await an.gen())
44
+
// BSY7/er4VJIu08o39NaRAiPY/MAvd7oQhlGCRDABjYU=tQa03kqUWG3VtHZ98++lHFBeQ4JKZwuTH2CjC/K6P8EFJjv96vhUki7Tyjf01pECI9j8wC93uhCGUYJEMAGNhQ==
14
45
15
-
console.log(await a.hash('Hello World'))
46
+
console.log(await an.hash('Hello World'))
16
47
// pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=
17
48
18
-
console.log(await a.sig('Hello World', await a.gen()))
19
-
// 6qEZt6kv82bBpDcN0KFMUd7Bhj9HM8pDmK/+AvoPuOGH/DxdBJyOf/wsIx/IyLJRDpSL4jbIKa7mNyUEfrSWDTE3NTUxOTI3NzkwMzhwWkdtMUF2MElFQktBUmN6ejdleGtOWXNaYjhMemFNclY3SjMyYTJmRkc0PQ==
49
+
console.log(await an.sign(hash, await a.gen()))
50
+
// BSY7/er4VJIu08o39NaRAiPY/MAvd7oQhlGCRDABjYU=yVpD8i7d3d4dls3YThEg1x1vSdmqeEweV4e4Ejl/8yPoVG7JR0YAKDPagQOgxXMrlCVLNNqvlNvj4xRDOYDLBjE3NTUxOTc4NDEzMTlwWkdtMUF2MElFQktBUmN6ejdleGtOWXNaYjhMemFNclY3SjMyYTJmRkc0PQ==
51
+
52
+
console.log(await an.open('BSY7/er4VJIu08o39NaRAiPY/MAvd7oQhlGCRDABjYU=yVpD8i7d3d4dls3YThEg1x1vSdmqeEweV4e4Ejl/8yPoVG7JR0YAKDPagQOgxXMrlCVLNNqvlNvj4xRDOYDLBjE3NTUxOTc4NDEzMTlwWkdtMUF2MElFQktBUmN6ejdleGtOWXNaYjhMemFNclY3SjMyYTJmRkc0PQ=='))
53
+
54
+
//1755197841319pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4=
20
55
```
56
+
21
57
---
58
+
22
59
MIT
+75
ROADMAP.md
+75
ROADMAP.md
···
1
+
# ANProto Roadmap
2
+
3
+
### Authenticated Non-networked Protocol (ANProto)
4
+
5
+
ANProto should be implemented in as many programming languages as possible so that it is useful to many programmers in different environments.
6
+
7
+
- [x] JavaScript https://github.com/evbogue/anproto
8
+
- [x] Golang https://github.com/vic/goan
9
+
- [x] Rust https://github.com/vic/anproto-rs/
10
+
- [ ] Zig
11
+
- [ ] C
12
+
- [ ] Python
13
+
- [ ] Haskell
14
+
- [ ] etc
15
+
16
+
### A Personal Data Server (apds)
17
+
18
+
apds authenticates and stores ANProto messages and blobs
19
+
20
+
This document only specifies what needs to happen with the JavaScript implementation.
21
+
22
+
- [x] Generate and save keypairs
23
+
- [x] Compose messages
24
+
- [x] Verify messages
25
+
- [x] Add messages to a DB
26
+
- [x] Query and search the DB
27
+
- [ ] Remove messages from a DB
28
+
- [ ] Block content from authors or specific messages from being added to a DB
29
+
- [ ] Websocket server (move over from Dovepub)
30
+
- [ ] HTTP server (move over from Dovepub)
31
+
- [ ] Static message browser
32
+
- [ ] Web 2.0 login/password with server-held keys
33
+
34
+
### Wiredove PoC Webapp (wd)
35
+
36
+
Wiredove is a proof of concept progressive web app (PWA) that allows users to interact local and remote APDSes.
37
+
38
+
Networking -- connects Wiredove to remote PDSes
39
+
40
+
- [x] Trystero
41
+
- [x] Websockets
42
+
- [ ] Does ws reconnect?
43
+
44
+
User interface
45
+
46
+
- [x] clientside hashrouter
47
+
- [x] profile pages
48
+
- [x] chronological feed
49
+
- [x] post composer
50
+
- [ ] "for you" feed
51
+
- [ ] bios on profile pages
52
+
- [ ] banners on profile pages
53
+
- [ ] notifications
54
+
- [ ] likes/hearts/emojis?
55
+
- [ ] delete specific blobs/posts/users
56
+
- [ ] client side mute
57
+
58
+
Random errors that need to get fixed
59
+
60
+
- [ ] new posts should show up at top
61
+
- [ ] syncing old messages should not be so overwhelming
62
+
- [ ] lost posts?
63
+
- [ ] double posts with different hashes?
64
+
- [ ] slow page load on mobile
65
+
66
+
### React Native App
67
+
68
+
- [ ] profile pages
69
+
- [ ] chronological feed
70
+
- [ ] apds support? or just a lite client?
71
+
- [ ] notifications page with read/unread state
72
+
73
+
### Open questions
74
+
75
+
- [ ] Do we support encrypted dms?
-30
a.js
-30
a.js
···
1
-
import nacl from "./lib/nacl-fast-es.js";
2
-
import { decode, encode } from "./lib/base64.js";
3
-
4
-
export const a = {};
5
-
6
-
a.gen = async () => {
7
-
const g = await nacl.sign.keyPair();
8
-
const k = await encode(g.publicKey) + encode(g.secretKey);
9
-
return k;
10
-
};
11
-
12
-
a.hash = async (d) => {
13
-
return encode(
14
-
Array.from(
15
-
new Uint8Array(
16
-
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(d)),
17
-
),
18
-
),
19
-
);
20
-
};
21
-
22
-
a.sign = async (d, k) => {
23
-
const ts = Date.now();
24
-
const h = await a.hash(d);
25
-
const s = encode(
26
-
nacl.sign(new TextEncoder().encode(ts + h), decode(k.substring(44))),
27
-
);
28
-
29
-
return s;
30
-
};
+37
an.js
+37
an.js
···
1
+
import nacl from "./lib/nacl-fast-es.js";
2
+
import { decode, encode } from "./lib/base64.js";
3
+
4
+
export const an = {};
5
+
6
+
an.gen = async () => {
7
+
const g = await nacl.sign.keyPair();
8
+
const k = await encode(g.publicKey) + encode(g.secretKey);
9
+
return k;
10
+
};
11
+
12
+
an.hash = async (d) => {
13
+
return encode(
14
+
Array.from(
15
+
new Uint8Array(
16
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(d)),
17
+
),
18
+
),
19
+
);
20
+
};
21
+
22
+
an.sign = async (h, k) => {
23
+
const ts = Date.now();
24
+
const s = encode(
25
+
nacl.sign(new TextEncoder().encode(ts + h), decode(k.substring(44))),
26
+
);
27
+
28
+
return k.substring(0, 44) + s;
29
+
};
30
+
31
+
an.open = async (m) => {
32
+
const o = new TextDecoder().decode(
33
+
nacl.sign.open(decode(m.substring(44)), decode(m.substring(0, 44))),
34
+
);
35
+
36
+
return o;
37
+
};
+8
-4
ex.js
+8
-4
ex.js
···
1
-
import { a } from "./a.js";
1
+
import { an } from "./an.js";
2
2
3
3
const m = "Hello World";
4
-
const k = await a.gen();
4
+
const h = await an.hash(m);
5
+
const k = await an.gen();
6
+
const s = await an.sign(h, k);
7
+
const o = await an.open(s);
5
8
6
9
console.log(k);
7
-
console.log(await a.hash(m));
8
-
console.log(await a.sign(m, k));
10
+
console.log(h);
11
+
console.log(s);
12
+
console.log(o);
+35
node_ex.js
+35
node_ex.js
···
1
+
// get an.js working in Node.js -- from https://github.com/vic/goan/blob/main/js_helper.js
2
+
3
+
import { createRequire } from 'module';
4
+
const require = createRequire(import.meta.url);
5
+
6
+
globalThis.self = {
7
+
crypto: {
8
+
getRandomValues: (buf) => {
9
+
require('crypto').randomFillSync(buf);
10
+
return buf;
11
+
}
12
+
}
13
+
};
14
+
15
+
if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.subtle === 'undefined') {
16
+
globalThis.crypto = globalThis.crypto || {};
17
+
try {
18
+
globalThis.crypto.subtle = require('crypto').webcrypto.subtle;
19
+
} catch (e) {
20
+
console.log(e)
21
+
}
22
+
}
23
+
24
+
const { an } = await import('./an.js');
25
+
26
+
const m = "Hello World";
27
+
const h = await an.hash(m);
28
+
const k = await an.gen();
29
+
const s = await an.sign(h, k);
30
+
const o = await an.open(s);
31
+
32
+
console.log(k);
33
+
console.log(h);
34
+
console.log(s);
35
+
console.log(o);
+154
serve.js
+154
serve.js
···
1
+
import { Hono } from "jsr:@hono/hono";
2
+
import { serveStatic } from "jsr:@hono/hono/deno";
3
+
import { marked } from "https://esm.sh/gh/evbogue/bog5@de70376265/lib/marked.esm.js";
4
+
5
+
import { foot, head } from "./template.js";
6
+
7
+
const app = new Hono();
8
+
9
+
const readme = await Deno.readTextFile("./README.md");
10
+
11
+
app.get("/", async (c) => {
12
+
const content = `
13
+
<div id="scroller">
14
+
<div class='message'>
15
+
${await marked(readme)}
16
+
</div>
17
+
</div>
18
+
`;
19
+
20
+
const html = await head("Index") + content + await foot();
21
+
return await c.html(html);
22
+
});
23
+
24
+
app.get("/try", async (c) => {
25
+
const body = `<body>
26
+
<div id='scroller'>
27
+
<div class='message'>
28
+
<h1>Try ANProto</h1>
29
+
30
+
<p><em>An Interactive Demonstration</em></p>
31
+
32
+
<p><strong>Step 1.</strong> Generate an ed25519 keypair</p>
33
+
34
+
<code>const kp = await an.gen()</code>
35
+
36
+
<input style='width: 100%;' id='key' placeholder='Make a keypair'></input>
37
+
38
+
<button id='but'>Generate keypair</button>
39
+
40
+
</div>
41
+
42
+
<div class='message'>
43
+
44
+
<p><strong>Step 2.</strong> Hash your blob with sha256</p>
45
+
46
+
<code>const hash = await an.hash(content)</code>
47
+
48
+
<input style='width: 100%;' id='content' placeholder='Write a message'></input>
49
+
50
+
<button id='hash'>Generate hash</button>
51
+
52
+
<span id='sha256'></span>
53
+
54
+
</div>
55
+
56
+
<div class='message'>
57
+
58
+
<p><strong>Step 3.</strong> Sign the ANProto message</p>
59
+
60
+
<code>const sig = await an.sign(hash, keypair)</code>
61
+
62
+
<input style='width: 100%' id='sig'></input>
63
+
64
+
<button id='sign'>Sign message</button>
65
+
66
+
</div>
67
+
<div class="message">
68
+
69
+
<p><strong>Step 4.</strong> Open the ANProto message</p>
70
+
71
+
<code>const opened = await an.open(msg)</code>
72
+
73
+
<input style='width: 100%;' id='openen'></input>
74
+
75
+
<button id='open'>Open</button>
76
+
77
+
</div>
78
+
<div class="message">
79
+
80
+
<p><strong>Step 5.</strong> Retrieve the blob</p>
81
+
82
+
<span id='msg'></span>
83
+
84
+
<button id='get'>Get</button>
85
+
86
+
</div>
87
+
</div>
88
+
</body>
89
+
90
+
<script type='module'>
91
+
import { an } from './an.js'
92
+
93
+
const key = document.getElementById('key')
94
+
const button = document.getElementById('but')
95
+
96
+
button.onclick = async () => {
97
+
key.value = await an.gen()
98
+
}
99
+
100
+
const content = document.getElementById('content')
101
+
const hashbutton = document.getElementById('hash')
102
+
const sha = document.getElementById('sha256')
103
+
104
+
let blobs = []
105
+
106
+
hashbutton.onclick = async () => {
107
+
sha.textContent = await an.hash(content.value)
108
+
blobs[sha.textContent] = content.value
109
+
}
110
+
111
+
const siginput = document.getElementById('sig')
112
+
const signbutton = document.getElementById('sign')
113
+
114
+
signbutton.onclick = async () => {
115
+
siginput.value = await an.sign(sha.textContent, key.value)
116
+
}
117
+
118
+
const openbutton = document.getElementById('open')
119
+
120
+
const openen = document.getElementById('openen')
121
+
122
+
openbutton.onclick = async () => {
123
+
openen.value = await an.open(siginput.value)
124
+
}
125
+
126
+
const msgspan = document.getElementById('msg')
127
+
const getbutton = document.getElementById('get')
128
+
129
+
getbutton.onclick = async () => {
130
+
msgspan.textContent = blobs[openen.value.substring(13)]
131
+
if (openen.value.substring(13) == sha.textContent) {
132
+
msgspan.textContent = msgspan.textContent + ' โ
'
133
+
} else {
134
+
msgspan.textContent = msgspan.textContent + ' โ'
135
+
}
136
+
}
137
+
138
+
</script>`;
139
+
140
+
const html = await head("Try it") + body + await foot();
141
+
return await c.html(html);
142
+
});
143
+
144
+
app.use(
145
+
"*",
146
+
serveStatic({
147
+
root: "./",
148
+
onFound: (_path, c) => {
149
+
c.header("Access-Control-Allow-Origin", "*");
150
+
},
151
+
}),
152
+
);
153
+
154
+
export default app;
+192
style.css
+192
style.css
···
1
+
body {
2
+
background-color: #f2f2f2;
3
+
color: #444;
4
+
font-family: "Source Sans 3", sans-serif;
5
+
max-width: 100%;
6
+
margin-top: 45px;
7
+
margin-bottom: 10em;
8
+
}
9
+
10
+
#scroller {max-width: 680px; margin-left: auto; margin-right: auto;}
11
+
12
+
blockquote { border-left: 5px solid #f5f5f5; margin-left: none; padding-left: 10px; color: #777; }
13
+
14
+
p, h1, h2, h3, h4, h5, h6 { margin-top: 5px; margin-bottom: 5px; }
15
+
16
+
pre {
17
+
//color: #dd1144;
18
+
background: #f5f5f5;
19
+
width: 100%;
20
+
display: block;
21
+
}
22
+
23
+
code {
24
+
background: #f5f5f5;
25
+
padding: 5px;
26
+
border-radius: 5px;
27
+
display: inline-block;
28
+
vertical-align: bottom;
29
+
}
30
+
31
+
code, pre {
32
+
font-family: "Roboto Mono", monospace;
33
+
font-size: .9em;
34
+
overflow: auto;
35
+
word-break: break-all;
36
+
word-wrap: break-word;
37
+
white-space: pre;
38
+
white-space: -moz-pre-wrap;
39
+
white-space: pre-wrap;
40
+
white-space: pre\9;
41
+
}
42
+
43
+
button {
44
+
font-size: .85em;
45
+
background: #fff;
46
+
background-image: linear-gradient(to bottom, #ffffff, #f2f2f2);
47
+
border: 1px solid #e4e4e4;
48
+
padding: 5px 10px 5px 10px;
49
+
border-radius: 5px;
50
+
}
51
+
52
+
hr { border: 1px solid #e4e4e4;}
53
+
54
+
button:hover {
55
+
background: #f2f2f2;
56
+
cursor: pointer;
57
+
}
58
+
59
+
textarea, input {
60
+
font-size: 1em;
61
+
font-family: "Source Sans 3", sans-serif;
62
+
border: 1px solid #f8f8f8;
63
+
border-radius: 5px;
64
+
background: #f8f8f8;
65
+
color: #555;
66
+
padding: 5px;
67
+
//box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
68
+
}
69
+
70
+
.composer { margin-left: 37px; margin-top: 0;}
71
+
72
+
textarea:hover, textarea:focus, input:hover, input:focus, {
73
+
background: transparent;
74
+
}
75
+
76
+
textarea:focus, input:focus {
77
+
outline: none !important;
78
+
}
79
+
80
+
textarea {
81
+
margin-top: 5px;
82
+
margin-bottom: 5px;
83
+
width: 99%;
84
+
height: 150px;
85
+
}
86
+
87
+
a {
88
+
color: #045fd0;
89
+
text-decoration: none;
90
+
}
91
+
92
+
a:hover {
93
+
color: #8d82fe;
94
+
text-decoration: underline;
95
+
}
96
+
97
+
img {width: 95%; margin: 1em;}
98
+
99
+
.material-symbols-outlined { color: #666; vertical-align: middle; font-size: 18px; cursor: pointer;}
100
+
101
+
iframe {
102
+
width: 100%;
103
+
border: 1px solid #e4e4e4;
104
+
border-radius: 5px;
105
+
margin-top: 5px;
106
+
height: 275px;
107
+
}
108
+
109
+
#navbar {
110
+
padding-top: .5em;
111
+
padding-left: 1em;
112
+
padding-bottom: .5em;
113
+
position: fixed;
114
+
width: 100%;
115
+
z-index: 1;
116
+
top: 0;
117
+
left: 0;
118
+
background-color: rgba(242,242,242,0.5);
119
+
backdrop-filter: blur(10px);
120
+
border-bottom: 1px solid #eee;
121
+
}
122
+
123
+
.message {
124
+
padding: .75em;
125
+
margin-top: 5px;
126
+
background: #f8f8f8;
127
+
border: 1px solid #f5f5f5;
128
+
min-height: 35px;
129
+
border-radius: 5px;
130
+
overflow: hidden;
131
+
}
132
+
133
+
.message:hover {
134
+
border: 1px solid #eee;
135
+
}
136
+
137
+
@media (prefers-color-scheme: dark) {
138
+
body {
139
+
background-color: #181818;
140
+
color: #f5f5f5;
141
+
}
142
+
#navbar { background-color: rgba(24,24,24,0.2); border-bottom: 1px solid #FE7A00;}
143
+
#navbar a { color: #FE7A00;}
144
+
#navbar:hover { border-bottom: 1px solid magenta;}
145
+
.message { background-color: #222; border: 1px solid #333;}
146
+
.message:hover { border: 1px solid magenta;}
147
+
148
+
textarea, input, iframe { background: #333; color: #f5f5f5; border: 1px solid #222;}
149
+
150
+
button { color: #ccc; background: #333; border: 1px solid #444;}
151
+
button:hover { background: #222;}
152
+
hr { border: 1px solid #333;}
153
+
pre, code { background: #333; color: #f5f5f5;}
154
+
a {color: #FE7A00;}
155
+
}
156
+
157
+
.content {margin-top: 5px;}
158
+
159
+
.message, .message > * {
160
+
animation: fadein .5s;
161
+
}
162
+
163
+
@keyframes fadein {
164
+
from { opacity: 0; }
165
+
to { opacity: 1; }
166
+
}
167
+
168
+
.pubkey {
169
+
color: #9da0a4;
170
+
font-family: monospace;
171
+
}
172
+
173
+
.avatar, .avatar_small {
174
+
border-radius: 100%;
175
+
margin: 0px;
176
+
margin-right: 10px;
177
+
object-fit: cover;
178
+
vertical-align: top;
179
+
}
180
+
181
+
.avatar {
182
+
height: 33px;
183
+
width: 33px;
184
+
}
185
+
186
+
.avatar_small { height: 25px; width: 25px;}
187
+
188
+
.breadcrumbs { font-size: 1em; }
189
+
190
+
.avatarlink { font-weight: 600;}
191
+
.unstyled { color: #ccc;}
192
+
.hljs { padding: 10px; border-radius: 5px; background: #555; color: #f2f2f2;}
+30
template.js
+30
template.js
···
1
+
export const head = async (title) => {
2
+
return await `
3
+
<!doctype html>
4
+
<html>
5
+
<head>
6
+
<title>ANProto | ${title}</title>
7
+
<link rel='stylesheet' href='./style.css' type='text/css' />
8
+
<meta name='viewport' content='width=device-width initial-scale=1' />
9
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
10
+
<link rel="preconnect" href="https://fonts.googleapis.com">
11
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
13
+
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
14
+
15
+
</head>
16
+
<body>
17
+
<div id='navbar'>
18
+
<a href='/'><img src='https://wiredove.net/doveorange_sm.png' class='avatar_small' style='vertical-align: middle;'></a>
19
+
<strong><span style="color: #fe7a00;">AN</span>Proto</strong>
20
+
<strong><a href='./try'>Try it</a></strong>
21
+
</div>
22
+
`;
23
+
};
24
+
25
+
export const foot = async () => {
26
+
return await `
27
+
</body>
28
+
</html>
29
+
`;
30
+
};