1
0
Fork 0

feat: Add coller la petite post

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-08-21 20:53:43 +02:00
parent f0cab2f7c3
commit 47ca98cc16
Signed by: jriou
GPG key ID: 9A099EDA51316854
6 changed files with 753 additions and 0 deletions

View file

@ -0,0 +1,207 @@
+++
title = 'Coller (la petite)'
date = 2025-08-21T20:00:00+02:00
+++
At the beginning of my career, we used e-mails to communicate with co-workers.
We could explain an issue in details with a very long e-mail, but it was hard
to follow live discussions. For synchronous communication, we used phones. We
had to constantly switch between both systems. Have you ever received a call
saying "have you seen my last e-mail?".
Then instant messaging systems came to fill the gap. There was
[MSN](https://en.m.wikipedia.org/wiki/MSN_Messenger) at home to chat with your
friends. Or XMPP at work. I have used this protocol for 10 years. OK there was
no gif, limited support of emojis and you can't call your colleagues but it
could ran everywhere, even on a [solarpunk phone](http://compost.party/).
Nowadays, we have Slack, Webex, Microsoft Teams, Discord. Maybe you have the
chance to use an open source solution like Matrix or Mattermost. The decision
to use one of another has probably been made before you joined the company and
you don't have enough power to introduce a major change because communication
systems are central to companies. So we use Webex at work. We have to deal with
all the limitations. One of them is the ability to paste long text without
polluting the feed.
# Pastebin service
In french, "paste" could be translated to "coller". One day, I decided to start
my own pastebin service based on [sticky
notes](https://github.com/sayakb/sticky-notes). I have ordered a VPS, an
_internal_ top-level domain, restricted network access. The _coller la petite_
service was born (in reference to [this
song](https://genius.com/Franko-coller-la-petite-lyrics)). The name was fun.
The service was useful. I did no marketing campaign around it because I knew it
could be used to store sensible data. I have contacted our security team to
tell them the service existed and told every user that it's an **external**
service with no service level agreement (_best effort_ mode).
This website had little to no maintenance. Upgrade Debian packages, dump and
restore the database on a new major version every year, easy peasy. For fun, I
have created a Perl client to learn this language (because I had to). Then a
more portable Go client was created by one of my team mates. Why do we need
clients when there's already a website? Because sometimes we prefer to use a
CLI and not a browser to share the content of our clipboard or the content of a
file.
The service became so popular that the need was proven. The team responsible
for managing collaboration tools came to me to see how could we move this
external service and make it internal: hosted on private services with the
enterprise security layer on top of it. I have explained the history, how it
works and how I see the future to multiple people of that team. There is a high
turnover rate apparently. Now I'm sick of explaining it over and over again.
# What's the problem?
There are multiple issues with this service today. Sticky notes is not
maintained anymore. It's written in PHP which makes it harder for us to fork,
contribute and deploy. Last but not least, there is no encryption.
# PrivateBin
The most popular and secure pastebin service out there is
[PrivateBin](https://privatebin.info/). They take security very seriously. The
content is encrypted from the browser so the hosting provider never knows
what's inside. While this solution is awesome for the security, it makes it
harder to use on a day to day basis. We can't use [curl](https://curl.se/) to
download the raw content of a note. I get the argument of users always making
the poorest choice but I don't want to set up a second solution to fill the gap
or telling people to deal with it. I want a single solution for doing both. I
want to paste public data. I also want to paste and download snippets securely.
I also don't like long URLs because, unlike Mastodon where size doesn't matter,
they tend to bloat the message on those platforms. The fact that you have to
use an URL shortener in the
[FAQ](https://github.com/PrivateBin/PrivateBin/wiki/FAQ#the-url-is-so-long-cant-i-just-use-an-url-shortener)
says it all.
# Coller
So I created a pastebin server and clients to create and read notes. Yep. Yet
another pastebin solution. Not because we definitely needed one more, but
because I use it daily, I like simplicity, usability, security and I love doing
side projects in Go.
## Features
- Website
- Raw content by default
- Compatible with curl
- Encryption with password
- Compression on the backend
- Automatic retention
- PostgreSQL or sqlite database
- CLI to create notes ("coller") from the clipboard or a file
- CLI to read encrypted notes ("copier")
- Easy to deploy
---
![Screenshot of the landing page](/coller/coller-index.png)
---
![Screenshot of the page saying "Note created sucessfully" with the note
URL](/coller/coller-created.png)
---
![Screenshot of the "coller" CLI creating a note from a file and reading the
node with "copier"](/coller/coller-cli.png)
---
# Encryption levels
## No encryption
[It starts with](https://www.youtube.com/watch?v=eVTXPUF4Oz4) no encryption.
Sounds crazy in a world where everything has to be encrypted, right? Not so
crazy. Sometimes, we just want to paste a bunch of characters that we could
have been said publicly on social networks and that's fine. The URL will be
very short and easy to share.
## Server side encryption
The next level is to use basic encryption by letting the website generate a
password for you. You click on a checkbox and the content of the note is
encrypted before being stored. The encryption key is the password. You can
choose to keep the key for yourself and only you and the server know it. Or
share it to other people you trust. Anyway, the server knows it.
## Server side encryption with user key
Same solution as the server side encryption but this time, the user defines the
password. Security could be lowered if the key is weak (too short, words from
the dictionary) when the server will always generate strong enough keys. In the
end, the server still knows your choosen key.
## Client side encryption
If you don't trust the server but you want to use the service anyway, you could
tell the client to encrypt the content before sending it to the server. The
client will generate a random (strong) key. This time, the key is private. Only
you knows it. You can do whatever you want with the key, but you should keep it
to yourself or share it carefully.
Accessing the encrypted note using the password in the URL will make the server
aware of your password. You should use the "copier" client to download the
encrypted note and decrypt outside of the server.
## Client side encryption with user key
Same as client side encryption but you can choose the password you want.
## Encryption summary
A picture is worth a thousand words:
![Diagram with the server on the left and two URL: one with the note ID, one
with the note ID and the password. On the right, there are the copier and the
web clients. Clients and server are separated by a straight line. Three lines
are linking clients and the server. The first one is between the copier client
and the server where the key icon is on the client for client-side encryption.
The second one is between the web client and the URL without password to show
no encryption. The last one is between the web client and the server with a key
icon on the URL below the server to show the server-side
encryption.](/coller/coller-encryption.svg)
# What to choose?
| Level | Usability | Confidentiality |
| ---------------------- | --------- | --------------- |
| No encryption | ⭐⭐ | ⭐ |
| Server-side encryption | ⭐⭐⭐ | ⭐⭐ |
| Client-side encryption | ⭐ | ⭐⭐⭐ |
If you can use the copier client (CLI) and keep the password for yourself, go
for it. It's the most secure solution. If you can't, the website is designed to
use the server-side encryption with a strong generated password.
# Cipher
The most famous cipher is
[AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) 256 with
[GCM](https://en.wikipedia.org/wiki/Galois/Counter_Mode). It would have been
simpler to implement
[CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation) like
[pgBackRest](https://pgbackrest.org/) but the mode has been removed from TLS
1.3 for several flaws. I decided to use
[XChaha20-Poly1305](https://pkg.go.dev/golang.org/x/crypto/chacha20) because it
reminds me [this banger](https://www.youtube.com/watch?v=DGsL8hA-1rE) from this
year's Eurovision and I wanted to use something different. Don't worry, this
cipher is [one of the most secure in the
world](https://wiki.mozilla.org/Security/Server_Side_TLS).
# Source code
The source code is available [on my forgejo](https://git.riou.xyz/jriou/coller)
instance under the permissive
[MIT](https://git.riou.xyz/jriou/coller/src/branch/main/LICENSE) license.
# Conclusion
This little side project turned into a fully functional pastebin solution. It
was also a great excuse to implement encryption in Go. If you find a security
issue or want to implement a feature or fix bugs, contributions are welcomed.

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,542 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "yKFkkk4Yj5Zauko2WKG-d",
"type": "rectangle",
"x": 555.25,
"y": 331,
"width": 179.99999999999997,
"height": 212,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "as",
"roundness": {
"type": 3
},
"seed": 760287448,
"version": 140,
"versionNonce": 1684089560,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "sNYRyKWZkChLuCLsm0VYi"
}
],
"updated": 1755789246679,
"link": null,
"locked": false
},
{
"id": "sNYRyKWZkChLuCLsm0VYi",
"type": "text",
"x": 592.5250015258789,
"y": 413,
"width": 105.44999694824219,
"height": 48,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "at",
"roundness": null,
"seed": 1834769832,
"version": 114,
"versionNonce": 27660248,
"isDeleted": false,
"boundElements": null,
"updated": 1755789246679,
"link": null,
"locked": false,
"text": "Server\n(collerd)",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "yKFkkk4Yj5Zauko2WKG-d",
"originalText": "Server\n(collerd)",
"autoResize": true,
"lineHeight": 1.2
},
{
"id": "Kc7_mrdgSLrf6jNGvILS3",
"type": "rectangle",
"x": 1175.75,
"y": 554,
"width": 141.00000000000003,
"height": 156,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "au",
"roundness": {
"type": 3
},
"seed": 545326552,
"version": 444,
"versionNonce": 652351192,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "CepAMVAuKJpOQI-AzdS4a"
},
{
"id": "ocyk_yJCiGwmnkR55FoTJ",
"type": "arrow"
},
{
"id": "BkLH2VDz86-eIlEB8PA3M",
"type": "arrow"
}
],
"updated": 1755789238168,
"link": null,
"locked": false
},
{
"id": "CepAMVAuKJpOQI-AzdS4a",
"type": "text",
"x": 1211.099998474121,
"y": 608,
"width": 70.30000305175781,
"height": 48,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "av",
"roundness": null,
"seed": 51546840,
"version": 421,
"versionNonce": 1445250264,
"isDeleted": false,
"boundElements": [],
"updated": 1755789187612,
"link": null,
"locked": false,
"text": "Client\n(web)",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "Kc7_mrdgSLrf6jNGvILS3",
"originalText": "Client\n(web)",
"autoResize": true,
"lineHeight": 1.2
},
{
"id": "iEFEFdLxEQSOj9PksxsbT",
"type": "text",
"x": 499.25,
"y": 584,
"width": 257.76666259765625,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "aw",
"roundness": null,
"seed": 1132344792,
"version": 117,
"versionNonce": 151768792,
"isDeleted": false,
"boundElements": null,
"updated": 1755788934562,
"link": null,
"locked": false,
"text": "https://coller/note-id",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "https://coller/note-id",
"autoResize": true,
"lineHeight": 1.2
},
{
"id": "8nsGIc_KHl1TFnpg6ejUo",
"type": "text",
"x": 499.3666687011719,
"y": 661,
"width": 399.9333190917969,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "ax",
"roundness": null,
"seed": 1693301208,
"version": 179,
"versionNonce": 1850772184,
"isDeleted": false,
"boundElements": [],
"updated": 1755789170471,
"link": null,
"locked": false,
"text": "https://coller/note-id/password 🔑",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "https://coller/note-id/password 🔑",
"autoResize": true,
"lineHeight": 1.2
},
{
"id": "_z-MUabVHx5E8_l4BWJ1z",
"type": "line",
"x": 1045.25,
"y": 274,
"width": 0,
"height": 461,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dotted",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "ay",
"roundness": {
"type": 2
},
"seed": 228580056,
"version": 141,
"versionNonce": 528663256,
"isDeleted": false,
"boundElements": null,
"updated": 1755788967611,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
461
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": null,
"polygon": false
},
{
"id": "JouKOO4uPqBFGr6XZX_py",
"type": "rectangle",
"x": 1174.75,
"y": 277,
"width": 141.00000000000003,
"height": 156,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "az",
"roundness": {
"type": 3
},
"seed": 496191400,
"version": 519,
"versionNonce": 1511097560,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "VEdH7Z2EKX0em85vXetqX"
}
],
"updated": 1755789290129,
"link": null,
"locked": false
},
{
"id": "VEdH7Z2EKX0em85vXetqX",
"type": "text",
"x": 1198.3833351135254,
"y": 331,
"width": 93.73332977294922,
"height": 48,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b00",
"roundness": null,
"seed": 1578898088,
"version": 520,
"versionNonce": 1421427624,
"isDeleted": false,
"boundElements": [],
"updated": 1755789189329,
"link": null,
"locked": false,
"text": "Client\n(copier)",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "JouKOO4uPqBFGr6XZX_py",
"originalText": "Client\n(copier)",
"autoResize": true,
"lineHeight": 1.2
},
{
"id": "ktbpPk_s1UpLcnSnu93ef",
"type": "text",
"x": 1183.2833404541016,
"y": 457,
"width": 411.6499938964844,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b03",
"roundness": null,
"seed": 1415160792,
"version": 493,
"versionNonce": 21443032,
"isDeleted": false,
"boundElements": [],
"updated": 1755789281126,
"link": null,
"locked": false,
"text": "copier -password PASSWORD 🔑 <URL> ",
"fontSize": 20,
"fontFamily": 3,
"textAlign": "left",
"verticalAlign": "top",
"containerId": null,
"originalText": "copier -password PASSWORD 🔑 <URL> ",
"autoResize": true,
"lineHeight": 1.2
},
{
"id": "6CQhcN4aPSfHv0u_lyIDx",
"type": "arrow",
"x": 1161.25,
"y": 477,
"width": 361,
"height": 104,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b04",
"roundness": {
"type": 2
},
"seed": 2133643432,
"version": 109,
"versionNonce": 574848728,
"isDeleted": false,
"boundElements": null,
"updated": 1755789311725,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-361,
104
]
],
"lastCommittedPoint": null,
"startBinding": null,
"endBinding": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false
},
{
"id": "ocyk_yJCiGwmnkR55FoTJ",
"type": "arrow",
"x": 1158.25,
"y": 664,
"width": 237,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b05",
"roundness": {
"type": 2
},
"seed": 1443036840,
"version": 52,
"versionNonce": 1027734696,
"isDeleted": false,
"boundElements": null,
"updated": 1755789226112,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-237,
0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "Kc7_mrdgSLrf6jNGvILS3",
"focus": -0.41025641025641013,
"gap": 17.5
},
"endBinding": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false
},
{
"id": "BkLH2VDz86-eIlEB8PA3M",
"type": "arrow",
"x": 1153.25,
"y": 599,
"width": 351,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "b06",
"roundness": {
"type": 2
},
"seed": 2066376872,
"version": 194,
"versionNonce": 2036082856,
"isDeleted": false,
"boundElements": null,
"updated": 1755789319209,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-351,
0
]
],
"lastCommittedPoint": null,
"startBinding": {
"elementId": "Kc7_mrdgSLrf6jNGvILS3",
"focus": 0.42307692307692313,
"gap": 22.5
},
"endBinding": null,
"startArrowhead": null,
"endArrowhead": "arrow",
"elbowed": false
}
],
"appState": {
"gridSize": 20,
"gridStep": 5,
"gridModeEnabled": false,
"viewBackgroundColor": "#ffffff",
"lockedMultiSelections": {}
},
"files": {}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB