Shhh 3.2.4 API 使用详解

目标:完整讲清 Shhh 3.2.4 的 REST API:创建/读取两端点参数语义、201/200/422/401/404/400 全错误码矩阵、tries 防爆破逐次实测与脚本化集成示例;全部回显为实测记录。

ShhhShhh2026/10/27

Shhh 3.2.4 API 使用详解

目标:完整讲清 Shhh 3.2.4 的 REST API——创建与读取两个端点的参数语义、成功/失败的全部响应形态(201/200/422/401/404/400 错误码矩阵)、tries 防爆破机制的逐次实测,以及脚本化集成的可运行示例。全部请求于 2026-10-27 对一套按 《Shhh 3.2.4 Docker Compose 部署指南》 部署的实例发出,回显逐行照抄实测结果;配置项背景见 《Shhh 3.2.4 配置参数详解》。

一、环境信息

项目值
软件版本Shhh 3.2.4
Base URLhttp://<服务器地址>:8081(本文实测实例为 http://127.0.0.1:8128,端口映射见部署篇)
交互格式请求与响应均为 JSON(畸形请求除外,见 6.5)
鉴权方式无请求头凭据——口令(passphrase)本身就是凭据,创建时设置、读取时提交
API 文档(官方 SwaggerHub)https://app.swaggerhub.com/apis-docs/smallwat3r/shhh-api/1.0.0
实测日期2026-10-27

💡 官方 SwaggerHub 文档描述的是同一套接口的语义参考;本文所有请求体/响应体均以 3.2.4 实测为准(如创建成功的状态码实测为 201)。

二、端点总览

Shhh 的 API 只有两个端点,恰好对应一条秘密的生命周期两端:

端点方法作用成功状态码
/api/secretPOST创建一条加密秘密,返回分享链接201
/api/secretGET提交口令解密,成功后秘密即焚200

安全语义回顾(详见 《Shhh 3.2.4 介绍与选型指南》):秘密以口令派生密钥加密存储,服务端不存口令明文;以下三类事件任一发生,记录永久删除——被成功读取一次、过期(默认 3 天)、错误口令次数用尽(默认 5 次)。

三、创建秘密(POST /api/secret)

3.1 请求参数

参数类型必填约束默认值
passphrasestring是≥8 字符,含至少 1 个数字、1 个大写字母无
secretstring是长度 ≤ SHHH_SECRET_MAX_LENGTH(默认 250)无
expirestring否10m / 30m / 1h / 3h / 6h / 1d / 2d / 3d / 5d / 7d3d
triesint否3 / 5 / 105

3.2 成功响应(实测)

curl -sS -w '\nHTTP_STATUS:%{http_code}\n' -X POST http://127.0.0.1:8128/api/secret \
  -H 'Content-Type: application/json' \
  -d '{"passphrase":"Q1w2e3r4","secret":"compose-deploy-check-4n7pQx","expire":"3d","tries":5}'
{"response":{"details":"Secret successfully created.","expires_on":"October 30, 2026 at 00:16 UTC","external_id":"Y_IB12WWjoJooUnQDoaz","link":"http://127.0.0.1:8128/secret/Y_IB12WWjoJooUnQDoaz","status":"created"}}

HTTP_STATUS:201

响应字段解读:

字段含义
external_id秘密的随机标识(实测 20 位大小写字母数字与 _/- 混合),读取时凭它定位
link完整分享链接(前缀受 SHHH_HOST 影响,见配置篇 4.1 实测)
expires_on过期时刻的英文格式时间戳(UTC)
statuscreated
details人读的成功说明

expire=10m 时的过期时刻(实测,创建于 00:21 UTC):"expires_on":"October 27, 2026 at 00:31 UTC"——恰好 +10 分钟,档位语义与字面一致。

四、读取秘密(GET /api/secret)

4.1 请求参数(query string)

参数必填说明
external_id是创建响应返回的标识
passphrase是创建时设置的口令

4.2 成功响应(实测)

curl -sS -w '\nHTTP_STATUS:%{http_code}\n' \
  'http://127.0.0.1:8128/api/secret?external_id=UXFGOP3h76Wryp6L8oXe&passphrase=Q1w2e3r4'
{"response":{"msg":"api-full-flow-ok-5t5t5t","status":"success"}}

HTTP_STATUS:200

原文在 msg 字段中返回。同一条秘密只能成功读取一次——紧接着再读同一链接(实测):

{"response":{"msg":"Sorry, we can't find a secret, it has expired, been deleted or has already been read.","status":"expired"}}

HTTP_STATUS:404

五、tries 防爆破机制逐次实测

创建一条 tries=3 的秘密,然后连续提交错误口令,观察剩余次数递减与销毁时点(实测全程):

---POST create tries=3---
HTTP 201
{"response":{"details":"Secret successfully created.","expires_on":"October 28, 2026 at 00:21 UTC","external_id":"eRhSSZ2dbfqWhwctrmGa","link":"http://127.0.0.1:8128/secret/eRhSSZ2dbfqWhwctrmGa","status":"created"}}

---GET wrong passphrase #1---
HTTP 401
{"response":{"msg":"Sorry, the passphrase is not valid. Number of tries remaining: 2","status":"invalid"}}

---GET wrong passphrase #2---
HTTP 401
{"response":{"msg":"Sorry, the passphrase is not valid. Number of tries remaining: 1","status":"invalid"}}

---GET wrong passphrase #3---
HTTP 401
{"response":{"msg":"The passphrase is not valid. You've exceeded the number of tries and the secret has been deleted.","status":"invalid"}}

