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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ A seed is used to generate the secret, it's recommended you don't use the same s
## ⚠️ Limitations

- Can't use ECDSA keys, they always give different signatures
- [RFC8332](https://www.rfc-editor.org/info/rfc8332) compatible agent (e.g. OpenSSH 7.6+) mandatory to use SHA2-512 signature scheme. You can still use deprecated SHA1 signatures with `rsaSignatureFlag: 0` option in `SSHAgentClient` constructor.

## 💻 CLI usage

Expand Down Expand Up @@ -93,3 +94,10 @@ const decrypted = await agent.decrypt(
)
console.log('Decrypted data:', decrypted.toString('utf8'))
```

## Local test

```bash
ssh-agent -D
SSH_AUTH_SOCK= ssh-add id_ecdsa id_ed25519 id_rsa
```
15 changes: 14 additions & 1 deletion src/lib/ssh_agent_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const Protocol = {

type Protocol = (typeof Protocol)[keyof typeof Protocol]

export const RsaSignatureFlag = {
DEFAULT: 0,
SSH_AGENT_RSA_SHA2_256: 2,
SSH_AGENT_RSA_SHA2_512: 4,
} as const

type RsaSignatureFlag = (typeof RsaSignatureFlag)[keyof typeof RsaSignatureFlag]

export interface SSHKey {
/** E.g. "ssh-rsa" */
type: string
Expand All @@ -40,6 +48,8 @@ export interface SSHAgentClientOptions {
sockFile?: string
cipherAlgo?: string
digestAlgo?: string
/** RSA signature method flag for signing request when using RSA keys */
rsaSignatureFlag?: RsaSignatureFlag
}

/** Read a length-prefixed string (uint32 BE length + bytes) from a buffer. */
Expand Down Expand Up @@ -71,6 +81,7 @@ export class SSHAgentClient {
private readonly sockFile: string
private readonly cipherAlgo: string
private readonly digestAlgo: string
private readonly rsaSignatureFlag: RsaSignatureFlag

/**
* @param options - Optional configuration.
Expand All @@ -84,6 +95,8 @@ export class SSHAgentClient {
this.cipherAlgo = options.cipherAlgo ?? 'aes-256-cbc'
this.digestAlgo = options.digestAlgo ?? 'sha256'

this.rsaSignatureFlag = options.rsaSignatureFlag ?? RsaSignatureFlag.SSH_AGENT_RSA_SHA2_512

const sockFile = options.sockFile ?? process.env.SSH_AUTH_SOCK
if (!sockFile || !existsSync(sockFile)) {
throw new Error(`Socket ${sockFile ?? '?'} not found`)
Expand Down Expand Up @@ -161,7 +174,7 @@ export class SSHAgentClient {
let offset = writeHeader(req, Protocol.SSH2_AGENTC_SIGN_REQUEST)
offset = writeString(req, key.raw, offset)
offset = writeString(req, data, offset)
req.writeUInt32BE(0, offset) // Flags = 0
req.writeUInt32BE(this.rsaSignatureFlag, offset)
return req
}

Expand Down
4 changes: 2 additions & 2 deletions test/ssh_agent_cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ describe('ssh-crypt cli tests', () => {
})
it('should decrypt', () => {
const data =
'ecfd6bb57f4891ba7226886e90d2eb848022a495b15ffd91ffe760bca5605f9062c305ee14226d9daf7faa58460c8f50'
'5f1979820d75926171e7028d5938f64ba5872683334f21b14947df1a4cce1f9ff1bb7c9c91e28e49aa8807b02c18c48c'
const output = execSync(
'npm exec -- tsx src/cli.ts -k key_rsa -s not_a_secret --decryptEncoding hex decrypt',
'npm exec -- tsx src/cli.ts -k key_ed25519 -s not_a_secret --decryptEncoding hex decrypt',
{
encoding: 'utf8',
input: data,
Expand Down
73 changes: 61 additions & 12 deletions test/ssh_agent_client.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as chai from 'chai'
import { describe, it } from 'mocha'
import { RsaSignatureFlag, SSHAgentClient } from '../src/lib/ssh_agent_client.ts'
import chaiAsPromised from 'chai-as-promised'
import { SSHAgentClient } from '../src/lib/ssh_agent_client.ts'

chai.use(chaiAsPromised)

Expand Down Expand Up @@ -65,8 +65,8 @@ describe('SSHAgentClient tests', () => {
chai.assert.strictEqual(identity.type, 'ssh-rsa')
chai.assert.strictEqual(identity.comment, 'key_rsa')
})
it('should sign', async () => {
const agent = new SSHAgentClient()
it('should sign with key_rsa and default ssh-rsa signature', async () => {
const agent = new SSHAgentClient({ rsaSignatureFlag: RsaSignatureFlag.DEFAULT })
const identity = await agent.getIdentity('key_rsa')
if (!identity) {
throw new Error()
Expand All @@ -78,6 +78,55 @@ describe('SSHAgentClient tests', () => {
'2Ox3c3wtOkmZwINZ0nSi31NP2ysWnERdaLWMcY/CONPcLK3DPBfEolG307yMdmYAsJY5VW/sgw0ye58zOu5daos2xtWxYTHUvY7peDMcJWgQJ5YFBQWGY6ku++tqR31FSlLl9KJMUnXGdE88T1RFbaWOsg8U37IsMd5juxCeakgmcvo4PXOcWlRKBnQKRaxoOl+lQxcBNb3GM2T/kqnKkae5NebaUMOI+v7U95tzNPq0xLrZ0805rNETEcLdMszi6XS8Gbh4iDZNZGV7sA5hh9rB/avhKQHYplJ9YzyLfLEX3S8bZf41xIynt62PXeEUuM5UcYRj1lUC6quGC59Z4P2pBujjvJqj9gmKjwVcwnVo28J9OEugmRnO2QmamR0LIJIhIJmdmRRFqD86vkfyYBz733KRkvA80gXvbvligTI5LZwpUoTX7YWGkpz8fCCrMJ4WRyu9nerp9EGk4afhlAb92wMSqQV3QmXI/uRjHkbs+8rfeZS6i77Xgj4AYkHp',
)
})
it('should sign with key_rsa and SHA2-256 signature', async () => {
const agent = new SSHAgentClient({ rsaSignatureFlag: RsaSignatureFlag.SSH_AGENT_RSA_SHA2_256 })
const identity = await agent.getIdentity('key_rsa')
if (!identity) {
throw new Error()
}
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
chai.assert.strictEqual(signature.type, 'rsa-sha2-256')
chai.assert.strictEqual(
signature.signature,
'InYgWb2UN3RCyQwKMWNSjljkzmMQ+6D+gwol2PfSrYaXejyKIEFH2ZwzueT0bTD8CEi0l51BZ6Lx5Pdi8JLTgdk3fneJQPayBkzq+QUINNeK8qLICYol36T2Huy2oS1TrqVvlhRZQSvJB8En6jRbsgo9qEoK4GtitiDYqNIdsG3mIEpmP8M+gA/iis6PHAxlT2cHPF8gQu43KXorZ1txvkOnahJ0LAfjB5axz+NvUkEgSbXbK3l4REFm3+TNXq/El9yqnC+5NW09v+m0dctW/YBY0eGpYZoO8pl/oXJ/47M8SOmDZjrmmAwG2zPzTq6Pctv3glVROY8sndcy32VpJs1DEEbk3MGgj2R2HkvCX6PPuhPDpMdcakpfSbOFvRDuYityeijJLA5d6v7EaWBTeH4nX10Dq4O+5CCsdb1dOhzS5gay4aedy2TwU8zpUF0GqzJwxLq7w26nokZLOoLmNFbkGsRXQqVfTPUN5aNNZqMNIdeXcjmvAb7fmcz6Gxdv',
)
})
it('should sign with key_rsa and SHA2-512 signature', async () => {
const agent = new SSHAgentClient({ rsaSignatureFlag: RsaSignatureFlag.SSH_AGENT_RSA_SHA2_512 })
const identity = await agent.getIdentity('key_rsa')
if (!identity) {
throw new Error()
}
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
chai.assert.strictEqual(signature.type, 'rsa-sha2-512')
chai.assert.strictEqual(
signature.signature,
'PEOu4DK+GXsdgd84PEEAFzQgsSz6kgCGiVgf4d464mBGyXhebFR5HBZ4RPCKDIPAu6zFt7DgAYBDmBio0LdwqgAs561ytLO+pQ1UCS1nmzE8f9n8220vGp18PSIXzDPAwlbAk9tPv940kFWbQOr1GwxmyERWC0XOdMLvueeCx5alThYWOAKbjHLhMSAry9E0I02g44UkoFV0VAgrSff03t9Y31c+n5ogpa2bii02IFg3khycrzaYv+3B+aU9kew7MhFH9awkJLFbFuQbLtOiINwhkZRnTAMbrqZPqeYpKrhHr+D6gjNhYXqNhAfZKBJFUurVAkccmkFWttmAZJwCpoiDD+yrOTYj7s5iQq5M0YlrIv3N+RP7MyN9GhvWwlD/Ti5Mnc7EckAIBIrFAja8hdJqmNbeVKD2o5tqJopvz7vvGpOiUHAZyL9Gbd74W3yjX971rZKSHsKtC7ngi3GqUK64QJ/sLjutQXnsxCWvEPaltnsT8dlyjdi2iizP1Prn',
)
})
it('should sign with ed25519', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
chai.assert.strictEqual(signature.type, 'ssh-ed25519')
chai.assert.strictEqual(
signature.signature,
'Uolma4H0fzvBSu1G/6pUYZthmFH/NjXRjP4Zx80SloXMIlTFsF/++HqOi4ooEhLoTh/ZlhAlyEONjVqqcAnWAQ==',
)
})
it('can sign with ecdsa', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_ecdsa')
if (!identity) {
throw new Error()
}
const signature = await agent.sign(identity, Buffer.from('hello', 'utf8'))
chai.assert.strictEqual(signature.type, 'ecdsa-sha2-nistp256')
// Can't assert signature value as it is not deterministic
})
it('should throw if wrong secret', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_rsa')
Expand Down Expand Up @@ -153,13 +202,13 @@ describe('SSHAgentClient cipher combination tests', () => {
const decrypted = await agent.decrypt(
identity,
SEED,
'ecfd6bb57f4891ba7226886e90d2eb848022a495b15ffd91ffe760bca5605f9062c305ee14226d9daf7faa58460c8f50',
'5af153e6fbf83b40cf98ed8bf5710321aa3234b2121cc3d8c47ef6854007f35ece319b056ddba7791b1db776b26a7ea7',
)
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)
})
it('should encrypt and decrypt with aes128/shake128', async () => {
const agent = new SSHAgentClient({ cipherAlgo: 'aes-128-cbc', digestAlgo: 'shake128' })
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
Expand All @@ -168,13 +217,13 @@ describe('SSHAgentClient cipher combination tests', () => {
const decrypted = await agent.decrypt(
identity,
SEED,
'9126f351eb84b1d9316b2808c69a09d379fa9f20d4f4a8d4e30135dbba262cbbfc3ad3774fcade60e6d1ae7c75af0a9c',
'9a9bdd5451fd1ed8220f4ce23a17aa0981d69521cadd73ff219fd85aa01bc8a3a44cbe95502854a5b37296e93a91db91',
)
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)
})
it('should encrypt with non matching aes192/sha512', async () => {
it('should encrypt and decrypt with non matching aes192/sha512', async () => {
const agent = new SSHAgentClient({ cipherAlgo: 'aes-192-cbc', digestAlgo: 'sha512' })
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
Expand All @@ -183,7 +232,7 @@ describe('SSHAgentClient cipher combination tests', () => {
const decrypted = await agent.decrypt(
identity,
SEED,
'280cd2993bb8f9d7ec14ef0c1d94d4ac8e128a39f3f9cba9f5730a7d99674057766e099c17a6786a4a6b33670d1b45b7',
'1e69398e2ad6ab5e1f748d538fafe6e54e9824b0646af6666437c556398a7495b48f76db5df52d52f9adde0a232465ab',
)
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)
})
Expand All @@ -192,7 +241,7 @@ describe('SSHAgentClient cipher combination tests', () => {
describe('SSHAgentClient encodings tests', () => {
it('should encrypt to base64', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
Expand All @@ -201,14 +250,14 @@ describe('SSHAgentClient encodings tests', () => {
})
it('should decrypt from base64', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
const decrypted = await agent.decrypt(
identity,
SEED,
'8epe+B3bWcSGTPpyW2MRqHeAKjTj2NVnR4q1YNVB5LfXw9JE02wsb3RqZBTFbMXc',
'QMf1r1/ZONTJImZyY1qO+ibDTqLCZNwD2dMs/tpfVpfLKovb7flyiU1Au/01xffv',
'base64',
)
chai.assert.strictEqual(decrypted.toString('utf8'), DECODED_STRING)
Expand Down
12 changes: 6 additions & 6 deletions test/ssh_agent_stream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const SEED = 'not_a_secret'
describe('SSHAgentClient streams tests', () => {
it('should encrypt', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
Expand All @@ -25,13 +25,13 @@ describe('SSHAgentClient streams tests', () => {
})
it('should decrypt', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
const transform = await agent.getDecryptTransform(identity, SEED, 'hex')
const stream = Readable.from(
'ecfd6bb57f4891ba7226886e90d2eb848022a495b15ffd91ffe760bca5605f9062c305ee14226d9daf7faa58460c8f50',
'2e306f1403b72eb17fe7187545dbd863be234ad1128ac3f34d60d102ced7bc43a7c4506d341463cff257b4d007b39143',
)
const decrypted = await text(stream.pipe(transform))
chai.assert.strictEqual(decrypted, DECODED_STRING)
Expand All @@ -41,7 +41,7 @@ describe('SSHAgentClient streams tests', () => {
describe('SSHAgentClient streams encodings tests', () => {
it('should encrypt to base64', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
Expand All @@ -52,12 +52,12 @@ describe('SSHAgentClient streams encodings tests', () => {
})
it('should decrypt from base64', async () => {
const agent = new SSHAgentClient()
const identity = await agent.getIdentity('key_rsa')
const identity = await agent.getIdentity('key_ed25519')
if (!identity) {
throw new Error()
}
const transform = await agent.getDecryptTransform(identity, SEED, 'base64')
const stream = Readable.from('8epe+B3bWcSGTPpyW2MRqHeAKjTj2NVnR4q1YNVB5LfXw9JE02wsb3RqZBTFbMXc')
const stream = Readable.from('5e0UbJX4+Rad+byLPnRNho3Qvjbeeqcwmg7yrXinHrTZ0788uuyvTl9jjbcpErF6')
const decrypted = await text(stream.pipe(transform))
chai.assert.strictEqual(decrypted, DECODED_STRING)
})
Expand Down
Loading