diff --git a/src/crypto.ts b/src/crypto.ts index fe4a569..6181377 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,14 +1,13 @@ import { ed25519, x25519 } from '@noble/curves/ed25519.js'; import { sha256 } from "@noble/hashes/sha2.js"; import { hmac } from '@noble/hashes/hmac.js'; -import { ecb, ecb, encrypt } from '@noble/ciphers/aes.js'; -import { bytesToHex, equalBytes, hexToBytes, encodedStringToBytes } from "./parser"; +import { ecb } from '@noble/ciphers/aes.js'; +import { bytesToHex, equalBytes, hexToBytes } from "./parser"; import { IPublicKey, ISharedSecret, IStaticSecret } from './crypto.types'; import { NodeHash } from './identity.types'; const PUBLIC_KEY_SIZE = 32; const SEED_SIZE = 32; -const PRIVATE_KEY_SIZE = 32; const HMAC_SIZE = 2; const SHARED_SECRET_SIZE = 32; const SIGNATURE_SIZE = 64; @@ -19,7 +18,7 @@ export class PublicKey implements IPublicKey { constructor(key: Uint8Array | string) { if (typeof key === 'string') { - this.key = encodedStringToBytes(key, PUBLIC_KEY_SIZE); + this.key = hexToBytes(key, PUBLIC_KEY_SIZE); } else if (key instanceof Uint8Array) { this.key = key; } else { @@ -46,7 +45,7 @@ export class PublicKey implements IPublicKey { } else if (other instanceof Uint8Array) { otherKey = other; } else if (typeof other === 'string') { - otherKey = encodedStringToBytes(other, PUBLIC_KEY_SIZE); + otherKey = hexToBytes(other, PUBLIC_KEY_SIZE); } else { throw new Error('Invalid type for PublicKey comparison'); } @@ -67,7 +66,7 @@ export class PrivateKey { constructor(seed: Uint8Array | string) { if (typeof seed === 'string') { - seed = encodedStringToBytes(seed, SEED_SIZE); + seed = hexToBytes(seed, SEED_SIZE); } if (seed.length !== SEED_SIZE) { throw new Error(`Invalid seed length: expected ${SEED_SIZE} bytes, got ${seed.length}`); @@ -165,15 +164,6 @@ export class SharedSecret implements ISharedSecret { return plaintext.slice(0, end); } - private zeroPad(data: Uint8Array): Uint8Array { - if (data.length % 16 === 0) { - return data; - } - const padded = new Uint8Array(Math.ceil(data.length / 16) * 16); - padded.set(data); - return padded; - } - public encrypt(data: Uint8Array): { hmac: Uint8Array, ciphertext: Uint8Array } { const key = this.secret.slice(0, 16); const cipher = ecb(key, { disablePadding: true }); @@ -204,7 +194,7 @@ export class SharedSecret implements ISharedSecret { static fromName(name: string): SharedSecret { if (name === "Public") { - return new SharedSecret(hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72")); + return new SharedSecret(hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72", 16)); } else if (!/^#/.test(name)) { throw new Error("Only the 'Public' group or groups starting with '#' are supported"); } @@ -218,7 +208,7 @@ export class StaticSecret implements IStaticSecret { constructor(secret: Uint8Array | string) { if (typeof secret === 'string') { - secret = encodedStringToBytes(secret, STATIC_SECRET_SIZE); + secret = hexToBytes(secret, STATIC_SECRET_SIZE); } if (secret.length !== STATIC_SECRET_SIZE) { throw new Error(`Invalid static secret length: expected ${STATIC_SECRET_SIZE} bytes, got ${secret.length}`); diff --git a/src/packet.ts b/src/packet.ts index b004812..1ff3e6f 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -17,8 +17,8 @@ import { TextPayload, TracePayload, type IPacket, - type NodeHash } from "./packet.types"; +import { NodeHash } from "./identity.types"; import { base64ToBytes, BufferReader, diff --git a/src/parser.test.ts b/src/parser.test.ts index 19f8319..5f7abd2 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -1,14 +1,14 @@ import { describe, it, expect } from 'vitest'; -import { base64ToBytes, encodedStringToBytes, BufferReader, BufferWriter } from './parser'; +import { base64ToBytes, hexToBytes, BufferReader, BufferWriter } from './parser'; describe('base64ToBytes', () => { it('decodes a simple base64 string', () => { - const bytes = base64ToBytes('aGVsbG8='); // "hello" + const bytes = base64ToBytes('aGVsbG8=', 5); // "hello" expect(Array.from(bytes)).toEqual([104, 101, 108, 108, 111]); }); it('handles empty string', () => { - const bytes = base64ToBytes(''); + const bytes = base64ToBytes('', 0); expect(bytes).toBeInstanceOf(Uint8Array); expect(bytes.length).toBe(0); }); @@ -69,24 +69,20 @@ describe('sizedStringToBytes', () => { it('decodes hex string of correct length', () => { // 4 bytes = 8 hex chars const hex = 'deadbeef'; - const result = encodedStringToBytes(hex, 4); + const result = hexToBytes(hex, 4); expect(Array.from(result)).toEqual([0xde, 0xad, 0xbe, 0xef]); }); it('decodes base64 string of correct length', () => { // 4 bytes = 8 hex chars, base64 for [0xde, 0xad, 0xbe, 0xef] is '3q2+7w==' const b64 = '3q2+7w=='; - const result = encodedStringToBytes(b64, 4); + const result = base64ToBytes(b64, 4); expect(Array.from(result)).toEqual([0xde, 0xad, 0xbe, 0xef]); }); it('throws on invalid string length', () => { - expect(() => encodedStringToBytes('abc', 4)).toThrow( - /Invalid input: .*, or raw string of size 4/ - ); - expect(() => encodedStringToBytes('deadbeef00', 4)).toThrow( - /Invalid input: .*, or raw string of size 4/ - ); + expect(() => hexToBytes('abc', 4)).toThrow(); + expect(() => hexToBytes('deadbeef00', 4)).toThrow(); }); }); @@ -182,17 +178,18 @@ describe('BufferWriter', () => { it('encodedStringToBytes decodes raw string', () => { const str = String.fromCharCode(0xde, 0xad, 0xbe, 0xef); - const result = encodedStringToBytes(str, 4); - expect(Array.from(result)).toEqual([0xde, 0xad, 0xbe, 0xef]); + const bytes = new Uint8Array(4); + for (let i = 0; i < 4; i++) bytes[i] = str.charCodeAt(i) & 0xff; + expect(Array.from(bytes)).toEqual([0xde, 0xad, 0xbe, 0xef]); }); - it('encodedStringToBytes rejects hex string of wrong length', () => { - expect(() => encodedStringToBytes('deadbe', 4)).toThrow(); + it('hexToBytes returns different length for wrong-size hex', () => { + expect(() => hexToBytes('deadbe', 4)).toThrow(); }); it('base64ToBytes handles URL-safe base64', () => { // [0xde, 0xad, 0xbe, 0xef] in URL-safe base64: '3q2-7w==' - const bytes = base64ToBytes('3q2-7w=='); + const bytes = base64ToBytes('3q2-7w==', 4); expect(Array.from(bytes)).toEqual([0xde, 0xad, 0xbe, 0xef]); }); }); diff --git a/src/parser.ts b/src/parser.ts index bd212b8..ad85165 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,13 +1,12 @@ import { equalBytes } from "@noble/ciphers/utils.js"; -import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; +import { bytesToHex, hexToBytes as nobleHexToBytes } from "@noble/hashes/utils.js"; export { bytesToHex, - hexToBytes, equalBytes }; -export const base64ToBytes = (base64: string): Uint8Array => { +export const base64ToBytes = (base64: string, size?: number): Uint8Array => { // Normalize URL-safe base64 to standard base64 let normalized = base64.replace(/-/g, '+').replace(/_/g, '/'); // Add padding if missing @@ -19,32 +18,23 @@ export const base64ToBytes = (base64: string): Uint8Array => { for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } + if (size !== undefined && bytes.length !== size) { + throw new Error(`Invalid base64 length: expected ${size} bytes, got ${bytes.length}`); + } return bytes; } -export const encodedStringToBytes = (str: string, size: number): Uint8Array => { - const hexRegex = /^[0-9a-fA-F]+$/; - // Accept both standard and URL-safe base64, with or without padding - const b64Regex = /^(?:[A-Za-z0-9+\/_-]{4})*(?:[A-Za-z0-9+\/_-]{2}(?:==)?|[A-Za-z0-9+\/_-]{3}=?)?$/; +// Note: encodedStringToBytes removed — prefer explicit parsers. +// Use `hexToBytes` for hex inputs and `base64ToBytes` for base64 inputs. - if (hexRegex.test(str) && str.length === size * 2) { - return hexToBytes(str); - } else if (b64Regex.test(str)) { - const bytes = base64ToBytes(str); - if (bytes.length === size) { - return bytes; - } - } else if (str.length === size) { - // Raw format: treat as bytes (latin1) - const bytes = new Uint8Array(size); - for (let i = 0; i < size; i++) { - bytes[i] = str.charCodeAt(i) & 0xFF; - } - return bytes; +// Wrapper around @noble/hashes hexToBytes that optionally validates size. +export const hexToBytes = (hex: string, size?: number): Uint8Array => { + const bytes = nobleHexToBytes(hex); + if (size !== undefined && bytes.length !== size) { + throw new Error(`Invalid hex length: expected ${size} bytes, got ${bytes.length}`); } - - throw new Error(`Invalid input: expected hex, base64 (standard or URL-safe), or raw string of size ${size}`); -} + return bytes; +}; export class BufferReader { private buffer: Uint8Array; @@ -56,6 +46,7 @@ export class BufferReader { } public readByte(): number { + if (!this.hasMore()) throw new Error('read past end'); return this.buffer[this.offset++]; } @@ -63,6 +54,7 @@ export class BufferReader { if (length === undefined) { length = this.buffer.length - this.offset; } + if (this.remainingBytes() < length) throw new Error('read past end'); const bytes = this.buffer.slice(this.offset, this.offset + length); this.offset += length; return bytes; @@ -77,31 +69,36 @@ export class BufferReader { } public peekByte(): number { + if (!this.hasMore()) throw new Error('read past end'); return this.buffer[this.offset]; } public readUint16LE(): number { + if (this.remainingBytes() < 2) throw new Error('read past end'); const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8); this.offset += 2; return value; } public readUint32LE(): number { - const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24); + if (this.remainingBytes() < 4) throw new Error('read past end'); + const value = (this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24)) >>> 0; this.offset += 4; return value; } public readInt16LE(): number { + if (this.remainingBytes() < 2) throw new Error('read past end'); const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8); this.offset += 2; return value < 0x8000 ? value : value - 0x10000; } public readInt32LE(): number { - const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24); + if (this.remainingBytes() < 4) throw new Error('read past end'); + const u = (this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8) | (this.buffer[this.offset + 2] << 16) | (this.buffer[this.offset + 3] << 24)) >>> 0; this.offset += 4; - return value < 0x80000000 ? value : value - 0x100000000; + return u < 0x80000000 ? u : u - 0x100000000; } public readTimestamp(): Date {