From ce13d28e793d6531d58cacd64a8038d129b0515f Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Thu, 16 May 2024 04:52:26 +0000 Subject: [PATCH] Upgrade packed, move base64armor in --- package-lock.json | 8 +++--- package.json | 2 +- src/pgp.ts | 7 +++--- src/ssh.ts | 3 ++- src/utils.ts | 63 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index af6a5ea..a216742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6", - "micro-packed": "~0.5.3" + "micro-packed": "~0.6.1" }, "devDependencies": { "@paulmillr/jsbt": "0.1.0", @@ -73,9 +73,9 @@ } }, "node_modules/micro-packed": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.5.3.tgz", - "integrity": "sha512-zWRoH+qUb/ZMp9gVZhexvRGCENDM5HEQF4sflqpdilUHWK2/zKR7/MT8GBctnTwbhNJwy1iuk5q6+TYP7/twYA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/micro-packed/-/micro-packed-0.6.1.tgz", + "integrity": "sha512-L990sYu+qWS4OWegPKyVDEpuVtnk6xxcZICU17Bp0it93eTaPsqMhJeqQvHj586P3JDEalWTEs4uIwheHxeEWg==", "dependencies": { "@scure/base": "~1.1.5" }, diff --git a/package.json b/package.json index cb0a754..ba6f0fe 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@noble/curves": "~1.4.0", "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6", - "micro-packed": "~0.5.3" + "micro-packed": "~0.6.1" }, "devDependencies": { "@paulmillr/jsbt": "0.1.0", diff --git a/src/pgp.ts b/src/pgp.ts index dd83bf1..1a1ce8f 100644 --- a/src/pgp.ts +++ b/src/pgp.ts @@ -9,6 +9,7 @@ import { sha3_256 } from '@noble/hashes/sha3'; import { CHash, concatBytes, randomBytes } from '@noble/hashes/utils'; import { utf8, hex } from '@scure/base'; import * as P from 'micro-packed'; +import { base64armor } from './utils.js'; export type Bytes = Uint8Array; @@ -474,7 +475,7 @@ const Packet = P.wrap({ decodeStream: (r: P.Reader): any => { const { tag, lenType } = PacketHead.decodeStream(r); const packetLen = - lenType !== 3 ? [P.U8, P.U16BE, P.U32BE][lenType].decodeStream(r) : r.data.length - r.pos; + lenType !== 3 ? [P.U8, P.U16BE, P.U32BE][lenType].decodeStream(r) : r.leftBytes; return { TAG: tag, data: PacketTags[tag].decode(r.bytes(packetLen)) }; }, }); @@ -551,8 +552,8 @@ function createPrivKey( return { pub, type: { TAG: 'encrypted', data: { enc, S2K, iv, secret } } }; } -export const pubArmor = P.base64armor('PGP PUBLIC KEY BLOCK', 64, Stream, crc24); -export const privArmor = P.base64armor('PGP PRIVATE KEY BLOCK', 64, Stream, crc24); +export const pubArmor = base64armor('PGP PUBLIC KEY BLOCK', 64, Stream, crc24); +export const privArmor = base64armor('PGP PRIVATE KEY BLOCK', 64, Stream, crc24); function validateDate(timestamp: number) { if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp > 2 ** 46) throw new Error('invalid PGP key creation time: must be a valid UNIX timestamp'); diff --git a/src/ssh.ts b/src/ssh.ts index 6254559..c75add8 100644 --- a/src/ssh.ts +++ b/src/ssh.ts @@ -1,6 +1,7 @@ import { ed25519 } from '@noble/curves/ed25519'; import { sha256 } from '@noble/hashes/sha256'; import { concatBytes, randomBytes } from '@noble/hashes/utils'; +import { base64armor } from './utils.js'; import { base64 } from '@scure/base'; import * as P from 'micro-packed'; @@ -35,7 +36,7 @@ export const AuthData = P.struct({ export type AuthDataType = P.UnwrapCoder; -export const PrivateExport = P.base64armor( +export const PrivateExport = base64armor( 'openssh private key', 70, P.struct({ diff --git a/src/utils.ts b/src/utils.ts index 4ff9594..e1a2892 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,2 +1,65 @@ import { randomBytes } from '@noble/hashes/utils'; +import { Coder, CoderType, utils as pkUtils } from 'micro-packed'; +import { base64 } from '@scure/base'; export { randomBytes }; + +/** + * Base64-armored values are commonly used in cryptographic applications, such as PGP and SSH. + * @param name - The name of the armored value. + * @param lineLen - Maximum line length for the armored value (e.g., 64 for GPG, 70 for SSH). + * @param inner - Inner CoderType for the value. + * @param checksum - Optional checksum function. + * @returns Coder representing the base64-armored value. + * @example + * // Base64-armored value without checksum + * const armoredValue = P.base64armor('EXAMPLE', 64, P.bytes(null)); + */ +export function base64armor( + name: string, + lineLen: number, + inner: CoderType, + checksum?: (data: Uint8Array) => Uint8Array +): Coder { + if (typeof name !== 'string' || name.length === 0) + throw new Error('name must be a non-empty string'); + if (!Number.isSafeInteger(lineLen) || lineLen <= 0) + throw new Error('lineLen must be a positive integer'); + if (!pkUtils.isCoder(inner)) throw new Error('inner must be a valid base coder'); + if (checksum !== undefined && typeof checksum !== 'function') + throw new Error('checksum must be a function or undefined'); + const markBegin = `-----BEGIN ${name.toUpperCase()}-----`; + const markEnd = `-----END ${name.toUpperCase()}-----`; + return { + encode(value: T) { + const data = inner.encode(value); + const encoded = base64.encode(data); + const lines = []; + for (let i = 0; i < encoded.length; i += lineLen) { + const s = encoded.slice(i, i + lineLen); + if (s.length) lines.push(`${encoded.slice(i, i + lineLen)}\n`); + } + let body = lines.join(''); + if (checksum) body += `=${base64.encode(checksum(data))}\n`; + return `${markBegin}\n\n${body}${markEnd}\n`; + }, + decode(s: string): T { + if (typeof s !== 'string') throw new Error('string expected'); + const beginPos = s.indexOf(markBegin); + const endPos = s.indexOf(markEnd); + if (beginPos === -1 || endPos === -1 || beginPos >= endPos) + throw new Error('invalid armor format'); + let lines = s.replace(markBegin, '').replace(markEnd, '').trim().split('\n'); + if (lines.length === 0) throw new Error('no data found in armor'); + lines = lines.map((l) => l.replace('\r', '').trim()); + const last = lines.length - 1; + if (checksum && lines[last].startsWith('=')) { + const body = base64.decode(lines.slice(0, -1).join('')); + const cs = lines[last].slice(1); + const realCS = base64.encode(checksum(body)); + if (realCS !== cs) throw new Error(`invalid checksum ${cs} instead of ${realCS}`); + return inner.decode(body); + } + return inner.decode(base64.decode(lines.join(''))); + }, + }; +}