Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/lib/decrypt_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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()
Expand Down
44 changes: 37 additions & 7 deletions src/lib/ssh_agent_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
}
})
Expand Down
16 changes: 13 additions & 3 deletions test/ssh_agent_client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,15 @@ 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) {
throw new Error()
}
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' })
Expand All @@ -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')
})
})

Expand Down
Loading