From e55e3ece95bb401bd4c2aa0bd18eb1d76698ac0b Mon Sep 17 00:00:00 2001 From: Damien Debin Date: Mon, 30 Mar 2026 12:03:19 +0200 Subject: [PATCH] check for unknown digest --- src/lib/decrypt_transform.ts | 10 ++++---- src/lib/ssh_agent_client.ts | 44 +++++++++++++++++++++++++++++------ test/ssh_agent_client.spec.ts | 16 ++++++++++--- 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/lib/decrypt_transform.ts b/src/lib/decrypt_transform.ts index 5cd2ad3..a023969 100644 --- a/src/lib/decrypt_transform.ts +++ b/src/lib/decrypt_transform.ts @@ -5,20 +5,20 @@ export class DecryptTransform extends Transform { private decipher?: crypto.Decipher private algo: string private cipherKey: crypto.KeyObject - private cipherIvLength: number + private ivLength: number private inputEncoding?: BufferEncoding constructor( algo: string, cipherKey: crypto.KeyObject, - cipherIvLength: number, + ivLength: number, inputEncoding?: BufferEncoding, opts?: TransformOptions, ) { super(opts) this.algo = algo this.cipherKey = cipherKey - this.cipherIvLength = cipherIvLength + this.ivLength = ivLength this.inputEncoding = inputEncoding } @@ -30,9 +30,9 @@ export class DecryptTransform extends Transform { if (!this.decipher) { // Unpackage the combined iv + encrypted message. // Since we are using a fixed size IV, we can hard code the slice length. - const iv = data.subarray(0, this.cipherIvLength) + const iv = data.subarray(0, this.ivLength) this.decipher = crypto.createDecipheriv(this.algo, this.cipherKey, iv) - data = data.subarray(this.cipherIvLength) + data = data.subarray(this.ivLength) } this.push(this.decipher.update(data)) callback() diff --git a/src/lib/ssh_agent_client.ts b/src/lib/ssh_agent_client.ts index 18e1f19..14b8593 100644 --- a/src/lib/ssh_agent_client.ts +++ b/src/lib/ssh_agent_client.ts @@ -74,13 +74,13 @@ export class SSHAgentClient { /** * @param options - Optional configuration. - * @throws {Error} if SSH_AUTH_SOCK is not set. + * @throws {Error} if SSH_AUTH_SOCK is not set or socket not found. */ constructor(options: SSHAgentClientOptions = {}) { /** Socket operation timeout in milliseconds (default: 10000) */ this.timeout = options.timeout ?? 10000 - /** Encryption and algo key length must match */ + /** Digest and cipher key length must match */ this.cipherAlgo = options.cipherAlgo ?? 'aes-256-cbc' this.digestAlgo = options.digestAlgo ?? 'sha256' @@ -180,6 +180,12 @@ export class SSHAgentClient { return this.request(buildRequest, parseResponse, Protocol.SSH2_AGENT_SIGN_RESPONSE) } + /** + * Encrypt data with given `SSHKey` and `seed` string, using SSH signature as the encryption key. + * + * Resolves with a string containing the IV and encrypted data, encoded in `outputEncoding` (default: "hex"). + * The IV is needed for decryption and is included in the output as a prefix to the encrypted data. + */ async encrypt( key: SSHKey, seed: string, @@ -192,6 +198,13 @@ export class SSHAgentClient { ) } + /** + * Get a Transform stream that encrypts data with given `SSHKey` and `seed` string, using SSH signature as + * the encryption key. + * + * The Transform stream outputs plain binary or a string encoded in `outputEncoding`. + * The IV is needed for decryption and is included in the output as a prefix to the encrypted data. + */ async getEncryptTransform( key: SSHKey, seed: string, @@ -206,6 +219,12 @@ export class SSHAgentClient { }) } + /** + * Decrypt data with given `SSHKey` and `seed` string, using SSH signature as the decryption key. + * + * Resolves with a Buffer containing the decrypted data. + * The IV is needed for decryption and should be included in the input as a prefix to the encrypted data. + */ async decrypt( key: SSHKey, seed: string, @@ -223,6 +242,14 @@ export class SSHAgentClient { }) } + /** + * Get a Transform stream that decrypts data with given `SSHKey` and `seed` string, using SSH signature as + * the decryption key. + * + * The Transform stream expects encrypted data (as plain binary or a string encoded in `inputEncoding`) and + * outputs a Buffer containing the decrypted data. + * The IV is needed for decryption and should be included in the input as a prefix to the encrypted data. + */ async getDecryptTransform( key: SSHKey, seed: string, @@ -252,14 +279,17 @@ export class SSHAgentClient { return this.sign(key, Buffer.from(seed, 'utf8')).then(signature => { const cipherInfo = crypto.getCipherInfo(this.cipherAlgo) if (!cipherInfo?.ivLength) { - throw new Error('Wrong cipher algo') + throw new Error('Unknown symmetric cipher algo') + } + if (!crypto.getHashes().includes(this.digestAlgo)) { + throw new Error('Unknown digest algo') } - const hash = crypto.createHash(this.digestAlgo).update(signature.raw).digest() - if (hash.length < cipherInfo.keyLength) { - throw new Error("Digest algo doesn't match cipher key length") + const digest = crypto.createHash(this.digestAlgo).update(signature.raw).digest() + if (digest.length < cipherInfo.keyLength) { + throw new Error("Digest length doesn't match cipher key length") } return { - cipherKey: crypto.createSecretKey(hash.subarray(0, cipherInfo.keyLength)), + cipherKey: crypto.createSecretKey(digest.subarray(0, cipherInfo.keyLength)), ivLength: cipherInfo.ivLength, } }) diff --git a/test/ssh_agent_client.spec.ts b/test/ssh_agent_client.spec.ts index 33908a7..d1f1ac2 100644 --- a/test/ssh_agent_client.spec.ts +++ b/test/ssh_agent_client.spec.ts @@ -102,7 +102,7 @@ describe('SSHAgentClient tests', () => { .expect(agent.decrypt(identity, SEED, data)) .to.be.rejectedWith(Error, 'error:1C80006B:Provider routines::wrong final block length') }) - it('should throw if unknown cipher', async () => { + it('should throw if cipher algorithm is unknown', async () => { const agent = new SSHAgentClient({ cipherAlgo: 'xxx' }) const identity = await agent.getIdentity('key_rsa') if (!identity) { @@ -110,7 +110,7 @@ describe('SSHAgentClient tests', () => { } return chai .expect(agent.encrypt(identity, SEED, DECODED_STRING_BUFFER)) - .to.be.rejectedWith(Error, 'Wrong cipher algo') + .to.be.rejectedWith(Error, 'Unknown symmetric cipher algo') }) it('should throw if digest length is less than cipher key length', async () => { const agent = new SSHAgentClient({ cipherAlgo: 'aes-192-cbc', digestAlgo: 'sha1' }) @@ -120,7 +120,17 @@ describe('SSHAgentClient tests', () => { } return chai .expect(agent.encrypt(identity, SEED, DECODED_STRING_BUFFER)) - .to.be.rejectedWith(Error, "Digest algo doesn't match cipher key length") + .to.be.rejectedWith(Error, "Digest length doesn't match cipher key length") + }) + it('should throw if hash algorithm is unknown', async () => { + const agent = new SSHAgentClient({ digestAlgo: 'xxx' }) + const identity = await agent.getIdentity('key_rsa') + if (!identity) { + throw new Error() + } + return chai + .expect(agent.encrypt(identity, SEED, DECODED_STRING_BUFFER)) + .to.be.rejectedWith(Error, 'Unknown digest algo') }) })