Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Conventional Commits by release-please.

## Unreleased

- Privacy policy version 6: "What websites can see" now names every way a page can hide
text that Kotiko still swaps (a transparent text colour, text turned away), and says a
disguised word's card can open on a click as well as a resting pointer.

- Security: a web page could restyle a swapped word (it sits in the page) so that it
covered the page invisibly; then your pointer resting anywhere, or your click on one of
the page's own buttons, opened the word's card, and the page could search the card's
Expand Down
2 changes: 1 addition & 1 deletion docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ earned, and gold once silver is; the answers can be entered before that.
|---|---|---|---|---|---|
| `crypto_used_network` | MUST [J] | Met | Kotiko's own outbound traffic uses HTTPS by default: model providers, OpenRouter, Telegram and Wiktionary. The server listens on 127.0.0.1 by default, so its plain-HTTP API doesn't leave the computer unless the owner sets BIND to another address; then the server logs a warning at start (daily for a public address), and the extension warns under the address field when the token would cross a network unencrypted. A plain-HTTP model endpoint (such as a local Ollama) is used only when the user enters one. | `server/lib/kotiko/config.ex:19`; `docs/security/assurance-case.md:29`; `extension/lib/url.js:85-99`; `docs/reference/configuration.md` (LLM_URL default `https://openrouter.ai/api/v1`) | – |
| `crypto_tls12` | MUST [J] | Met | TLS comes from Erlang/OTP (26 and newer, which offer TLS 1.2 and 1.3 by default) for the server's outbound requests and from the browser for the extension. Kotiko never sets or lowers the TLS version. | `.github/workflows/ci.yml:46-47` (OTP 26.2 and 28.1); `grep -rn` for versions:, :tlsv, ssl: in `server/lib server/config`: no match | – |
| `hardened_site` | MUST [URL] | Unmet | The repository (https://github.com/ScriptKittyOS/kotiko) and its release downloads are served by GitHub with HTTP Strict Transport Security, a Content Security Policy, X-Frame-Options: deny and X-Content-Type-Options: nosniff. The project website kotiko.org is not published yet; it is to be served through Cloudflare with the same headers before it goes live. | `curl -sI https://github.com/ScriptKittyOS/kotiko`: strict-transport-security, x-frame-options: deny, x-content-type-options: nosniff, content-security-policy present; `dig +short kotiko.org A` empty (NS at Cloudflare only); `slices/44-docs-site/SPEC.md:447-451` | Maintainer: publish kotiko.org with HSTS, CSP, X-Frame-Options and nosniff set (slice 44 going live) and check them, or set this entry's homepage URL to the GitHub repository |
| `hardened_site` | MUST [URL] | Met | The project website https://kotiko.org/ is served through Cloudflare with HTTP Strict Transport Security (max-age one year), a Content Security Policy (only the site's own files; a stricter one on /connect/; frame-ancestors 'none'), X-Frame-Options: DENY, X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer, over TLS 1.2 or later with Full (strict) TLS to the origin, and it loads nothing from other hosts. The repository and its release downloads are served by GitHub with the same kinds of headers. | `curl -sI https://kotiko.org/` and `/privacy/`, `/connect/` (2026-10-07): strict-transport-security max-age=31536000, content-security-policy, x-frame-options: DENY, x-content-type-options: nosniff, referrer-policy: no-referrer; a Chromium load of 6 pages plus a search makes no request to another host (security review C-10, reviewers E and F); `slices/44-docs-site/SPEC.md:447-451` | – |
| `security_review` | MUST [J] | Unmet | Not yet as gold requires. A line-by-line adversarial review with probes of the extension and the server (2026-10-01, docs/research/06-adversarial-qa.md) found and fixed an authentication bypass and other issues, but it predates the written security requirements. A pre-release review by three independent reviewers against the security requirements and trust boundaries is planned before the first store release; its report will be published in docs/security. | `docs/research/06-adversarial-qa.md:1-20`; `scripts/check-security-gate.mjs:5-15` (expects `docs/security/review-<tag>.md` with a closed gate); `ls docs/security` (no review report) | Slice 54 review (maintainer runs it; report in `docs/security/review-v1.0.0.md`) |
| `hardening` | MUST [URL] [N/A J] | Met | Extension pages run under an explicit content security policy (script-src 'self'; object-src 'self') with no inline scripts, and ESLint forbids HTML string sinks (innerHTML, outerHTML, insertAdjacentHTML) so a bug can't turn word data into markup. The server checks the Host header against an allowlist before anything else (against DNS rebinding), caps request bodies, sends no CORS headers, marks answers no-store, binds to loopback by default, compares tokens in constant time, and filters secrets out of logs. Planned: X-Content-Type-Options and similar headers on server responses, and a sandboxed systemd unit. Evidence: https://github.com/ScriptKittyOS/kotiko/blob/main/extension/manifest.json and https://github.com/ScriptKittyOS/kotiko/blob/main/docs/security/assurance-case.md | `extension/manifest.json:46`; `eslint.config.js:10-13,45-49`; `server/lib/kotiko/router.ex:11,20-21,33,214`; `docs/security/assurance-case.md:30,34` | – |

Expand Down
22 changes: 13 additions & 9 deletions docs/privacy/en.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kotiko privacy policy

Version 5 · 7 October 2026
Version 6 · 7 October 2026

Kotiko is a browser extension that swaps words on the web pages you read for words you're
learning in other languages. This page says exactly what it keeps, what it sends, and to
Expand Down Expand Up @@ -110,19 +110,20 @@ Kotiko and, for each word it swapped, which of the site's own words you have a w
that word, and its language.

Kotiko limits this to what it shows you. It swaps only text that is on your screen or
near it, never text the page hides: not text it doesn't display, makes see-through or
nearly so (also with a filter), or puts under a clipping shape or a mask (Kotiko leaves
text under those alone even where you can see it). In one visit to a page it swaps at
most 500 different words, each in at most 3 of your languages. Text you can't make out
still counts as shown: very small text, text in the page's background colour, blurred
text, or text under something else. So a site can't read your whole word list at once,
near it, never text the page hides in these ways: text it doesn't display, fades out
with opacity (also through a filter) until it's see-through or nearly so, or puts under a
clipping shape or a mask (Kotiko leaves text under those alone even where you can see
it). In one visit to a page it swaps at most 500 different words, each in at most 3 of
your languages. Text you can't make out in other ways still counts as shown: very small
text, text in the page's background colour or in a transparent colour, blurred or
darkened text, text turned away from you, or text under something else. So a site can't read your whole word list at once,
but one you visit often, or that keeps reloading itself, can learn more of it over time.

Only you open a word's card: by pointing at, clicking, tapping or pressing Enter on a
swapped word you can see, where it's drawn. A site can't open one by itself, or by
stretching an invisible word over its page; one that moves a word under your pointer and
disguises it (in the page's background colour, or under something else) could still get
its card opened while your pointer rests there. While you have a card open, a site that
disguises it (in the page's background colour or a transparent colour, or under something
else) could still get its card opened while your pointer rests there or when you click. While you have a card open, a site that
searches its page for a word it guesses could find the card's text.

On a site you paused, on sites Kotiko leaves alone (such as banks), and while Kotiko is
Expand Down Expand Up @@ -192,6 +193,9 @@ everything in Kotiko's Settings.

## Changelog

- **Version 6, 7 October 2026.** "What websites can see" now names every way a page can
hide text that Kotiko still swaps (a transparent text colour, text turned away), and
says a disguised word's card can open on a click as well as a resting pointer.
- **Version 5, 7 October 2026.** "What websites can see" now says which hidden text
Kotiko never swaps (also text made see-through with a filter, clipped or masked), what
text still counts as shown, and when a word's card opens.
Expand Down
2 changes: 1 addition & 1 deletion docs/privacy/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Last checked against the code: 2026-10-06 (extension 0.2.0, spec 2.0.0).
| Text you type or paste to add words (the popup, the dashboard, the welcome page, bulk add), plus your base languages, up to five of your recent target languages and a language hint | Add jobs in IndexedDB (`meta`), with a copy in `storage.local` for the popup (kept 7 days after they finish, at most 20); the checked result in a lookup cache in IndexedDB (30 days, at most 5,000) | Yes, when you add a word that needs a lookup. Never for "word = meaning" lines | The provider you chose (OpenRouter by default; OpenAI, Anthropic, Google Gemini, Groq, a local Ollama or LM Studio, or any address you enter), or your Kotiko server, which asks its own provider | `lib/llm/client.js` `call()`, `background.js` `lookupJob()` |
| Your saved words (word, meaning, sense, base language), to write their pronunciations | Nothing new | Yes, for words saved without a pronunciation (added by hand, imported, or from an older version), at most twice per word, in the background, when words live in this browser and a provider is set up | The provider you chose | `lib/refresh-job.js` → `client.respell()` |
| A word you add in a language with word stress (Russian, Ukrainian, Spanish and others in `spec/pronunciation.json`): the word only | The Wiktionary page's pronunciation section, 30 days, in the lookup cache | Yes, when you add the word with a lookup or as "word = meaning", and once in the background for words saved earlier | The Wikimedia Foundation (en.wiktionary.org), with a `Api-User-Agent` header naming Kotiko and `hello@scriptkittyos.com` | `background.js` `wiktionaryPage()`, `lib/wiktionary-pass.js` |
| Your provider key and your server's access token | IndexedDB (`secrets`), which only the background reads. Never in `storage.local`, `storage.sync` or `storage.session`; never shown again in full (the settings show the first six and last four characters) | The provider key: only to the service it belongs to, and only at an address chosen on one of Kotiko's own pages (slice 28 §7). The server's token: never; each request to the server is signed with it (`Authorization: Kotiko-HMAC v1 ts, nonce, body hash, mac`) and the server's answers must carry its signature (slice 54, D-01) | Your provider; nobody (your server gets signatures made with the token) | `background.js` `secret()`, `secretFor()`, `routeAllows()`, `request()`; `lib/server-auth.js` |
| Your provider key and your server's access token | IndexedDB (`secrets`), which only the background reads. Never in `storage.local`, `storage.sync` or `storage.session`; never shown again in full (the settings show the first six and last four characters) | The provider key: only to the service it belongs to, and only at an address chosen on one of Kotiko's own pages (slice 28 §7). The server's token: never; each request to the server is signed with it (`Authorization: Kotiko-HMAC v2 ts, nonce, boot, body hash, mac`, bound to the server's current boot) and the server's answers must carry its signature (slice 54, D-01 and E-01) | Your provider; nobody (your server gets signatures made with the token) | `background.js` `secret()`, `secretFor()`, `routeAllows()`, `request()`; `lib/server-auth.js` |
| Which provider, model and server address you use; whether a key is saved (yes or no, never the key) | IndexedDB (`meta`), with a copy in `storage.local` that pages read | Each request goes to that address | Your provider or your server | `lib/local-mode.js` `readSettings()` |
| The model list (any provider whose models Kotiko lists) and your remaining free lookups (OpenRouter only) | IndexedDB `meta`; the list refreshed at most daily, the count at most every 5 minutes while lookups run | The request itself, with your key | Your provider (`/models`); OpenRouter (`/key`) | `lib/llm/client.js`, `lib/llm/catalog.js` |
| The Test button's lookup | Nothing | The word "hello" and your base languages | Your provider | `lib/llm/client.js` `test()` |
Expand Down
2 changes: 1 addition & 1 deletion docs/security/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Kotiko has two parts: the browser extension, and an optional server you run your

### The server

- **Nothing but `/health` answers without your token.** Every other request, whatever its
- **Nothing but `GET /health` and `POST /api/v1/proof` answers without your token** (the proof route shows the server holds the token without revealing it). Every other request, whatever its
method or however its path is spelled (`/%61pi`, `/API`, `//api`), gets `401` unless it
carries `Authorization: Bearer <your token>` or is signed with it (the extension signs;
it never sends the token, and uses an answer only if the server signed it too). A signed
Expand Down
Loading