Puzzle Captcha API
Based on @xyzshantaram's slider-captcha.
This MR introduces an endpoint to get a puzzle captcha, and another one to verify the solution and mark the user as having solved a captcha. Users who solved a captcha will be able to bypass certain policy restrictions, like being able to post without a NIP-05 name.
When a puzzle is solved, the DITTO_NSEC publishes a NIP-32 label event tagging the user's pubkey, which looks like this:
{
kind: 1985,
tags: [
['L', 'pub.ditto.captcha'],
['l', 'solved', 'pub.ditto.captcha'],
['p', pubkey, Conf.relay],
],
}
Other parts of the server can then query for the presence of this event to decide policy actions.
GET /api/v1/ditto/captcha
Returns a puzzle captcha object, like this:
{
"id": "7657d68a-351f-4d31-b539-3b50b9fc4fd8",
"type": "puzzle",
"bg": "data:image/png;base64,...",
"puzzle": "data:image/png;base64,...",
"created_at": "2024-10-04T21:47:47.900Z",
"expires_at": "2024-10-04T21:52:47.900Z"
}
In the frontend, the images can be displayed like <img src={captcha.puzzle} />
for the piece, and <img src={captcha.bg} />
for the background. The frontend should let the user drag the piece into the correct position, and then get the coordinates in pixels based on the original size of the background.
Here is an example of the images:
Note that this endpoint is public, but it's rate-limited to 3 requests per minute.
POST /api/v1/ditto/captcha/:id/verify
Verify a captcha solution, and mark the user as having "solved" the captcha in the database. Authorization is required for this endpoint, so the frontend must have already called verify_credentials
, and be at least partially logged in.
The request body is just JSON of a point:
{
"x": 20,
"y": 53
}
The captcha ID is in the URL path itself.
Possible responses:
- HTTP 204 - success (no body)
- HTTP 400 - incorrect solution
- HTTP 422 - misbehaving client
This endpoint is rate-limited to 8 requests per minute.
Implementation Details
- This uses
@gfx/canvas-wasm
to draw the background and puzzle images with the Canvas API. - The puzzle icon is from Tabler and the backgrounds are pulled from elementary OS wallpapers.
- Captcha answers are kept in a TTL cache for 5 minutes, so they are automatically expired after 5 minutes. When a captcha is solved, it's deleted from the cache immediately to prevent replay attacks.
- All captcha images are cached in memory when the server starts, so it doesn't have to load them on each request.
Other Thoughts
- In Soapbox, we should display this in the login/signup modal when the user tries to log in, if they haven't solved a captcha already.
-
We probably need to add an extra field to user accounts. Something likeEDIT: I added aditto.captcha_solved
boolean, so we can detect whether to display the captcha to users in Soapbox.account.source.ditto.captcha_solved
field. - This is not a comprehensive solution on its own to stop spam. Yes people can write scripts with OpenCV or something to get around it. It's just a measure to slow attackers down.
- Later we could extend this endpoint to require proof-of-work, similar to what Proton Mail does.
- This is only useful to prevent spam from users of our own relays. Improvements to WoT are still needed to prevent spam across the network.
- Since a label event is published publicly, other clients (including other Ditto servers) can use those labels as a trust metric, as long as they trust the label author.