Orginize
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Opaque key-handle registry: TTL, optional max uses, revocation (no key material).
|
||||
*/
|
||||
|
||||
/** @typedef {{ id: string, purpose: string, algorithm: string, atMs: number, expiresAtMs: number | null, maxUses: number, uses: number, scopes: string[], revoked: boolean }} BareOsSecretHandleRow */
|
||||
|
||||
/** @type {Map<string, BareOsSecretHandleRow>} */
|
||||
const handles = new Map()
|
||||
let seq = 0
|
||||
|
||||
const MAX_HANDLES = 4096
|
||||
|
||||
/**
|
||||
* @param {{ purpose?: string, algorithm?: string, ttlMs?: number | null, maxUses?: number, scopes?: string[] }} opts
|
||||
*/
|
||||
export function bareOsSecretHandleAcquire(opts = {}) {
|
||||
if (handles.size >= MAX_HANDLES) {
|
||||
for (const [id, h] of handles) {
|
||||
if (h.revoked || bareOsSecretHandleIsExpired(h)) {
|
||||
handles.delete(id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const purpose = String(opts.purpose || 'default').trim().slice(0, 64) || 'default'
|
||||
const algorithm = String(opts.algorithm || 'opaque').trim().slice(0, 32) || 'opaque'
|
||||
const ttlRaw =
|
||||
opts.ttlMs != null
|
||||
? Number.parseInt(String(opts.ttlMs), 10)
|
||||
: Number.NaN
|
||||
const ttlMs =
|
||||
Number.isFinite(ttlRaw) && ttlRaw > 0
|
||||
? Math.min(ttlRaw, 86400000)
|
||||
: null
|
||||
const maxUses =
|
||||
opts.maxUses != null && Number.isFinite(Number(opts.maxUses)) && Number(opts.maxUses) > 0
|
||||
? Math.min(1e6, Math.floor(Number(opts.maxUses)))
|
||||
: 1e9
|
||||
const scopes = Array.isArray(opts.scopes)
|
||||
? opts.scopes
|
||||
.map((s) => String(s).trim().slice(0, 48))
|
||||
.filter(Boolean)
|
||||
.slice(0, 8)
|
||||
: []
|
||||
const atMs = Date.now()
|
||||
const id = `kh_${++seq}_${atMs.toString(36)}`
|
||||
const row = {
|
||||
id,
|
||||
purpose,
|
||||
algorithm,
|
||||
atMs,
|
||||
expiresAtMs: ttlMs != null ? atMs + ttlMs : null,
|
||||
maxUses,
|
||||
uses: 0,
|
||||
scopes: scopes.length ? scopes : ['default'],
|
||||
revoked: false
|
||||
}
|
||||
handles.set(id, row)
|
||||
return {
|
||||
handle: id,
|
||||
algorithm,
|
||||
atMs,
|
||||
ttlMs,
|
||||
scopes: row.scopes,
|
||||
zeroizeAfterMs: ttlMs
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {BareOsSecretHandleRow} h */
|
||||
function bareOsSecretHandleIsExpired(h) {
|
||||
if (h.revoked) return true
|
||||
if (h.expiresAtMs != null && Date.now() > h.expiresAtMs) return true
|
||||
if (h.uses >= h.maxUses) return true
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function bareOsSecretHandleTouch(id) {
|
||||
const h = handles.get(String(id || ''))
|
||||
if (!h || bareOsSecretHandleIsExpired(h)) return false
|
||||
h.uses++
|
||||
if (h.uses >= h.maxUses) h.revoked = true
|
||||
return !h.revoked
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export function bareOsSecretHandleRelease(id) {
|
||||
const h = handles.get(String(id || ''))
|
||||
if (h) h.revoked = true
|
||||
handles.delete(String(id || ''))
|
||||
}
|
||||
|
||||
export function bareOsSecretHandlePruneExpired() {
|
||||
const now = Date.now()
|
||||
for (const [id, h] of handles) {
|
||||
if (h.revoked || (h.expiresAtMs != null && now > h.expiresAtMs) || h.uses >= h.maxUses)
|
||||
handles.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
/** Non-secret snapshot for `/proc` / posture. */
|
||||
export function bareOsSecretHandleSnapshot() {
|
||||
bareOsSecretHandlePruneExpired()
|
||||
let active = 0
|
||||
for (const h of handles.values()) {
|
||||
if (!bareOsSecretHandleIsExpired(h)) active++
|
||||
}
|
||||
return {
|
||||
schema: 1,
|
||||
totalRegistered: handles.size,
|
||||
activeHandles: active,
|
||||
cap: MAX_HANDLES,
|
||||
atMs: Date.now()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Plaintext NDJSON audit on the personal drive (no sealed vault material).
|
||||
*/
|
||||
|
||||
import b4a from 'b4a'
|
||||
|
||||
/**
|
||||
* Append a non-secret rotation / handoff checkpoint to the personal drive (plaintext NDJSON audit).
|
||||
* Signing keys stay in the vault; this records operator metadata only.
|
||||
* Rows may include **`kind`**: **`vault_save`** (after vault snapshot) or rotation handoff fields from callers.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, unknown>} row
|
||||
*/
|
||||
export async function bareOsAppendVaultRotationCheckpoint(ctx, row) {
|
||||
const drive = ctx.personalDrive
|
||||
if (!drive || typeof drive.put !== 'function') {
|
||||
throw new Error('bareOsAppendVaultRotationCheckpoint: personal drive unavailable')
|
||||
}
|
||||
const line =
|
||||
JSON.stringify({
|
||||
schema: 1,
|
||||
ts: Date.now(),
|
||||
...row
|
||||
}) + '\n'
|
||||
const path = '/.bare/vault-rotation-audit.ndjson'
|
||||
let prev = ''
|
||||
try {
|
||||
const b = await drive.get(path, { follow: false })
|
||||
if (b) prev = b4a.toString(b)
|
||||
} catch {
|
||||
/* new */
|
||||
}
|
||||
await drive.put(path, b4a.from(prev + line))
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* WWW + managed Holesail + swarm chat initd units — registered and started only after identity unlock,
|
||||
* stopped on logout. Guest boot skips this stack entirely.
|
||||
*/
|
||||
import {
|
||||
findBareServiceDefinition,
|
||||
startBareService,
|
||||
stopBareService
|
||||
} from './bare-initd.js'
|
||||
import { maybeRegisterBareHolesailInitd } from './bare-holesail.js'
|
||||
import { maybeRegisterBareOsChatInitd } from './bare-os-chat-initd.js'
|
||||
import { maybeRegisterBareOsWwwInitd } from './bare-os-www-initd.js'
|
||||
import {
|
||||
maybeStartBareOsDiscordAfterIdentity,
|
||||
stopBareOsDiscordInitd
|
||||
} from './bare-os-discord-initd.js'
|
||||
import { bareOpensshGetListenEndpoint } from './bare-openssh.js'
|
||||
import { ensureBareOsSshHolesailTunnel } from './bare-os-ssh-holesail.js'
|
||||
import {
|
||||
bareOsChatMuxEnabled,
|
||||
ensureDiskBareOsChatTransport
|
||||
} from './bare-os-chat-service.js'
|
||||
import {
|
||||
bareOsMeshdropMuxEnabled,
|
||||
ensureDiskBareOsMeshdropTransport
|
||||
} from './bare-os-meshdrop-service.js'
|
||||
|
||||
/** @param {Record<string, string | undefined>} env */
|
||||
export function registerBareUserSessionStackUnits(env) {
|
||||
maybeRegisterBareHolesailInitd(env)
|
||||
maybeRegisterBareOsWwwInitd(env)
|
||||
maybeRegisterBareOsChatInitd(env)
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotent: registers units once, attaches chat service to `ctx.disk` when mux is enabled, then starts
|
||||
* **bare-os-www** → **bare-holesail** → **bare-os-chat** (initd DAG order).
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function startBareUserSessionStack(ctx) {
|
||||
const env =
|
||||
ctx.env && typeof ctx.env === 'object'
|
||||
? /** @type {Record<string, string | undefined>} */ (ctx.env)
|
||||
: /** @type {Record<string, string | undefined>} */ ({})
|
||||
|
||||
registerBareUserSessionStackUnits(env)
|
||||
|
||||
const disk = /** @type {{ bareOsChatService?: unknown }} */ (ctx.disk)
|
||||
if (disk && bareOsChatMuxEnabled(globalThis.process?.env)) {
|
||||
ensureDiskBareOsChatTransport(
|
||||
/** @type {import('./swarm-disk.js').SwarmDisk} */ (disk),
|
||||
env
|
||||
)
|
||||
}
|
||||
if (disk && bareOsMeshdropMuxEnabled(globalThis.process?.env)) {
|
||||
ensureDiskBareOsMeshdropTransport(
|
||||
/** @type {import('./swarm-disk.js').SwarmDisk} */ (disk),
|
||||
env
|
||||
)
|
||||
}
|
||||
|
||||
/** Peers may have connected during guest boot; pair chat after this tick (avoid mux re-entrancy). */
|
||||
if (disk && typeof disk.pairBareOsChatExistingPeers === 'function') {
|
||||
await new Promise((r) => setImmediate(r))
|
||||
disk.pairBareOsChatExistingPeers()
|
||||
}
|
||||
if (disk && typeof disk.pairBareOsMeshdropExistingPeers === 'function') {
|
||||
await new Promise((r) => setImmediate(r))
|
||||
disk.pairBareOsMeshdropExistingPeers()
|
||||
}
|
||||
|
||||
await maybeStartBareOsDiscordAfterIdentity(ctx)
|
||||
|
||||
const order = ['bare-os-www', 'bare-holesail', 'bare-os-chat']
|
||||
for (const name of order) {
|
||||
if (!findBareServiceDefinition(name)) continue
|
||||
try {
|
||||
await startBareService(ctx, name)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-user-stack] start ${name}: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** bare-openssh may already be listening from guest boot — run managed tunnel ensure after holesail is up. */
|
||||
try {
|
||||
const ep = bareOpensshGetListenEndpoint()
|
||||
if (ep) await ensureBareOsSshHolesailTunnel(ctx, env, ep.port, ep.host)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-user-stack] ssh holesail ensure: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops **bare-holesail** (before www so the tunnel drops first), **bare-os-www**, **bare-os-chat**.
|
||||
* Swarm chat **transport** (`disk.bareOsChatService`) stays; `logoutIdentity` refreshes it for guest.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
*/
|
||||
export async function stopBareUserSessionStack(ctx) {
|
||||
await stopBareOsDiscordInitd(ctx)
|
||||
|
||||
const order = ['bare-holesail', 'bare-os-www', 'bare-os-chat']
|
||||
for (const name of order) {
|
||||
if (!findBareServiceDefinition(name)) continue
|
||||
try {
|
||||
await stopBareService(ctx, name)
|
||||
} catch (e) {
|
||||
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||
try {
|
||||
ctx.console?.error?.(`[bare-user-stack] stop ${name}: ${msg}`)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user