anproto -- authenticated non-networked protocol or another proto sha256 blobs signed with ed25519 keypairs anproto.com
ed25519 social protocols

Compare changes

Choose any two refs to compare.

+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 + };