---GET correct after exhausted---
HTTP 404
{"response":{"msg":"Sorry, we can't find a secret, it has expired, been deleted or has already been read.","status":"expired"}}

三个关键结论:

  1. 每次错口令扣一次,401 + 剩余次数提示;第 N 次全错(N=tries)时直接销毁并告知 "exceeded the number of tries and the secret has been deleted"。
  2. 销毁后即使口令正确也读不回(实测最后一发用的是正确口令 Q1w2e3r4,仍 404)——tries 是硬销毁闸门,不是软锁定。
  3. 因此 tries 的取值要在「容错」与「防爆破」间权衡:3 次最严格(手滑两次就没了),10 次适合口令由人工转述易听错的场景。

六、错误码矩阵(全部实测)

6.1 参数缺失 → 422

HTTP 422
{"response":{"details":"Missing data for required field.","status":"error"}}

缺一个字段是一条提示;POST 与 GET 缺参同此形态。若请求体连字段结构都没有(如用表单编码发送),webargs 会按缺失字段数重复提示(实测):

{"response":{"details":"Missing data for required field. Missing data for required field.","status":"error"}}

6.2 口令过弱 → 422

{"response":{"details":"Sorry, your passphrase is too weak. It needs minimum 8 characters, with 1 number and 1 uppercase.","status":"error"}}

6.3 秘密超长 → 422

{"response":{"details":"The secret should not exceed 250 characters.","status":"error"}}

(上限随 SHHH_SECRET_MAX_LENGTH 联动,实测调为 50 时提示同步变为 "should not exceed 50 characters.")

6.4 非法 expire / tries → 422

{"response":{"details":"Must be one of: 10m, 30m, 1h, 3h, 6h, 1d, 2d, 3d, 5d, 7d.","status":"error"}}
{"response":{"details":"Must be one of: 3, 5, 10.","status":"error"}}

6.5 畸形 JSON → 400(HTML)

Content-Type: application/json 但请求体不是合法 JSON 时,Flask 层直接拒绝(响应为 HTML 错误页,非 JSON):

HTTP 400
<!doctype html><html lang=en> <title>400 Bad Request</title> <h1>Bad Request</h1> <p>The browser (or proxy) sent a request that this server could not understand.</p>

6.6 读取侧 → 401 与 404

场景状态码status 字段
口令错误(次数未尽)401invalid(含剩余次数)
错误次数用尽401invalid(消息含 deleted)
external_id 不存在 / 已读取 / 已过期 / 已因超限销毁404expired(统一消息,不区分具体原因)

external_id 不存在的实测:

HTTP 404
{"response":{"msg":"Sorry, we can't find a secret, it has expired, been deleted or has already been read.","status":"expired"}}

404 消息对四种「查无此密」场景统一文案,避免向试探者泄露该标识是否曾存在——这是刻意的安全设计。

6.7 矩阵速查表

状态码触发条件status
201创建成功created
200读取成功(同时秘密销毁)success
422创建参数缺失/弱口令/超长/非法档位error
401口令错误(含销毁通知)invalid
404查无此密(不存在/已读/过期/超限)expired
400请求体不是合法 JSON(HTML)

七、脚本化集成示例

以下函数封装为实测可用形态(curl + python3 标准 JSON 解析,无第三方依赖),口令需满足强度规则:

#!/usr/bin/env bash
# shhh-create.sh —— 创建一条秘密并打印分享链接
SHHH_URL="${SHHH_URL:-http://127.0.0.1:8128}"

create_secret() {
  local secret="$1" passphrase="$2" expire="${3:-3d}" tries="${4:-5}"
  curl -sS -X POST "$SHHH_URL/api/secret" \
    -H 'Content-Type: application/json' \
    -d "{\"passphrase\":\"$passphrase\",\"secret\":\"$secret\",\"expire\":\"$expire\",\"tries\":$tries}" \
    | python3 -c 'import sys,json; r=json.load(sys.stdin)["response"]; print(r["link"]); print(r["external_id"])'
}

read_secret() {
  local external_id="$1" passphrase="$2"
  curl -sS "$SHHH_URL/api/secret?external_id=$external_id&passphrase=$passphrase" \
    | python3 -c 'import sys,json; r=json.load(sys.stdin)["response"]; print(r["status"]); print(r.get("msg",""))'
}

创建与读取各跑一次(实测调用与输出):

```bash
create_secret "db-root-password-9k2mTz-x" "Q1w2e3r4" "1d" 5
http://127.0.0.1:8128/secret/u6GDN7yzaYADZQFKRAm3
u6GDN7yzaYADZQFKRAm3
read_secret "u6GDN7yzaYADZQFKRAm3" "Q1w2e3r4"
success
db-root-password-9k2mTz-x

CI/自动化场景的典型用法:发布脚本把部署产物的临时口令开成一条 expire=10m, tries=3 的秘密,把 link 发到值班频道、口令走另一个渠道,值班人一次性读取。

⚠️ 安全提醒:GET 读取把口令放在 query string 中,会进入反向代理与服务器的访问日志。脚本集成时注意日志脱敏(或限制读取请求仅走内网/本机),日志留存面大的环境优先走人读页面而非脚本轮询。

八、与 Web 页面的关系

API 与页面共用同一套存储与加密逻辑:页面 / 的创建表单提交后同样落到 POST /api/secret 的语义上,/secret/<external_id> 读取页对应 GET 语义(错误口令同样消耗 tries,页面与脚本共用同一条秘密的尝试预算)。站点 robots.txt 明确禁止爬取(实测原文):

User-Agent: *

Disallow: /secret/
Disallow: /api/

九、参考链接