Skip to content

Commit

Permalink
Upgrade packed, move base64armor in
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed May 16, 2024
1 parent bc3ee83 commit ce13d28
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 9 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions src/pgp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)) };
},
});
Expand Down Expand Up @@ -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');
Expand Down
3 changes: 2 additions & 1 deletion src/ssh.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -35,7 +36,7 @@ export const AuthData = P.struct({

export type AuthDataType = P.UnwrapCoder<typeof AuthData>;

export const PrivateExport = P.base64armor(
export const PrivateExport = base64armor(
'openssh private key',
70,
P.struct({
Expand Down
63 changes: 63 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
name: string,
lineLen: number,
inner: CoderType<T>,
checksum?: (data: Uint8Array) => Uint8Array
): Coder<T, string> {
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('')));
},
};
}

0 comments on commit ce13d28

Please sign in to comment.