Skip to content

Commit 3c93bbd

Browse files
committed
use named import, options to select encodings, add streaming encryption
1 parent a351034 commit 3c93bbd

19 files changed

+376
-144
lines changed

.github/codecov.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
codecov:
2+
parsers:
3+
lcov:
4+
partials_as_hits: true

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
matrix:
1313
node-version: [18, 20, 22, 24, latest]
1414
steps:
15-
- uses: actions/checkout@v5
15+
- uses: actions/checkout@v6
1616
- uses: webfactory/ssh-agent@v0.10.0
1717
with:
1818
ssh-private-key: |
@@ -29,6 +29,6 @@ jobs:
2929
- run: node ./dist/src/cli.js --help
3030
- run: npm run coverage
3131
- name: Upload coverage reports to Codecov
32-
uses: codecov/codecov-action@v5
32+
uses: codecov/codecov-action@v6
3333
with:
3434
token: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/npm-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
id-token: write # Required for OIDC
1616
contents: read
1717
steps:
18-
- uses: actions/checkout@v5
18+
- uses: actions/checkout@v6
1919
- uses: webfactory/ssh-agent@v0.10.0
2020
with:
2121
ssh-private-key: |

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![CI](https://img.shields.io/github/actions/workflow/status/ddebin/ssh-agent-secrets/main.yml)](https://github.com/ddebin/ssh-agent-secrets/actions)
44
[![Codecov](https://img.shields.io/codecov/c/github/ddebin/ssh-agent-secrets)](https://app.codecov.io/gh/ddebin/ssh-agent-secrets)
5-
[![Release](https://img.shields.io/github/v/release/ddebin/ssh-agent-secrets)](https://github.com/ddebin/ssh-agent-secrets/releases)
5+
[![NPM](https://img.shields.io/npm/v/ssh-agent-secrets)](https://www.npmjs.com/package/ssh-agent-secrets)
66
[![License](https://img.shields.io/github/license/ddebin/ssh-agent-secrets)](./LICENSE)
77

88
> Encrypt and decrypt secrets using your SSH agent — no plaintext, no extra key management.
@@ -43,14 +43,19 @@ Usage: ssh-crypt [options] <command>
4343
Encrypt/Decrypt a file with your ssh-agent private key
4444
4545
Arguments:
46-
command action (choices: "encrypt", "decrypt")
46+
command action (choices: "encrypt", "decrypt")
4747
4848
Options:
49-
-i, --input <path> input path (default to stdin)
50-
-o, --output <path> output path (default to stdout)
51-
-k, --key <string> select the first matching pubkey in the ssh-agent
52-
-s, --seed <string> is used to generate the secret
53-
-h, --help display help for command
49+
-i, --input <path> input path (default to stdin)
50+
--encryptEncoding <encoding> encrypt output encoding (choices: "hex",
51+
"base64")
52+
-o, --output <path> output path (default to stdout)
53+
--decryptEncoding <encoding> decrypt input encoding (choices: "hex",
54+
"base64")
55+
-k, --key <string> select the first matching pubkey in the
56+
ssh-agent
57+
-s, --seed <string> is used to generate the secret
58+
-h, --help display help for command
5459
```
5560

5661
## 🛠️ Library installation
@@ -68,6 +73,7 @@ const agent = new SSHAgentClient()
6873

6974
const identities = await agent.getIdentities()
7075
console.log(identities)
76+
7177
const identity = await agent.getIdentity('AWS')
7278

7379
const encrypted = await agent.encrypt(

codecov.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
parsers:
2+
lcov:
3+
partials_as_hits: true
4+
javascript:
5+
enable_partials: true

eslint.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ export default defineConfig([
3333
'max-statements': 'off',
3434
'max-lines': 'off',
3535
'max-lines-per-function': 'off',
36+
'max-params': 'off',
3637
'sort-imports': [
3738
'error',
3839
{
3940
ignoreCase: true,
40-
ignoreDeclarationSort: true,
41+
ignoreMemberSort: false,
42+
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
43+
allowSeparatedGroups: false,
4144
},
4245
],
46+
'@typescript-eslint/no-explicit-any': 'off',
4347
'@typescript-eslint/restrict-template-expressions': [
4448
'error',
4549
{

package-lock.json

Lines changed: 32 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"name": "ssh-agent-secrets",
3-
"version": "0.2.1",
4-
"description": "Node library to encrypt and decrypt secrets using an SSH agent",
3+
"version": "0.3.0",
4+
"description": "Encrypt and decrypt secrets using an SSH agent",
55
"keywords": [
66
"ssh",
7-
"encryption",
8-
"decryption",
9-
"typescript",
107
"ssh-agent",
8+
"encryption",
9+
"ssh-key",
10+
"cryptography",
1111
"cli"
1212
],
1313
"homepage": "https://github.com/ddebin/ssh-agent-secrets",
@@ -21,8 +21,8 @@
2121
"license": "MIT",
2222
"author": "Damien Debin <damien.debin@gmail.com>",
2323
"type": "module",
24-
"main": "dist/src/lib/ssh_agent_client.js",
25-
"types": "dist/src/lib/ssh_agent_client.d.ts",
24+
"main": "dist/src/lib/index.js",
25+
"types": "dist/src/lib/index.d.ts",
2626
"bin": {
2727
"ssh-crypt": "dist/src/cli.js"
2828
},
@@ -32,7 +32,7 @@
3232
"scripts": {
3333
"build": "npm run clean && tsc -b",
3434
"clean": "del-cli ./dist",
35-
"coverage": "nyc --reporter=lcov --reporter=text npm test",
35+
"coverage": "nyc --reporter=lcovonly --reporter=text npm test",
3636
"eslint:check": "eslint .",
3737
"eslint:fix": "eslint . --fix",
3838
"fix": "npm run eslint:fix && npm run prettier:fix",
@@ -49,6 +49,8 @@
4949
},
5050
"devDependencies": {
5151
"@eslint/js": "^10",
52+
"@tsconfig/node-ts": "^23.6.4",
53+
"@tsconfig/node16": "^16.1.8",
5254
"@types/chai": "^5",
5355
"@types/chai-as-promised": "^8",
5456
"@types/mocha": "^10",
@@ -65,7 +67,7 @@
6567
"prettier-plugin-packagejson": "^3",
6668
"ts-node": "^10",
6769
"tsx": "^4",
68-
"typescript": "^5",
70+
"typescript": "^5.8",
6971
"typescript-eslint": "^8"
7072
},
7173
"engines": {

src/cli.ts

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,33 @@
11
#!/usr/bin/env node
22

3-
import * as fs from 'fs'
4-
import { Argument, program } from '@commander-js/extra-typings'
3+
import { Argument, Option, program } from '@commander-js/extra-typings'
4+
import { createReadStream, createWriteStream } from 'node:fs'
55
import { SSHAgentClient } from './lib/ssh_agent_client.js'
6-
import { buffer, text } from 'stream/consumers'
76

87
program
98
.name('ssh-crypt')
109
.description('Encrypt/Decrypt a file with your ssh-agent private key')
1110
.addArgument(new Argument('<command>', 'action').choices(['encrypt', 'decrypt']))
1211
.option('-i, --input <path>', 'input path (default to stdin)')
12+
.addOption(new Option('--encryptEncoding <encoding>', 'encrypt output encoding').choices(['hex', 'base64']))
1313
.option('-o, --output <path>', 'output path (default to stdout)')
14+
.addOption(new Option('--decryptEncoding <encoding>', 'decrypt input encoding').choices(['hex', 'base64']))
1415
.requiredOption('-k, --key <string>', 'select the first matching pubkey in the ssh-agent')
1516
.requiredOption('-s, --seed <string>', 'is used to generate the secret')
1617
.action(async (action, options) => {
1718
try {
18-
const agent = new SSHAgentClient()
19+
const agent = new SSHAgentClient({ timeout: 10000 })
1920
const key = await agent.getIdentity(options.key)
2021
if (!key) {
2122
program.error(`Error: no SSH key found for "${options.key}"`)
2223
}
23-
const readable = options.input ? fs.createReadStream(options.input) : process.stdin
24-
const writable = options.output ? fs.createWriteStream(options.output) : process.stdout
25-
switch (action) {
26-
case 'encrypt': {
27-
await buffer(readable)
28-
.then(data => agent.encrypt(key, options.seed, data))
29-
.then(encrypted => writable.write(encrypted))
30-
31-
break
32-
}
33-
case 'decrypt': {
34-
await text(readable)
35-
.then(data => agent.decrypt(key, options.seed, data.trim()))
36-
.then(decrypted => writable.write(decrypted))
37-
38-
break
39-
}
40-
default:
41-
throw new Error('unknwon action')
42-
}
24+
const readable = options.input ? createReadStream(options.input) : process.stdin
25+
const writable = options.output ? createWriteStream(options.output) : process.stdout
26+
const transform =
27+
action === 'decrypt'
28+
? await agent.getDecryptTransform(key, options.seed, options.decryptEncoding)
29+
: await agent.getEncryptTransform(key, options.seed, options.encryptEncoding)
30+
readable.pipe(transform).pipe(writable)
4331
} catch (err) {
4432
program.error(`Error: ${(err as Error).message}`)
4533
}

src/lib/decrypt_transform.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as crypto from 'node:crypto'
2+
import { Transform, type TransformCallback, type TransformOptions } from 'node:stream'
3+
4+
export class DecryptTransform extends Transform {
5+
private decipher?: crypto.Decipher
6+
private algo: string
7+
private cipherKey: crypto.KeyObject
8+
private cipherIvLength: number
9+
private inputEncoding?: BufferEncoding
10+
11+
constructor(
12+
algo: string,
13+
cipherKey: crypto.KeyObject,
14+
cipherIvLength: number,
15+
inputEncoding?: BufferEncoding,
16+
opts?: TransformOptions,
17+
) {
18+
super(opts)
19+
this.algo = algo
20+
this.cipherKey = cipherKey
21+
this.cipherIvLength = cipherIvLength
22+
this.inputEncoding = inputEncoding
23+
}
24+
25+
override _transform(chunk: any, _encoding: BufferEncoding, callback: TransformCallback) {
26+
let data = chunk as Buffer
27+
if (this.inputEncoding && this.inputEncoding !== 'binary') {
28+
data = Buffer.from(data.toString().trim(), this.inputEncoding)
29+
}
30+
if (!this.decipher) {
31+
// Unpackage the combined iv + encrypted message.
32+
// Since we are using a fixed size IV, we can hard code the slice length.
33+
const iv = data.subarray(0, this.cipherIvLength)
34+
this.decipher = crypto.createDecipheriv(this.algo, this.cipherKey, iv)
35+
data = data.subarray(this.cipherIvLength)
36+
}
37+
this.push(this.decipher.update(data))
38+
callback()
39+
}
40+
41+
override _flush(callback: TransformCallback) {
42+
this.push(this.decipher?.final())
43+
callback()
44+
}
45+
}

0 commit comments

Comments
 (0)