Release rolling / release (push) Successful in 11m16s
Bump published pins (compact-encoding 3, bare-fetch/tls/https/ws 3, bare-subprocess 6, bare-signals 5, corestore 7.12, protomux 3.11, hypercore-crypto 3.7, bare-runtime 1.31) and regenerate catalogs, manifests, and kernel/seeder bundles. Adapt call sites to the new APIs: - Corestore: explicit session flush before suspend(); treeCache ctor opts - bare-crypto: KeyObject.export() instead of removed ._key - Protomux 3.11: wait for fullyOpened()/fullyClosed() on chat channels - bare-fetch: surface response.type and Headers.getSetCookie - host snapshots: bare-os 3.9 / bare-posix / bare-fs.statfs frsize - bare-subprocess 6: optional IPC channel + json serialization Keep catalog sync from wiping curated pearEntries. Teach the Node test shim to stub bare-thread/bare-worker (ESM absolute paths) and chain Bare.on so bare-timers can load. Booter 479, protocol 34, seeder 14.
297 lines
8.1 KiB
JavaScript
297 lines
8.1 KiB
JavaScript
/**
|
|
* On-disk account v2: Ed25519 (bare-crypto); secret key sealed with
|
|
* PBKDF2-SHA256 + ChaCha20-Poly1305.
|
|
* v1 used libsodium — use `login --new` to create a v2 account.
|
|
*/
|
|
|
|
import b4a from 'b4a'
|
|
import bareCrypto from 'bare-crypto'
|
|
|
|
const {
|
|
generateKeyPair,
|
|
pbkdf2Sync,
|
|
createCipheriv,
|
|
createDecipheriv,
|
|
randomFillSync,
|
|
createHash
|
|
} = bareCrypto
|
|
|
|
export const ACCOUNT_MAGIC = new Uint8Array([
|
|
0x42, 0x41, 0x52, 0x45, 0x4f, 0x53, 0x30, 0x31
|
|
]) // BAREOS01
|
|
export const ACCOUNT_VERSION = 2
|
|
export const ACCOUNT_PATH = '/.bare/account'
|
|
|
|
/** Documented crypto profile for v2 on-disk accounts (algorithm agility metadata). */
|
|
export const BARE_OS_ACCOUNT_CRYPTO_PROFILE_V2 = Object.freeze({
|
|
kdf: 'pbkdf2-sha256',
|
|
aead: 'chacha20-poly1305',
|
|
signing: 'ed25519',
|
|
encodingVersion: ACCOUNT_VERSION,
|
|
defaultKdfIterations: 210_000
|
|
})
|
|
|
|
const LEGACY_ACCOUNT_VERSION = 1
|
|
|
|
export const ED25519_PUBLIC_KEY_LENGTH = 32
|
|
export const ED25519_SECRET_KEY_LENGTH = 64
|
|
|
|
const SALT_LENGTH = 16
|
|
const NONCE_LENGTH = 12
|
|
const TAG_LENGTH = 16
|
|
/** OWASP-style iteration count for PBKDF2-SHA256 */
|
|
export const PBKDF2_ITERATIONS = 210_000
|
|
|
|
/** @param {ArrayBuffer | ArrayBufferView} view */
|
|
function u8(view) {
|
|
if (view instanceof Uint8Array) return view
|
|
if (view instanceof ArrayBuffer) return new Uint8Array(view)
|
|
return new Uint8Array(view.buffer, view.byteOffset, view.byteLength)
|
|
}
|
|
|
|
export function secureZero(buf) {
|
|
if (!buf) return
|
|
u8(buf).fill(0)
|
|
}
|
|
|
|
export function hashPublicKeyForUid(publicKey) {
|
|
const h = createHash('sha256')
|
|
h.update(u8(publicKey))
|
|
const d = u8(h.digest())
|
|
const n = (d[0] | (d[1] << 8) | (d[2] << 16) | (d[3] << 24)) >>> 0
|
|
return String(10000 + (n % 50000))
|
|
}
|
|
|
|
/** 32-byte subkey for vault crypto (SHA-256 of signing secret). */
|
|
export function vaultKeyFromSecret(secretKey) {
|
|
const h = createHash('sha256')
|
|
h.update(u8(secretKey))
|
|
return u8(h.digest())
|
|
}
|
|
|
|
/** Unkeyed SHA-256 for vault blob names (path-stable). */
|
|
export function hashUtf8Path(pathUtf8) {
|
|
const h = createHash('sha256')
|
|
h.update(u8(pathUtf8))
|
|
return u8(h.digest())
|
|
}
|
|
|
|
/**
|
|
* @param {ArrayBufferView} key32
|
|
* @param {ArrayBufferView} plaintext
|
|
* @returns {Uint8Array} nonce | ciphertext | tag
|
|
*/
|
|
export function sealBytes(key32, plaintext) {
|
|
const nonce = new Uint8Array(NONCE_LENGTH)
|
|
randomFillSync(nonce)
|
|
const key = u8(key32)
|
|
const pt = u8(plaintext)
|
|
const cipher = createCipheriv('chacha20-poly1305', key, nonce)
|
|
cipher.update(pt)
|
|
const ct = u8(cipher.final())
|
|
const tag = u8(cipher.getAuthTag())
|
|
const out = new Uint8Array(NONCE_LENGTH + ct.length + TAG_LENGTH)
|
|
out.set(nonce, 0)
|
|
out.set(ct, NONCE_LENGTH)
|
|
out.set(tag, NONCE_LENGTH + ct.length)
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* @param {ArrayBufferView} key32
|
|
* @param {ArrayBufferView} boxed
|
|
* @returns {Uint8Array}
|
|
*/
|
|
export function openBytes(key32, boxed) {
|
|
const key = u8(key32)
|
|
const b = u8(boxed)
|
|
if (b.length < NONCE_LENGTH + TAG_LENGTH + 1) {
|
|
throw new Error('Invalid sealed blob')
|
|
}
|
|
const nonce = b.subarray(0, NONCE_LENGTH)
|
|
const tag = b.subarray(b.length - TAG_LENGTH)
|
|
const ct = b.subarray(NONCE_LENGTH, b.length - TAG_LENGTH)
|
|
const decipher = createDecipheriv('chacha20-poly1305', key, nonce)
|
|
decipher.update(ct)
|
|
decipher.setAuthTag(tag)
|
|
return u8(decipher.final())
|
|
}
|
|
|
|
/**
|
|
* @param {string | Uint8Array} passphrase
|
|
* @param {Uint8Array} salt
|
|
* @param {number} iterations
|
|
*/
|
|
function deriveKey(passphrase, salt, iterations) {
|
|
if (typeof passphrase === 'string') {
|
|
const pass = b4a.from(passphrase, 'utf8')
|
|
const out = u8(pbkdf2Sync(pass, u8(salt), iterations, 32, 'sha256'))
|
|
secureZero(pass)
|
|
return out
|
|
}
|
|
const pass = u8(passphrase)
|
|
return u8(pbkdf2Sync(pass, u8(salt), iterations, 32, 'sha256'))
|
|
}
|
|
|
|
/** Raw bytes from a bare-crypto KeyObject (`export()`; `_key` is gone). */
|
|
function rawKeyBytes(key) {
|
|
if (!key) throw new Error('missing key')
|
|
if (key instanceof Uint8Array || key instanceof ArrayBuffer) return u8(key)
|
|
if (key._key) return u8(key._key)
|
|
if (typeof key.export === 'function') {
|
|
const ex = key.export()
|
|
if (ex && (ex.byteLength != null || ex instanceof ArrayBuffer))
|
|
return u8(ex)
|
|
}
|
|
throw new Error('bare-crypto KeyObject has no exportable key material')
|
|
}
|
|
|
|
export function generateEd25519Keypair() {
|
|
const { publicKey, privateKey } = generateKeyPair('ed25519')
|
|
return {
|
|
publicKey: rawKeyBytes(publicKey),
|
|
secretKey: rawKeyBytes(privateKey)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} passphrase
|
|
* @returns {Uint8Array}
|
|
*/
|
|
export function encodeNewAccount(passphrase) {
|
|
const { publicKey, secretKey } = generateEd25519Keypair()
|
|
const buf = encodeAccount(passphrase, publicKey, secretKey)
|
|
secureZero(secretKey)
|
|
return buf
|
|
}
|
|
|
|
/**
|
|
* @param {string} passphrase
|
|
* @param {Uint8Array} pk
|
|
* @param {Uint8Array} sk
|
|
*/
|
|
export function encodeAccount(passphrase, pk, sk) {
|
|
const salt = new Uint8Array(SALT_LENGTH)
|
|
randomFillSync(salt)
|
|
const subkey = deriveKey(passphrase, salt, PBKDF2_ITERATIONS)
|
|
const sealed = sealBytes(subkey, u8(sk))
|
|
secureZero(subkey)
|
|
|
|
const iters = PBKDF2_ITERATIONS
|
|
const out = new Uint8Array(
|
|
ACCOUNT_MAGIC.length +
|
|
1 +
|
|
ED25519_PUBLIC_KEY_LENGTH +
|
|
SALT_LENGTH +
|
|
4 +
|
|
sealed.length
|
|
)
|
|
let o = 0
|
|
out.set(ACCOUNT_MAGIC, o)
|
|
o += ACCOUNT_MAGIC.length
|
|
out[o++] = ACCOUNT_VERSION
|
|
out.set(u8(pk), o)
|
|
o += ED25519_PUBLIC_KEY_LENGTH
|
|
out.set(salt, o)
|
|
o += SALT_LENGTH
|
|
out[o++] = (iters >>> 24) & 255
|
|
out[o++] = (iters >>> 16) & 255
|
|
out[o++] = (iters >>> 8) & 255
|
|
out[o++] = iters & 255
|
|
out.set(sealed, o)
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* @param {string} passphrase
|
|
* @param {Uint8Array} buf
|
|
* @returns {{ publicKey: Uint8Array, secretKey: Uint8Array }}
|
|
*/
|
|
export function decodeAccount(passphrase, buf) {
|
|
if (
|
|
!buf ||
|
|
buf.length <
|
|
ACCOUNT_MAGIC.length +
|
|
1 +
|
|
ED25519_PUBLIC_KEY_LENGTH +
|
|
SALT_LENGTH +
|
|
4 +
|
|
NONCE_LENGTH +
|
|
TAG_LENGTH +
|
|
1
|
|
) {
|
|
throw new Error('Invalid account file')
|
|
}
|
|
for (let i = 0; i < ACCOUNT_MAGIC.length; i++) {
|
|
if (buf[i] !== ACCOUNT_MAGIC[i]) throw new Error('Invalid account magic')
|
|
}
|
|
let o = ACCOUNT_MAGIC.length
|
|
const ver = buf[o++]
|
|
if (ver === LEGACY_ACCOUNT_VERSION) {
|
|
throw new Error(
|
|
'Account uses legacy crypto (v1). Run: login --new (then enter passphrase at prompt) to create a new account'
|
|
)
|
|
}
|
|
if (ver !== ACCOUNT_VERSION) throw new Error('Unsupported account version')
|
|
|
|
const pk = buf.subarray(o, o + ED25519_PUBLIC_KEY_LENGTH)
|
|
o += ED25519_PUBLIC_KEY_LENGTH
|
|
const salt = buf.subarray(o, o + SALT_LENGTH)
|
|
o += SALT_LENGTH
|
|
const iterations =
|
|
(buf[o] << 24) | (buf[o + 1] << 16) | (buf[o + 2] << 8) | buf[o + 3]
|
|
o += 4
|
|
if (iterations < 10000 || iterations > 10_000_000) {
|
|
throw new Error('Invalid account parameters')
|
|
}
|
|
const sealed = buf.subarray(o)
|
|
|
|
const subkey = deriveKey(passphrase, salt, iterations)
|
|
let sk
|
|
try {
|
|
sk = openBytes(subkey, sealed)
|
|
} catch {
|
|
secureZero(subkey)
|
|
throw new Error('Wrong passphrase')
|
|
}
|
|
secureZero(subkey)
|
|
|
|
if (sk.length !== ED25519_SECRET_KEY_LENGTH) {
|
|
secureZero(sk)
|
|
throw new Error('Invalid account file')
|
|
}
|
|
|
|
return { publicKey: Uint8Array.from(pk), secretKey: sk }
|
|
}
|
|
|
|
/**
|
|
* Read non-secret crypto metadata from an account blob (no decryption).
|
|
* @param {Uint8Array | null | undefined} buf
|
|
* @returns {Record<string, unknown> | null}
|
|
*/
|
|
export function readAccountCryptoProfile(buf) {
|
|
if (!buf || buf.length < ACCOUNT_MAGIC.length + 1) return null
|
|
for (let i = 0; i < ACCOUNT_MAGIC.length; i++) {
|
|
if (buf[i] !== ACCOUNT_MAGIC[i]) return null
|
|
}
|
|
const ver = buf[ACCOUNT_MAGIC.length]
|
|
if (ver === LEGACY_ACCOUNT_VERSION) {
|
|
return {
|
|
encodingVersion: ver,
|
|
supported: false,
|
|
note: 'Legacy v1 account; migrate with login --new'
|
|
}
|
|
}
|
|
if (ver !== ACCOUNT_VERSION) {
|
|
return { encodingVersion: ver, supported: false }
|
|
}
|
|
return {
|
|
encodingVersion: ver,
|
|
supported: true,
|
|
kdf: BARE_OS_ACCOUNT_CRYPTO_PROFILE_V2.kdf,
|
|
aead: BARE_OS_ACCOUNT_CRYPTO_PROFILE_V2.aead,
|
|
signing: BARE_OS_ACCOUNT_CRYPTO_PROFILE_V2.signing,
|
|
publicKeyBytes: ED25519_PUBLIC_KEY_LENGTH
|
|
}
|
|
}
|