@ 编码- cc/ 密封
保密应用程序在自己的数据库中保存(它所调用服务的 API 密钥, 表示人们粘贴到一个
admin 面板)被AES-256-GCM封入数据库从未持有的密钥:一个工人秘密. 副本或导出
数据库中没有任何人被泄露。 WebCrypto 只运行在工人、节点和德诺;它唯一的依赖
实值 @codefusion-cc/workers-crypto,用于 base64url 和摘要.
npm install @codefusion-cc/sealed
@codefusion-cc/sealed/box 把一个小秘密封在某人的公用钥匙上, 这样他们才打开它
谁不能传达它下级) (中文(简体) ).
钥匙
codefusion-console cloudflare workers:edit -- sh -c 'npx -p @codefusion-cc/sealed codefusion-sealed-key | npx wrangler secret put CONNECTIONS_KEY'
钥匙直接进入秘密 永远不要进入屏幕 Wrangler为这辆装满了硬币
命令,哪个 codefusion-console 删除退出时 :
没有 wrangler login 在麦克.
在工人中
import { loadKeyring } from '@codefusion-cc/sealed'
const keyring = await loadKeyring(env.CONNECTIONS_KEY) // null: no key, or the secret is not one
if (!keyring) throw new Error('Set CONNECTIONS_KEY first')
// Bound to where it is kept: the same text opens only for the same context.
const sealed = await keyring.seal(JSON.stringify({ token }), `connection:${id}`)
const opened = await keyring.open(row.sealed, `connection:${row.id}`)
if (opened.ok) {
use(JSON.parse(opened.value))
if (opened.stale) await save(row.id, await keyring.seal(opened.value, `connection:${row.id}`))
} else {
// 'unknown-key': the key that sealed it is gone; 'tampered': changed, or moved from another row;
// 'malformed': not a sealed value.
askForItAgain(opened.reason)
}
- 一个密封值是
v1.<key id>.<iv>.<ciphertext>(基数64url). 密钥ID是密钥的前8个十六进制数字 SHA-256:它命名密钥而不将其送出,所以一个应用程序可以显示其行需要的密钥. open绝不要为一行人而抛下任何物;seal财务报告和审定财务报表open当给定一个不是字符串的上下文时, 扔出, 而当平台的 WebCrypto 失败时拒绝(如loadKeyring是,哪个 然后在下次召唤时再次尝试这个秘密.tampered财务报告和审定财务报表unknown-key总是指排队, 从来没有停赛.open给出准确的被封存的文本,包含一个主字节顺序标记(U+FEFF)或白空格. 清理封口前有人贴的东西:value.trim()两者都放下,所以从保存的文件复制一个密钥 BOM仍然符合请求标题.- 密钥在每次隔离时被导入一次,用于同一密文.
一个新的密钥
密钥可能先有数个新密钥,除以逗号或白相间. 最新的封印 每把钥匙打开,
和什么更古老的密钥密封打开作为 stale: true所以应用程序在读它的时候又封了它.
codefusion-console cloudflare workers:edit -- npx wrangler secret put CONNECTIONS_KEY # paste: <new key>,<old key>
一旦每行再次被封(每行) open 说 stale: false),只将新密钥放入密钥. 没有
旧密钥没有退路:它密封的东西必须再输入.
从早先的代码移动
前面写入的值 <iv>.<ciphertext> (标准基数64,无上下文,如柴油机插件所做的)
open(value, context, { legacy: true }),总是如所僵化:按所读再封相. 离开 legacy 关一次
由于该格式的值无一行约束,因此没有留下.
封入公用钥匙
@codefusion-cc/sealed/box 是一个通过一个不能读取的服务器的秘密: 页面封存到
使用它的人的公钥(在某人的Mac上的中继器,设备),服务器存储或转发被封件
文本,只有私钥的持有人才能打开。 它的工作像 Libsodium 的密封箱, 在WebCrypto,所以
同样的代码运行在浏览器(Chrimium,Firefox,Safari),节点20和后期,以及Workers.
在收件人上, 在每个开始时: 只有在还没有密钥文件时才会新建密钥 。 旧文件中的新密钥会留下 每个封入到旧公钥的盒子都无法打开,所以其他的(一个它无法读取的文件,一个没有钥匙的文件,a) WebCrypto 失败 停止开始 :
import { link, readFile, rm, writeFile } from 'node:fs/promises'
import { generateBoxKeys, loadBoxKeys, saveBoxKeys, type BoxKeys } from '@codefusion-cc/sealed/box'
async function recipientKeys(path: string): Promise<BoxKeys> {
let text: string
try {
text = await readFile(path, 'utf8')
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error // unreadable is not missing
const keys = await generateBoxKeys()
// It holds the private key. Written whole beside it, then linked in only while the path is still free: a start
// that dies midway leaves no half-written key file, and two starts at once never write over each other's keys.
const draft = `${path}.${crypto.randomUUID()}.new`
await writeFile(draft, await saveBoxKeys(keys), { mode: 0o600, flag: 'wx' })
try {
await link(draft, path)
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
return recipientKeys(path) // another start made them first: use those
} finally {
await rm(draft, { force: true })
}
return keys
}
const keys = await loadBoxKeys(text.trim()) // rejects when WebCrypto fails, rather than saying there are none
if (!keys) throw new Error(`${path} holds no box keys: move it aside to make new ones`)
return keys
}
const keys = await recipientKeys(path)
register(keys.publicKey) // 43 characters of base64url: give it to the server, which hands it to the page
在页面中,封入服务器提供给它的公钥:
import { sealBox } from '@codefusion-cc/sealed/box'
const sealed = await sealBox(relay.publicKey, prompt, `terminal:${relay.id}:${promptId}`) // null: not a box public key
在接收者上打开服务器转发的内容:
import { openBox } from '@codefusion-cc/sealed/box'
const opened = await openBox(keys, sealed, `terminal:${relayId}:${promptId}`)
if (opened.ok) run(opened.text) // or opened.bytes
// 'malformed': not a sealed box, or longer than maxBytes (1 MiB unless given);
// 'tampered': changed, sealed to another key, or for another context.
- 它会做什么:每个印章会制作一对新鲜的X25519密钥;它与收件人密钥一致的秘密去
通过HKDF-SHA256(用两种公用钥匙进行盐分,其信息中的内容)到一次使用的AES-256-GCM密钥. 这个
密封文本为
box1.<ephemeral public key>.<iv>.<ciphertext>,每个部分基数64url不粘贴. - A. 背景情况: 打开框的字符串名称, 如
seal上面写着 一个框移动到另一个 上下文(另一个提示,另一个中继)不会打开。 把所有的ID 接收检查. - 谁的钥匙:无论谁交出页面,收件人的公钥决定谁打开盒子. 一个掌上电脑的服务器 打开它自己的密钥 而不是读取页面封条的内容 所以盒子对存储和保存的服务器保密 转发它(它的数据库,它的日志,其中任何一个都漏出),而不是隐藏在钥匙上的。 这很重要, 页面获取或检查密钥的另一种方式(在收件人上显示指纹).
- 匿名发送者:一个盒子没有说是谁封的. 谁有公用钥匙,谁可以封起来,那么一个盒子 hold是来自外部的输入:认证谁单独要求它(服务器在转发之前是这样做的).
- 没有自己的保护: 同一框为同一上下文再次打开. 使背景独特(a) )并让收件人拒绝它已经运行的id.
- 钥匙在休息数字 :
saveBoxKeys写私钥的JWK(RFC 8037); 保存在只有所有者阅读的地方.loadBoxKeys返回时为无效,包括两个半个不是一对的JWK,并加载 私人密钥不可提取,除非{ extractable: true }(只需要再保存一次). - 错误数字 :
sealBox解析一个不是盒子公钥的收件人为无效(或其中少数 X25519 键之一) - 小型命令,其中不包含任何秘密;openBox永远不要为封存的文本丢弃。 都投给 参数 编程错误给定的(不是字符串的上下文,不是框键的密钥),以及每个 函数,它使用 WebCrypto 拒绝在失败时使用平台本身的错误(旧浏览器中没有 X25519,一个已损坏) 约束:无效,malformed财务报告和审定财务报表tampered只描述过输入. - 小秘密( 暗号): a box 是 AES-GCM 信件, 保存在全内存中.
openBox拒绝次数超过maxBytes联 合 国 解码任何内容之前的纯文本( 默认 1 MiB) .
@codefusion-cc/realtime账户密钥( e)packages/realtime/src/accountKeys.ts在P-256上有一个自己的盒子
使用自己的格式,设备已经将数据封存起来,所以它们留在上面而不是移动到这里.