Sharded, encrypted storage between friends over Yggdrasil: lend each other disk space, send files by sending a sealed stub.
Go
1
15 commits
updated Oct 6, 2026
Proof of concept: sharded, encrypted file storage between peers on the Yggdrasil overlay, with no VPN coordinator: node IDs come from public keys, each node only answers group members, and storage challenges check that peers still hold what they were given.
The idea: people in a group lend each other disk space. Each file is encrypted, cut into pieces and spread over everyone's machines, so it survives any one machine failing. The small stub left behind holds the key, so sending someone a stub (sealed for them, by email or anything else) is a way to send them the file, with no cloud service in between.
Status: experimental. The cryptography has not been reviewed by anyone independent. Don't make it the only copy of anything you can't lose.
put splits the file into 4 MiB chunks, encrypts each with AES-256-GCM
(one random key per file, fresh nonce per chunk), and erasure-codes each
chunk into 4 data + 2 parity shards.FILE.ystub holds the manifest, including the key. Anyone with
the stub and access to the peers can read the file.get fetches shards in parallel, rejects any whose hash doesn't match,
and rebuilds each chunk from any 4 of its 6 shards.FILE.ystub.challenges.json
(keep this private). verify sends one per shard: "hash this byte range
with this nonce", and checks the answer.A Yggdrasil address (200::/7) is derived from the node's public key, so the
source address of a connection over the overlay identifies the caller. The
address is the node ID. Each server only answers callers whose address is in
peers.json (default deny), binds only to its overlay address (or, with the
built-in Yggdrasil, has nothing on the machine's network
at all), and only lets the original writer delete a shard.
Download yggstore for your system from the
releases page (Linux on
PCs, Raspberry Pis and arm64 boards, Windows, macOS), and check it against
SHA256SUMS. Or build it yourself with Go 1.24 or later:
go build -o bin/yggstore ./cmd/yggstore
bin/yggstore id # prints your node ID and peers.json entry
# collect one entry from every machine into peers.json, copy it to each one
bin/yggstore serve -peers peers.json # on every machine (port 7400 by default)
bin/yggstore status -peers peers.json
bin/yggstore put -peers peers.json photo.jpg # -> photo.jpg.ystub
bin/yggstore verify photo.jpg.ystub
bin/yggstore get -o copy.jpg photo.jpg.ystub
bin/yggstore rm photo.jpg.ystub # delete its shards
peers.json:
[
{"name": "desktop", "addr": "[200:1234:5678::1]:7400", "admin": true},
{"name": "pi", "addr": "[201:abcd::1]:7400", "slow": true}
]
Each chunk is 6 shards, and no node gets more than 2 of them, so losing any
one node never loses data. A node marked "slow": true gets one shard per
chunk while the others have room for the rest, so it doesn't hold uploads
back. Only the uploading machine's peers.json decides this.
Nodes that run on the same machine (Docker containers, say) should share a
"host" tag, e.g. {"name": "t1", "addr": "...", "host": "desktop"}. The
2-shard limit then applies per machine, and uploads need 3 machines online,
not just 3 nodes.
On an admin dashboard, open Invite someone, type their name and press
Make invite. You get a message to send them, with a one-time invite
(yggjoin1:…, valid 7 days). They need Yggdrasil running and connected
(the message lists public peers), and the yggstore program; then:
yggstore join -service yggjoin1:… # -name NODE -me NAME -quota GB to change the defaults
join asks the admin node to add this machine (over Yggdrasil, presenting the
invite), saves the member list, adds the inviter as a contact, and with
-service starts the node and dashboard as user services. On the admin side
the newcomer is added to peers.json (owner: their name) and to your contacts,
and the list sync tells every node. An invite works once; the admin node
only keeps a hash of it. A node on a machine that is already a member is
tagged with that machine automatically; otherwise tag it with "host" by
hand if you know two nodes share a box.
The join endpoint is the only call a non-member can make, and it does nothing without a valid invite (rate-limited to 6 tries a minute).
An "admin": true node (the desktop) keeps every node's list in step. Each
node reports which list it holds; the dashboard sends its own peers.json to any
node that holds another one, so a node added on the dashboard, or a hand edit
of the desktop's peers.json, reaches every node within a minute, with no
restarts. A node keeps a pushed list in its data folder
(peers.pushed.json), and uses whichever of that and its own -peers file
changed last. Only admins may push, and a push must keep the sender admin.
To add a machine: open Add a node on the dashboard and follow the steps.
In short, the new machine starts with a peers.json holding just the desktop's
entry, runs yggstore serve, and you paste what yggstore id -name NAME
prints. New uploads use it straight away; files already stored stay put.
Nodes on older software show "update yggstore" on the dashboard and are not
sent lists. Install the new binary on them and restart them; a node that has
never been sent a list also needs "admin": true on the admin node's entry in
its own peers.json, so that it accepts lists from it.
Every person runs their own node and their own dashboard. The dashboard listens on localhost only and holds your stubs, which carry the keys to your files, so it is never shared. What people share is the network of nodes.
To send someone an item, add them under Sharing → Contacts with their
sharing code (ys1…, shown on their dashboard), then press Share on the
item. You get a .ysend file to send by email or any other way. Only that
person can open it; the item's name, your note and its key are sealed inside,
and opening it proves which sharing code sent it. They drop it into their
outfiles, and it appears under Shared with you, where they can restore
it. They need a node in this network, since nodes serve shards to members
only.
~/.yggstore/sharing.key (contacts in
~/.yggstore/contacts.json). Losing it means you can't open what people
send you any more, and they need your new code.X-Yggstore header, and
requests that name another host, so other web pages in your browser
cannot press its buttons.Each node reports how many bytes each uploader has stored on it. The dashboard
totals this per person (set with "owner" in peers.json; defaults to the
machine): what they use across the network, and what their nodes hold for
others. The aim is to hold about 1.5× what you use. For now it is accounting
only; nothing is enforced.
bin/yggstore dashboard -peers peers.json -outfiles outfiles # dashboard + watcher
bin/yggstore watch -peers peers.json -dir outfiles # watcher only, logs to stdout
outfiles/ drop files in, or folders of them → each file is sharded, then
replaced by NAME.ystub beside it, so the folder tree stays
outfiles/received/ items people sent you (drop their .ysend anywhere in outfiles)
outfiles/whole/ drop a file or folder in → stored as one item (a folder becomes
one archive, restored as a whole)
outfiles/restore/ drop any .ystub in → rebuilt into restored/
outfiles/restored/ restored files and folders
outfiles/delete/ drop a .ystub in → the item is removed from every node, then its stub
outfiles/.yggstore/ stubs that have been restored (kept for reference)
.part, .crdownload, .tmp and the like are ignored.whole/ are streamed as a tar archive and stored as one item. On restore,
only regular files and folders are written, and paths that would escape the
destination are rejected. Symlinks are skipped.-keep keeps it anyway)..NAME.ystub.challenges.json
next to it.delete/ folder, the dashboard's Delete button, or
yggstore rm STUB) removes every shard first. Only when all are gone are the
stubs and challenges removed; if a node is down, the stub stays in delete/
and is retried every 2 minutes, so no shard is left without a record.
Deleting cannot be undone.restored/.
A restore never overwrites: a second copy becomes name (2).ext.bin/yggstore dashboard -peers peers.json -stubs stubs # http://127.0.0.1:7480
Every 3 seconds the dashboard asks each peer for /v1/info (up/down, round
trip, shard count, disk use) and checks, for every .ystub under -stubs,
which shards their holders still have (HEAD /shards/{hash}, nothing is
downloaded). A file is healthy when every chunk has 6/6 shards,
degraded below that while 4 remain, and lost below 4. The Verify
button spends one stored challenge per shard. The activity log records nodes
going up or down, shard counts changing and file status changes. It listens on
localhost only by default.
To try it on one machine: scripts/local-demo.sh runs six nodes on
different ports of your Yggdrasil address, stores a file, stops two nodes and
restores it. scripts/local-demo.sh loopback does the same on ::1.
Tests: go test -race ./...
scripts/testnet.sh up # five test nodes t1..t5 in Docker, each with its own Yggdrasil
scripts/testnet.sh status
bin/yggstore put -peers testnet/shared/peers.json FILE
docker stop yggtest-t2-1 yggtest-t4-1 # simulate two nodes failing
scripts/testnet.sh down # stop (keys and shards kept); `clean` deletes everything
The containers sit on their own bridge (br-yggtest) and find this machine's
Yggdrasil by link-local multicast, as LAN nodes do. The host firewall must let
the bridge in (sudo ufw allow in on br-yggtest). The test peer list is the
desktop plus t1..t5, so test files never land on the real nodes. All six share
one machine and disk: this tests behaviour, not real redundancy.
sharing.key.peers.json, pushed to every node; there is
no gossip or vouching yet.transport.Transport
is the seam for it.Sharded, encrypted storage between friends over Yggdrasil: lend each other disk space, send files by sending a sealed stub.
Go
1
15 commits
updated Oct 6, 2026
Proof of concept: sharded, encrypted file storage between peers on the Yggdrasil overlay, with no VPN coordinator: node IDs come from public keys, each node only answers group members, and storage challenges check that peers still hold what they were given.
The idea: people in a group lend each other disk space. Each file is encrypted, cut into pieces and spread over everyone's machines, so it survives any one machine failing. The small stub left behind holds the key, so sending someone a stub (sealed for them, by email or anything else) is a way to send them the file, with no cloud service in between.
Status: experimental. The cryptography has not been reviewed by anyone independent. Don't make it the only copy of anything you can't lose.
put splits the file into 4 MiB chunks, encrypts each with AES-256-GCM
(one random key per file, fresh nonce per chunk), and erasure-codes each
chunk into 4 data + 2 parity shards.FILE.ystub holds the manifest, including the key. Anyone with
the stub and access to the peers can read the file.get fetches shards in parallel, rejects any whose hash doesn't match,
and rebuilds each chunk from any 4 of its 6 shards.FILE.ystub.challenges.json
(keep this private). verify sends one per shard: "hash this byte range
with this nonce", and checks the answer.A Yggdrasil address (200::/7) is derived from the node's public key, so the
source address of a connection over the overlay identifies the caller. The
address is the node ID. Each server only answers callers whose address is in
peers.json (default deny), binds only to its overlay address (or, with the
built-in Yggdrasil, has nothing on the machine's network
at all), and only lets the original writer delete a shard.
Download yggstore for your system from the
releases page (Linux on
PCs, Raspberry Pis and arm64 boards, Windows, macOS), and check it against
SHA256SUMS. Or build it yourself with Go 1.24 or later:
go build -o bin/yggstore ./cmd/yggstore
bin/yggstore id # prints your node ID and peers.json entry
# collect one entry from every machine into peers.json, copy it to each one
bin/yggstore serve -peers peers.json # on every machine (port 7400 by default)
bin/yggstore status -peers peers.json
bin/yggstore put -peers peers.json photo.jpg # -> photo.jpg.ystub
bin/yggstore verify photo.jpg.ystub
bin/yggstore get -o copy.jpg photo.jpg.ystub
bin/yggstore rm photo.jpg.ystub # delete its shards
peers.json:
[
{"name": "desktop", "addr": "[200:1234:5678::1]:7400", "admin": true},
{"name": "pi", "addr": "[201:abcd::1]:7400", "slow": true}
]
Each chunk is 6 shards, and no node gets more than 2 of them, so losing any
one node never loses data. A node marked "slow": true gets one shard per
chunk while the others have room for the rest, so it doesn't hold uploads
back. Only the uploading machine's peers.json decides this.
Nodes that run on the same machine (Docker containers, say) should share a
"host" tag, e.g. {"name": "t1", "addr": "...", "host": "desktop"}. The
2-shard limit then applies per machine, and uploads need 3 machines online,
not just 3 nodes.
On an admin dashboard, open Invite someone, type their name and press
Make invite. You get a message to send them, with a one-time invite
(yggjoin1:…, valid 7 days). They need Yggdrasil running and connected
(the message lists public peers), and the yggstore program; then:
yggstore join -service yggjoin1:… # -name NODE -me NAME -quota GB to change the defaults
join asks the admin node to add this machine (over Yggdrasil, presenting the
invite), saves the member list, adds the inviter as a contact, and with
-service starts the node and dashboard as user services. On the admin side
the newcomer is added to peers.json (owner: their name) and to your contacts,
and the list sync tells every node. An invite works once; the admin node
only keeps a hash of it. A node on a machine that is already a member is
tagged with that machine automatically; otherwise tag it with "host" by
hand if you know two nodes share a box.
The join endpoint is the only call a non-member can make, and it does nothing without a valid invite (rate-limited to 6 tries a minute).
An "admin": true node (the desktop) keeps every node's list in step. Each
node reports which list it holds; the dashboard sends its own peers.json to any
node that holds another one, so a node added on the dashboard, or a hand edit
of the desktop's peers.json, reaches every node within a minute, with no
restarts. A node keeps a pushed list in its data folder
(peers.pushed.json), and uses whichever of that and its own -peers file
changed last. Only admins may push, and a push must keep the sender admin.
To add a machine: open Add a node on the dashboard and follow the steps.
In short, the new machine starts with a peers.json holding just the desktop's
entry, runs yggstore serve, and you paste what yggstore id -name NAME
prints. New uploads use it straight away; files already stored stay put.
Nodes on older software show "update yggstore" on the dashboard and are not
sent lists. Install the new binary on them and restart them; a node that has
never been sent a list also needs "admin": true on the admin node's entry in
its own peers.json, so that it accepts lists from it.
Every person runs their own node and their own dashboard. The dashboard listens on localhost only and holds your stubs, which carry the keys to your files, so it is never shared. What people share is the network of nodes.
To send someone an item, add them under Sharing → Contacts with their
sharing code (ys1…, shown on their dashboard), then press Share on the
item. You get a .ysend file to send by email or any other way. Only that
person can open it; the item's name, your note and its key are sealed inside,
and opening it proves which sharing code sent it. They drop it into their
outfiles, and it appears under Shared with you, where they can restore
it. They need a node in this network, since nodes serve shards to members
only.
~/.yggstore/sharing.key (contacts in
~/.yggstore/contacts.json). Losing it means you can't open what people
send you any more, and they need your new code.X-Yggstore header, and
requests that name another host, so other web pages in your browser
cannot press its buttons.Each node reports how many bytes each uploader has stored on it. The dashboard
totals this per person (set with "owner" in peers.json; defaults to the
machine): what they use across the network, and what their nodes hold for
others. The aim is to hold about 1.5× what you use. For now it is accounting
only; nothing is enforced.
bin/yggstore dashboard -peers peers.json -outfiles outfiles # dashboard + watcher
bin/yggstore watch -peers peers.json -dir outfiles # watcher only, logs to stdout
outfiles/ drop files in, or folders of them → each file is sharded, then
replaced by NAME.ystub beside it, so the folder tree stays
outfiles/received/ items people sent you (drop their .ysend anywhere in outfiles)
outfiles/whole/ drop a file or folder in → stored as one item (a folder becomes
one archive, restored as a whole)
outfiles/restore/ drop any .ystub in → rebuilt into restored/
outfiles/restored/ restored files and folders
outfiles/delete/ drop a .ystub in → the item is removed from every node, then its stub
outfiles/.yggstore/ stubs that have been restored (kept for reference)
.part, .crdownload, .tmp and the like are ignored.whole/ are streamed as a tar archive and stored as one item. On restore,
only regular files and folders are written, and paths that would escape the
destination are rejected. Symlinks are skipped.-keep keeps it anyway)..NAME.ystub.challenges.json
next to it.delete/ folder, the dashboard's Delete button, or
yggstore rm STUB) removes every shard first. Only when all are gone are the
stubs and challenges removed; if a node is down, the stub stays in delete/
and is retried every 2 minutes, so no shard is left without a record.
Deleting cannot be undone.restored/.
A restore never overwrites: a second copy becomes name (2).ext.bin/yggstore dashboard -peers peers.json -stubs stubs # http://127.0.0.1:7480
Every 3 seconds the dashboard asks each peer for /v1/info (up/down, round
trip, shard count, disk use) and checks, for every .ystub under -stubs,
which shards their holders still have (HEAD /shards/{hash}, nothing is
downloaded). A file is healthy when every chunk has 6/6 shards,
degraded below that while 4 remain, and lost below 4. The Verify
button spends one stored challenge per shard. The activity log records nodes
going up or down, shard counts changing and file status changes. It listens on
localhost only by default.
To try it on one machine: scripts/local-demo.sh runs six nodes on
different ports of your Yggdrasil address, stores a file, stops two nodes and
restores it. scripts/local-demo.sh loopback does the same on ::1.
Tests: go test -race ./...
scripts/testnet.sh up # five test nodes t1..t5 in Docker, each with its own Yggdrasil
scripts/testnet.sh status
bin/yggstore put -peers testnet/shared/peers.json FILE
docker stop yggtest-t2-1 yggtest-t4-1 # simulate two nodes failing
scripts/testnet.sh down # stop (keys and shards kept); `clean` deletes everything
The containers sit on their own bridge (br-yggtest) and find this machine's
Yggdrasil by link-local multicast, as LAN nodes do. The host firewall must let
the bridge in (sudo ufw allow in on br-yggtest). The test peer list is the
desktop plus t1..t5, so test files never land on the real nodes. All six share
one machine and disk: this tests behaviour, not real redundancy.
sharing.key.peers.json, pushed to every node; there is
no gossip or vouching yet.transport.Transport
is the seam for it.