s-totpkit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- s_totpkit-0.1.0/.gitignore +52 -0
- s_totpkit-0.1.0/.gitlab-ci.yml +31 -0
- s_totpkit-0.1.0/AGENTS.md +45 -0
- s_totpkit-0.1.0/LICENSE +21 -0
- s_totpkit-0.1.0/PKG-INFO +87 -0
- s_totpkit-0.1.0/README.md +68 -0
- s_totpkit-0.1.0/pyproject.toml +32 -0
- s_totpkit-0.1.0/scripts/self_check.py +88 -0
- s_totpkit-0.1.0/skills/totp/SKILL.md +156 -0
- s_totpkit-0.1.0/src/totpkit/__init__.py +48 -0
- s_totpkit-0.1.0/src/totpkit/backend.py +394 -0
- s_totpkit-0.1.0/src/totpkit/bot/__init__.py +18 -0
- s_totpkit-0.1.0/src/totpkit/bot/config.py +151 -0
- s_totpkit-0.1.0/src/totpkit/bot/egress.py +214 -0
- s_totpkit-0.1.0/src/totpkit/bot/handlers.py +224 -0
- s_totpkit-0.1.0/src/totpkit/bot/keyboards.py +224 -0
- s_totpkit-0.1.0/src/totpkit/bot/runner.py +433 -0
- s_totpkit-0.1.0/src/totpkit/cli.py +268 -0
- s_totpkit-0.1.0/src/totpkit/codes.py +190 -0
- s_totpkit-0.1.0/src/totpkit/entries.py +385 -0
- s_totpkit-0.1.0/tests/conftest.py +35 -0
- s_totpkit-0.1.0/tests/test_bot.py +267 -0
- s_totpkit-0.1.0/tests/test_codes.py +79 -0
- s_totpkit-0.1.0/tests/test_entries.py +145 -0
- s_totpkit-0.1.0/uv.lock +1480 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# === atlas universal gitignore ===
|
|
2
|
+
|
|
3
|
+
# OS / IDE
|
|
4
|
+
.DS_Store
|
|
5
|
+
Thumbs.db
|
|
6
|
+
.vscode/
|
|
7
|
+
.idea/
|
|
8
|
+
*.swp
|
|
9
|
+
*.swo
|
|
10
|
+
|
|
11
|
+
# Sensitive
|
|
12
|
+
.env
|
|
13
|
+
.env.local
|
|
14
|
+
*.key
|
|
15
|
+
*.pem
|
|
16
|
+
secrets/
|
|
17
|
+
private/
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.py[cod]
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
|
|
28
|
+
# Node / JS
|
|
29
|
+
node_modules/
|
|
30
|
+
.next/
|
|
31
|
+
dist/
|
|
32
|
+
build/
|
|
33
|
+
|
|
34
|
+
# Temporary / large
|
|
35
|
+
*.log
|
|
36
|
+
*.tmp
|
|
37
|
+
nul
|
|
38
|
+
NUL
|
|
39
|
+
*.zip
|
|
40
|
+
*.rar
|
|
41
|
+
*.7z
|
|
42
|
+
|
|
43
|
+
# Media (selectively unignore via !path/*.ext if needed for fixtures)
|
|
44
|
+
*.mp4
|
|
45
|
+
*.mov
|
|
46
|
+
*.avi
|
|
47
|
+
*.mkv
|
|
48
|
+
|
|
49
|
+
# TOTP: секреты и локальные сторы — НИКОГДА в git
|
|
50
|
+
secrets.json
|
|
51
|
+
*.history.json
|
|
52
|
+
bot_config.json
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Автопубликация на PyPI по семвер-тегу vX.Y.Z через Trusted Publishing (OIDC).
|
|
2
|
+
# Токены НИГДЕ не хранятся: GitLab выдаёт короткоживущий OIDC-токен (PYPI_ID_TOKEN),
|
|
3
|
+
# он одноразово обменивается на PyPI API-токен. Издатель настраивается на pypi.org:
|
|
4
|
+
# Manage -> Publishing -> GitLab (namespace=S-kits, project=totpkit,
|
|
5
|
+
# top-level pipeline file=.gitlab-ci.yml, environment — пусто).
|
|
6
|
+
# Запускается ТОЛЬКО на тег vX.Y.Z (обычный push на main ничего не публикует).
|
|
7
|
+
stages: [publish]
|
|
8
|
+
|
|
9
|
+
publish-pypi:
|
|
10
|
+
stage: publish
|
|
11
|
+
image: ghcr.io/astral-sh/uv:python3.12-bookworm
|
|
12
|
+
rules:
|
|
13
|
+
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
|
|
14
|
+
id_tokens:
|
|
15
|
+
PYPI_ID_TOKEN:
|
|
16
|
+
aud: pypi
|
|
17
|
+
script:
|
|
18
|
+
- uv build
|
|
19
|
+
# обмен GitLab OIDC -> одноразовый PyPI API-токен (Trusted Publishing):
|
|
20
|
+
- |
|
|
21
|
+
export UV_PUBLISH_TOKEN=$(python3 - <<'PY'
|
|
22
|
+
import json, os, urllib.request
|
|
23
|
+
req = urllib.request.Request(
|
|
24
|
+
"https://pypi.org/_/oidc/mint-token",
|
|
25
|
+
data=json.dumps({"token": os.environ["PYPI_ID_TOKEN"]}).encode(),
|
|
26
|
+
headers={"Content-Type": "application/json"},
|
|
27
|
+
)
|
|
28
|
+
print(json.load(urllib.request.urlopen(req))["token"])
|
|
29
|
+
PY
|
|
30
|
+
)
|
|
31
|
+
- uv publish
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# AGENTS.md — totpkit
|
|
2
|
+
|
|
3
|
+
> Контекст для AI-ассистентов (Claude Code, ChatGPT, Cursor и т.п.), работающих
|
|
4
|
+
> над этим проектом.
|
|
5
|
+
|
|
6
|
+
## Что это
|
|
7
|
+
|
|
8
|
+
Кит s-totpkit: generic TOTP-хранилище (сервис/аккаунт/иконка/группа) + CLI totp + Telegram-бот выдачи кодов для всех навыков
|
|
9
|
+
|
|
10
|
+
## Atlas
|
|
11
|
+
|
|
12
|
+
Проект зарегистрирован в Atlas-БД (Atlas). Карточка:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
atlas project get totpkit
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Любые изменения метаданных (приоритет, статус, теги) — через atlas CLI:
|
|
19
|
+
|
|
20
|
+
- `atlas project update totpkit --priority P0` — поменять приоритет
|
|
21
|
+
- `atlas project tag add totpkit -t domain:<slug>` — добавить тег
|
|
22
|
+
- `atlas project move totpkit --to-type <type>` — конвертировать тип
|
|
23
|
+
|
|
24
|
+
## Тип / Статус (на момент создания)
|
|
25
|
+
|
|
26
|
+
- type=`personal-utility`, status=`experiment`, priority=`P1`
|
|
27
|
+
|
|
28
|
+
## Правила работы
|
|
29
|
+
|
|
30
|
+
- Все исходные тексты, документы, код проекта — в этом репо.
|
|
31
|
+
- Чувствительные данные (`.env`, токены, ключи) — игнорируются `.gitignore`.
|
|
32
|
+
- AI-ассистенту разрешено: читать, генерировать, редактировать в этом репо.
|
|
33
|
+
|
|
34
|
+
## Канонические команды
|
|
35
|
+
|
|
36
|
+
- `atlas project get totpkit` — карточка проекта
|
|
37
|
+
- `atlas task list --project totpkit` — задачи проекта (когда W7
|
|
38
|
+
волна будет реализована)
|
|
39
|
+
|
|
40
|
+
<!-- atlas:usage:start -->
|
|
41
|
+
## Управление проектом — через Atlas
|
|
42
|
+
|
|
43
|
+
Этот проект ведётся в Atlas (личная PM-система портфеля). Для задач/проектов/эпиков/бэкапов
|
|
44
|
+
используй CLI `atlas` и вызывай навык `atlas` — вся логика и роутинг внутри навыка.
|
|
45
|
+
<!-- atlas:usage:end -->
|
s_totpkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
s_totpkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: s-totpkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TOTP-хранилище и генератор одноразовых кодов для любых сервисов: CLI, библиотека и Telegram-бот выдачи кодов
|
|
5
|
+
Author: Dmitry Semyonov
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: keyring>=24
|
|
10
|
+
Requires-Dist: pyotp>=2.9
|
|
11
|
+
Requires-Dist: s-clikit>=0.1.5
|
|
12
|
+
Requires-Dist: s-librarykit>=0.7.7
|
|
13
|
+
Provides-Extra: bot
|
|
14
|
+
Requires-Dist: aiogram>=3.13; extra == 'bot'
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# totpkit — одноразовые коды (TOTP) для любых сервисов
|
|
21
|
+
|
|
22
|
+
Одно шифрованное хранилище вторых факторов на все сервисы: PyPI, GitHub,
|
|
23
|
+
Bitrix24, что угодно. Код можно получить командой, а можно кнопкой в Telegram,
|
|
24
|
+
когда ноутбука под рукой нет. Другие проекты берут коды из того же стора одной
|
|
25
|
+
строкой, не заводя собственных копий секретов.
|
|
26
|
+
|
|
27
|
+
## Установка
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv add s-totpkit # как зависимость
|
|
31
|
+
uv sync --extra bot # с Telegram-ботом
|
|
32
|
+
uv run python scripts/self_check.py
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Быстрый старт
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
totp entry add --uri "otpauth://totp/PyPI:alice?secret=…&issuer=PyPI"
|
|
39
|
+
totp list
|
|
40
|
+
totp code pypi # {"code": "366845", "seconds_left": 20}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Вывод — JSON по умолчанию; `--text` переключает на человекочитаемый.
|
|
44
|
+
|
|
45
|
+
## Из своего кода
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from totpkit import provider_for
|
|
49
|
+
|
|
50
|
+
login(username, password, totp_provider=provider_for("pypi"))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Провайдер отдаёт код по сигнатуре `(попытка, прошлая ошибка) -> код` и сам
|
|
54
|
+
разбирается с границей 30-секундного окна: ждёт ротацию, когда коду осталось
|
|
55
|
+
жить меньше трёх секунд, и никогда не отдаёт дважды один и тот же код — иначе
|
|
56
|
+
повторная попытка входа сгорает на коде, который сервис уже отверг.
|
|
57
|
+
|
|
58
|
+
## Модель хранения
|
|
59
|
+
|
|
60
|
+
Запись — это **сервис + аккаунт + иконка + группа**. Ключ строится как
|
|
61
|
+
`сервис` или `сервис:аккаунт` и служит тем, что вы набираете руками: пока
|
|
62
|
+
аккаунт один, работает короткое `totp code pypi`; когда их стало два, короткая
|
|
63
|
+
форма даёт ошибку со списком вариантов, а не выдаёт код наугад.
|
|
64
|
+
|
|
65
|
+
Групп по умолчанию нет — всё лежит одним плоским списком, пока группу не
|
|
66
|
+
задали.
|
|
67
|
+
|
|
68
|
+
Секреты пишутся в системное хранилище (keyring). Если рабочего бэкенда нет, кит
|
|
69
|
+
честно предупреждает и уходит в файл — проверить можно командой
|
|
70
|
+
`totp store status`. Длинные сторы режутся на куски: у Windows Credential
|
|
71
|
+
Manager есть жёсткий потолок на размер значения, и без нарезки хранилище молча
|
|
72
|
+
деградировало бы в открытый файл ровно тогда, когда записей стало много.
|
|
73
|
+
|
|
74
|
+
## Telegram-бот
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
totp bot setup # токен бота и первый разрешённый аккаунт
|
|
78
|
+
totp bot run
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Бот отдаёт коды только аккаунтам из белого списка: бот, отвечающий кому угодно,
|
|
82
|
+
— это утечка второго фактора. Карточка показывает код, остаток жизни окна и
|
|
83
|
+
кнопку «Обновить».
|
|
84
|
+
|
|
85
|
+
## Лицензия
|
|
86
|
+
|
|
87
|
+
MIT
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# totpkit — одноразовые коды (TOTP) для любых сервисов
|
|
2
|
+
|
|
3
|
+
Одно шифрованное хранилище вторых факторов на все сервисы: PyPI, GitHub,
|
|
4
|
+
Bitrix24, что угодно. Код можно получить командой, а можно кнопкой в Telegram,
|
|
5
|
+
когда ноутбука под рукой нет. Другие проекты берут коды из того же стора одной
|
|
6
|
+
строкой, не заводя собственных копий секретов.
|
|
7
|
+
|
|
8
|
+
## Установка
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv add s-totpkit # как зависимость
|
|
12
|
+
uv sync --extra bot # с Telegram-ботом
|
|
13
|
+
uv run python scripts/self_check.py
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Быстрый старт
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
totp entry add --uri "otpauth://totp/PyPI:alice?secret=…&issuer=PyPI"
|
|
20
|
+
totp list
|
|
21
|
+
totp code pypi # {"code": "366845", "seconds_left": 20}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Вывод — JSON по умолчанию; `--text` переключает на человекочитаемый.
|
|
25
|
+
|
|
26
|
+
## Из своего кода
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from totpkit import provider_for
|
|
30
|
+
|
|
31
|
+
login(username, password, totp_provider=provider_for("pypi"))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Провайдер отдаёт код по сигнатуре `(попытка, прошлая ошибка) -> код` и сам
|
|
35
|
+
разбирается с границей 30-секундного окна: ждёт ротацию, когда коду осталось
|
|
36
|
+
жить меньше трёх секунд, и никогда не отдаёт дважды один и тот же код — иначе
|
|
37
|
+
повторная попытка входа сгорает на коде, который сервис уже отверг.
|
|
38
|
+
|
|
39
|
+
## Модель хранения
|
|
40
|
+
|
|
41
|
+
Запись — это **сервис + аккаунт + иконка + группа**. Ключ строится как
|
|
42
|
+
`сервис` или `сервис:аккаунт` и служит тем, что вы набираете руками: пока
|
|
43
|
+
аккаунт один, работает короткое `totp code pypi`; когда их стало два, короткая
|
|
44
|
+
форма даёт ошибку со списком вариантов, а не выдаёт код наугад.
|
|
45
|
+
|
|
46
|
+
Групп по умолчанию нет — всё лежит одним плоским списком, пока группу не
|
|
47
|
+
задали.
|
|
48
|
+
|
|
49
|
+
Секреты пишутся в системное хранилище (keyring). Если рабочего бэкенда нет, кит
|
|
50
|
+
честно предупреждает и уходит в файл — проверить можно командой
|
|
51
|
+
`totp store status`. Длинные сторы режутся на куски: у Windows Credential
|
|
52
|
+
Manager есть жёсткий потолок на размер значения, и без нарезки хранилище молча
|
|
53
|
+
деградировало бы в открытый файл ровно тогда, когда записей стало много.
|
|
54
|
+
|
|
55
|
+
## Telegram-бот
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
totp bot setup # токен бота и первый разрешённый аккаунт
|
|
59
|
+
totp bot run
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Бот отдаёт коды только аккаунтам из белого списка: бот, отвечающий кому угодно,
|
|
63
|
+
— это утечка второго фактора. Карточка показывает код, остаток жизни окна и
|
|
64
|
+
кнопку «Обновить».
|
|
65
|
+
|
|
66
|
+
## Лицензия
|
|
67
|
+
|
|
68
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "s-totpkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "TOTP-хранилище и генератор одноразовых кодов для любых сервисов: CLI, библиотека и Telegram-бот выдачи кодов"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [{ name = "Dmitry Semyonov" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"s-librarykit>=0.7.7",
|
|
11
|
+
"s-clikit>=0.1.5",
|
|
12
|
+
"pyotp>=2.9",
|
|
13
|
+
"keyring>=24",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
bot = ["aiogram>=3.13"]
|
|
18
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23"]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
totp = "totpkit.cli:main"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/totpkit"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
addopts = "-q -p no:faker"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Проверка готовности кита totpkit.
|
|
2
|
+
|
|
3
|
+
Отвечает на один вопрос: сможет ли кит выдать код прямо сейчас. Секретов не
|
|
4
|
+
печатает — только сколько записей и куда они пишутся.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
#: Тестовый вектор RFC 4648, не чей-то настоящий секрет.
|
|
13
|
+
PROBE_SECRET = "JBSWY3DPEHPK3PXP"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check() -> dict[str, object]:
|
|
17
|
+
checks: list[dict[str, object]] = []
|
|
18
|
+
|
|
19
|
+
def record(name: str, ok: bool, detail: str = "") -> None:
|
|
20
|
+
checks.append({"check": name, "ok": ok, "detail": detail})
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import totpkit
|
|
24
|
+
|
|
25
|
+
record("import totpkit", True, totpkit.__version__)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
record("import totpkit", False, f"{type(e).__name__}: {e}")
|
|
28
|
+
return {"ready": False, "checks": checks}
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import pyotp # noqa: F401
|
|
32
|
+
|
|
33
|
+
record("pyotp", True)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
record("pyotp", False, f"{type(e).__name__}: {e} — без него кодов не будет")
|
|
36
|
+
|
|
37
|
+
# Генератор проверяется на известном секрете: если сломан он, всё остальное
|
|
38
|
+
# не имеет значения.
|
|
39
|
+
try:
|
|
40
|
+
from totpkit.codes import seconds_remaining, totp_now
|
|
41
|
+
|
|
42
|
+
code = totp_now(PROBE_SECRET)
|
|
43
|
+
ok = len(code) == 6 and code.isdigit()
|
|
44
|
+
record("генератор кодов", ok, f"окно живёт ещё {seconds_remaining()} с")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
record("генератор кодов", False, f"{type(e).__name__}: {e}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from totpkit.entries import TotpStore
|
|
50
|
+
|
|
51
|
+
store = TotpStore.default()
|
|
52
|
+
described = store.backend.describe()
|
|
53
|
+
entries = store.list()
|
|
54
|
+
record(
|
|
55
|
+
"стор секретов",
|
|
56
|
+
described["backend"] == "keyring",
|
|
57
|
+
f"бэкенд: {described['backend']}"
|
|
58
|
+
+ ("" if described["backend"] == "keyring" else " — секреты лежат открытым текстом"),
|
|
59
|
+
)
|
|
60
|
+
record("записи", True, f"{len(entries)} шт., групп: {len(store.groups())}")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
record("стор секретов", False, f"{type(e).__name__}: {e}")
|
|
63
|
+
|
|
64
|
+
# Бот опционален: кит полноценен и без aiogram.
|
|
65
|
+
try:
|
|
66
|
+
import aiogram # noqa: F401
|
|
67
|
+
|
|
68
|
+
record("aiogram (опционально)", True, "бот доступен")
|
|
69
|
+
except ImportError:
|
|
70
|
+
record(
|
|
71
|
+
"aiogram (опционально)",
|
|
72
|
+
True,
|
|
73
|
+
"не установлен — CLI работает, бот недоступен (`uv sync --extra bot`)",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
required = {"import totpkit", "pyotp", "генератор кодов", "стор секретов"}
|
|
77
|
+
ready = all(c["ok"] for c in checks if c["check"] in required)
|
|
78
|
+
return {"ready": ready, "checks": checks}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main() -> int:
|
|
82
|
+
result = check()
|
|
83
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
84
|
+
return 0 if result["ready"] else 1
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
sys.exit(main())
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: totp
|
|
3
|
+
description: "CLI `totp` и библиотека для одноразовых кодов (TOTP, RFC 6238) по любым сервисам: хранение секретов в шифрованном сторе (keyring), запись = сервис + аккаунт + иконка + группа, выдача текущего кода, импорт из ссылки otpauth:// под QR-кодом, Telegram-бот с кнопкой «Обновить» для доступа к кодам с телефона. Другие навыки берут коды одной строкой `from totpkit import provider_for` — так проходят форму второго фактора pypi, bitrix24, github. EN triggers: totp, one-time code, second factor, 2fa code, authenticator, otpauth uri, qr secret, totp bot, telegram bot with codes, generate 2fa code, store totp secret, google authenticator replacement. RU триггеры: одноразовый код, второй фактор, двухфакторная аутентификация, код аутентификатора, totp секрет, добавь аутентификатор, коды для pypi, коды для битрикса, бот с кодами, отдай код с телефона, otpauth ссылка, перенести аутентификатор, где мои коды."
|
|
4
|
+
metadata:
|
|
5
|
+
openclaw: totp
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Respond in the user's language.
|
|
9
|
+
|
|
10
|
+
# totp — одноразовые коды для любых сервисов
|
|
11
|
+
|
|
12
|
+
Одно хранилище вторых факторов на все сервисы сразу: PyPI, Bitrix24, GitHub,
|
|
13
|
+
что угодно. Код можно получить командой, а можно — кнопкой в Telegram, когда
|
|
14
|
+
ноутбука под рукой нет. Другие навыки берут коды из того же стора, не заводя
|
|
15
|
+
собственных копий секретов.
|
|
16
|
+
|
|
17
|
+
## When activated
|
|
18
|
+
|
|
19
|
+
1. Нужен код второго фактора: `totp code pypi`.
|
|
20
|
+
2. Подключается новый аутентификатор — есть QR-код или строка секрета.
|
|
21
|
+
3. Другому навыку нужно проходить форму 2FA автоматически.
|
|
22
|
+
4. Настраивается или запускается Telegram-бот выдачи кодов.
|
|
23
|
+
5. Надо посмотреть, какие аутентификаторы вообще заведены, и навести порядок
|
|
24
|
+
(иконки, группы, лишние записи).
|
|
25
|
+
|
|
26
|
+
## Route by request type
|
|
27
|
+
|
|
28
|
+
| Запрос | Команда |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Дай код | `totp code <сервис>` |
|
|
31
|
+
| Что у меня заведено | `totp list` |
|
|
32
|
+
| Добавить из QR-ссылки | `totp entry add --uri "otpauth://totp/…"` |
|
|
33
|
+
| Добавить секретом | `totp entry add <сервис>` (спросит скрытым вводом) |
|
|
34
|
+
| Второй аккаунт того же сервиса | `totp entry add <сервис> --account <логин>` |
|
|
35
|
+
| Сменить иконку или группу | `totp entry update <ключ> --icon 🎁 --group работа` |
|
|
36
|
+
| Удалить запись | `totp entry delete <ключ>` |
|
|
37
|
+
| Куда пишутся секреты | `totp store status` |
|
|
38
|
+
| Откатить случайную перезапись | `totp store history` → `totp store restore` |
|
|
39
|
+
| Настроить бота | `totp bot setup` → `totp bot user allow` → `totp bot run` |
|
|
40
|
+
| Взять коды из другого навыка | `from totpkit import provider_for` |
|
|
41
|
+
|
|
42
|
+
## Instructions
|
|
43
|
+
|
|
44
|
+
### Добавление записи
|
|
45
|
+
|
|
46
|
+
Проще всего — ссылкой из-под QR-кода: сервис показывает её текстом рядом с
|
|
47
|
+
картинкой.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
totp entry add --uri "otpauth://totp/PyPI:alice?secret=JBSWY3DPEHPK3PXP&issuer=PyPI"
|
|
51
|
+
totp entry add pypi --account alice # или секретом, скрытым вводом
|
|
52
|
+
totp code pypi # {"code": "366845", "seconds_left": 20}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Секрет проверяется на Base32 **в момент добавления**. Иначе кривая строка молча
|
|
56
|
+
доживает до того единственного входа, когда была нужна, — и выясняется это в
|
|
57
|
+
худший момент.
|
|
58
|
+
|
|
59
|
+
### Ключ записи — то, что вы набираете
|
|
60
|
+
|
|
61
|
+
Ключ строится как `сервис` либо `сервис:аккаунт`. Пока аккаунт один, работает
|
|
62
|
+
короткое `totp code pypi`. Когда аккаунтов стало два, короткая форма даёт
|
|
63
|
+
**ошибку со списком вариантов**, а не выдаёт код наугад: молча вернуть код
|
|
64
|
+
чужого аккаунта хуже, чем попросить уточнить.
|
|
65
|
+
|
|
66
|
+
### Группы и иконки
|
|
67
|
+
|
|
68
|
+
Групп по умолчанию нет — все записи лежат одним плоским списком, в одном месте.
|
|
69
|
+
Группировка появляется ровно тогда, когда её задали:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
totp entry update bitrix24:kso24.bitrix24.ru --group клиенты --icon 🏢
|
|
73
|
+
totp groups
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Иконка подставляется по сервису (`pypi` → 📦, `github` → 🐙, `bitrix24` → 🏢),
|
|
77
|
+
чтобы список читался взглядом; своя всегда перекрывает.
|
|
78
|
+
|
|
79
|
+
### Из другого навыка
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from totpkit import provider_for
|
|
83
|
+
|
|
84
|
+
login(username, password, totp_provider=provider_for("pypi"))
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Провайдер отдаёт код по сигнатуре `(попытка, прошлая ошибка) -> код` и сам
|
|
88
|
+
разбирается с границей 30-секундного окна: ждёт ротацию, когда коду осталось
|
|
89
|
+
жить меньше трёх секунд, и никогда не отдаёт дважды один и тот же код — иначе
|
|
90
|
+
повторная попытка входа сгорает на коде, который сервис уже отверг.
|
|
91
|
+
|
|
92
|
+
### Telegram-бот
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
totp bot setup # токен бота + первый разрешённый аккаунт (по /start)
|
|
96
|
+
totp bot user allow <id> # добавить ещё кому-то
|
|
97
|
+
totp bot run # long-poll; остановить — totp bot stop
|
|
98
|
+
totp bot status # настроен ли, работает ли (токен замаскирован)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Бот отдаёт коды **только** аккаунтам из белого списка. Бот, отвечающий кому
|
|
102
|
+
угодно, — это не удобство, а утечка второго фактора.
|
|
103
|
+
|
|
104
|
+
## Examples
|
|
105
|
+
|
|
106
|
+
### Пример 1 — перенести аутентификатор с телефона
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
totp entry add --uri "otpauth://totp/GitHub:alice?secret=…&issuer=GitHub"
|
|
110
|
+
totp list # проверить, что запись появилась
|
|
111
|
+
totp code github # сверить с приложением на телефоне
|
|
112
|
+
```
|
|
113
|
+
Совпало — секрет перенесён верно.
|
|
114
|
+
|
|
115
|
+
### Пример 2 — два аккаунта одного сервиса
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
totp entry add pypi --account personal
|
|
119
|
+
totp entry add pypi --account work
|
|
120
|
+
totp code pypi # ошибка: подходит pypi:personal, pypi:work
|
|
121
|
+
totp code pypi:work # так
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Rules
|
|
125
|
+
|
|
126
|
+
- **Секрет проверяется при добавлении, а не при входе.** Ошибка должна всплыть
|
|
127
|
+
тогда, когда её дёшево исправить.
|
|
128
|
+
- **Неоднозначная ссылка — ошибка, а не догадка.** Код не того аккаунта хуже
|
|
129
|
+
отказа.
|
|
130
|
+
- **Групп по умолчанию нет.** Пока их не задали, всё лежит одним списком.
|
|
131
|
+
- **Секреты не ходят через argv.** По умолчанию — скрытый ввод: аргумент
|
|
132
|
+
командной строки попадает в историю shell и в транскрипт агента.
|
|
133
|
+
- **Бот только по белому списку.** Без него бот раздаёт вторые факторы.
|
|
134
|
+
- **Стор — шифрованный.** Основной путь — keyring; фолбэк в файл честно
|
|
135
|
+
предупреждает, что секреты лежат открыто. Проверяй `totp store status`.
|
|
136
|
+
|
|
137
|
+
## Troubleshooting
|
|
138
|
+
|
|
139
|
+
**`totp store status` показывает `plaintext-file`.** Рабочего бэкенда keyring
|
|
140
|
+
нет, секреты лежат открытым текстом. На Windows это обычно значит сломанный
|
|
141
|
+
Credential Manager; поставь рабочий keyring и перезапиши записи.
|
|
142
|
+
|
|
143
|
+
**Код не подходит, хотя секрет верный.** Проверь часы: TOTP завязан на время, и
|
|
144
|
+
расхождение больше 30 секунд ломает всё. Второе по частоте — секрет от другого
|
|
145
|
+
аккаунта того же сервиса.
|
|
146
|
+
|
|
147
|
+
**Случайно перезаписал запись.** `totp store history` покажет поколения (без
|
|
148
|
+
секретов), `totp store restore` вернёт последнее. Глубина ограничена — это
|
|
149
|
+
страховка от «затёр только что», а не архив.
|
|
150
|
+
|
|
151
|
+
**Бот не отвечает.** `totp bot status`: не настроен — `totp bot setup`; не
|
|
152
|
+
запущен — `totp bot run`; отвечает отказом — твой Telegram-ID не в белом списке
|
|
153
|
+
(`totp bot user allow <id>`).
|
|
154
|
+
|
|
155
|
+
**`ImportError: pyotp`.** Кит ставится с зависимостями: `uv sync` в каталоге
|
|
156
|
+
проекта или `uv add s-totpkit` в потребителе.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""totpkit — одноразовые коды (TOTP) для любых сервисов.
|
|
2
|
+
|
|
3
|
+
Кит закрывает три вещи разом: где лежит секрет (шифрованный стор), как из него
|
|
4
|
+
получить код (RFC 6238 с аккуратной работой на границе окна) и как этот код
|
|
5
|
+
получает человек (CLI и Telegram-бот).
|
|
6
|
+
|
|
7
|
+
Навыку, которому нужен код для формы 2FA, достаточно одной строки::
|
|
8
|
+
|
|
9
|
+
from totpkit import provider_for
|
|
10
|
+
login(..., totp_provider=provider_for("pypi"))
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from totpkit.codes import (
|
|
14
|
+
OtpParams,
|
|
15
|
+
TotpProvider,
|
|
16
|
+
build_totp_provider,
|
|
17
|
+
parse_provisioning_uri,
|
|
18
|
+
seconds_remaining,
|
|
19
|
+
totp_now,
|
|
20
|
+
validate_secret,
|
|
21
|
+
)
|
|
22
|
+
from totpkit.entries import (
|
|
23
|
+
DEFAULT_ICONS,
|
|
24
|
+
Entry,
|
|
25
|
+
TotpStore,
|
|
26
|
+
default_store,
|
|
27
|
+
icon_for,
|
|
28
|
+
provider_for,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__version__ = "0.1.0"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"DEFAULT_ICONS",
|
|
35
|
+
"Entry",
|
|
36
|
+
"OtpParams",
|
|
37
|
+
"TotpProvider",
|
|
38
|
+
"TotpStore",
|
|
39
|
+
"__version__",
|
|
40
|
+
"build_totp_provider",
|
|
41
|
+
"default_store",
|
|
42
|
+
"icon_for",
|
|
43
|
+
"parse_provisioning_uri",
|
|
44
|
+
"provider_for",
|
|
45
|
+
"seconds_remaining",
|
|
46
|
+
"totp_now",
|
|
47
|
+
"validate_secret",
|
|
48
|
+
]
|