Skip to content
This repository was archived by the owner on Jan 19, 2026. It is now read-only.

Commit afc2610

Browse files
committed
chor: cleanup; added: setup, autostart, license
1 parent de3a8ce commit afc2610

31 files changed

Lines changed: 1378 additions & 132 deletions

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
*wip*
44

5+
A wrapper arround the Bitwarden CLI to provide a SSH Key Agent IPC interface.
6+
57
## Installation
68

79
```bash
@@ -22,6 +24,43 @@ export SSH_AUTH_SOCK=~/.ssh/s-bit-agent.sock
2224
```bash
2325
s-bit-agent --help
2426
s-bit-agent -- bw --help
25-
s-bit-agent -- bwa --help # the wrapper will take care about the session creation
27+
s-bit-agent -- bwa --help
2628
s-bit-agent status
2729
```
30+
31+
## Differences to `bw` and `bwa`
32+
33+
```bash
34+
user@example:~$ s-bit-agent -- bw status
35+
{..., "status": "locked"}
36+
37+
user@example:~$ s-bit-agent -- bwa status
38+
Requesting session
39+
Connected to server
40+
Sent S_BIT_AGENT_REQUEST_SESSION
41+
Received session
42+
{..., "status": "unlocked"}
43+
```
44+
45+
46+
## TODO
47+
- [X] Add basic IPC communication to talk accordingly to [draft-miller-ssh-agent](https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent)
48+
- [X] Add caching for the session
49+
- [X] add a `key add` command
50+
- [X] Add a `key import` command
51+
- [X] Add a `status` command
52+
- [X] Implement S_BIT_AGENT_REQUEST_SESSION into IPC
53+
- [X] Add a `bw` and `bwa` command
54+
- [X] Add a `setup` command to automatically install the daemon in the autostart
55+
- [ ] Look into the secure heap implementation possibilitys
56+
- [ ] Add a `lock` command
57+
- [ ] Add setting to disable approval requests, or at least to set a timeout
58+
- [ ] Fix issue that the agent sometimes stops responding
59+
- [ ] Add a `key list` command
60+
- [ ] Add a `key delete` command
61+
- [ ] Add a `key rename` command
62+
- [ ] Add a `key export` command
63+
- [ ] Expand the S_BIT_AGENT_REQUEST_SESSION to also handle some other requests
64+
- [ ] Add tests
65+
- [ ] [Maybe™] Develop a Tauri frontend/client, which internally uses the `s-bit-agent` to communicate with the server.
66+
- [ ] [Maybe™] Add capability to unlock the agent through bitwarden remote approval

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,22 @@
3030
"dev": "nest build && REPL=true node dist/s-bit-agent.js",
3131
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
3232
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
33-
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\""
33+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
34+
"license:hash": "node -e \"const f=require('fs'),c=require('crypto'),e=f.readFileSync('./LICENSE'),h=c.createHash('sha256').update(e).digest('hex');console.log(h);\""
3435
},
3536
"dependencies": {
3637
"@bitwarden/cli": "^2024.2.0",
3738
"@nestjs/common": "^10.0.0",
3839
"@nestjs/core": "^10.0.0",
3940
"@nestjs/platform-express": "^10.0.0",
41+
"@types/plist": "^3.0.5",
4042
"colors": "^1.4.0",
4143
"express": "^4.18.2",
4244
"inquirer": "^8.2.6",
4345
"nest-commander": "^3.12.5",
4446
"node-ipc": "^10.1.0",
4547
"open": "^9.1.0",
48+
"plist": "^3.1.0",
4649
"reflect-metadata": "^0.2.0",
4750
"rxjs": "^7.8.1",
4851
"sshpk": "^1.18.0"

pnpm-lock.yaml

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

src/app.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ import { BitwardenModule } from './bitwarden/bitwarden.module';
44
import { CliModule } from './cli/cli.module';
55
import { SharedModule } from './shared/shared.module';
66
import { GuiModule } from './gui/gui.module';
7+
import { AutostartModule } from './autostart/autostart.module';
78

89
@Module({
9-
imports: [SharedModule, KeyModule, BitwardenModule, GuiModule, CliModule],
10+
imports: [
11+
SharedModule,
12+
KeyModule,
13+
BitwardenModule,
14+
GuiModule,
15+
CliModule,
16+
AutostartModule,
17+
],
1018
})
1119
export class AppModule {}
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export interface AutostartService {
22
name: string; // Human-readable name for user display
3-
canActivate(): boolean;
4-
install(command: string): Promise<boolean>;
5-
uninstall(): Promise<boolean>;
3+
canActivate(): Promise<boolean>;
4+
install(command: string, name: string, marker: string): Promise<boolean>;
5+
uninstall(marker: string): Promise<boolean>;
6+
isInstalled(marker: string): Promise<boolean>;
67
}

src/autostart/autostart.module.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Global, Module } from '@nestjs/common';
2+
import { AutostartService } from './autostart.service';
3+
import { CronAutostartService } from './cron.service';
4+
import { SystemdAutostartService } from './systemd.service';
5+
import { XinitrcAutostartService } from './xinitrc.service';
6+
import { LaunchAgentAutostartService } from './macos.service';
7+
8+
@Global()
9+
@Module({
10+
providers: [
11+
AutostartService,
12+
CronAutostartService,
13+
SystemdAutostartService,
14+
XinitrcAutostartService,
15+
LaunchAgentAutostartService,
16+
],
17+
exports: [AutostartService],
18+
})
19+
export class AutostartModule {}

src/autostart/autostart.service.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { AutostartService as AutostartServiceInterface } from './autostart.interface';
3+
import { CronAutostartService } from './cron.service';
4+
import { SystemdAutostartService } from './systemd.service';
5+
import { XinitrcAutostartService } from './xinitrc.service';
6+
import { LaunchAgentAutostartService } from './macos.service';
7+
8+
@Injectable()
9+
export class AutostartService {
10+
constructor(
11+
private readonly cronService: CronAutostartService,
12+
private readonly systemdService: SystemdAutostartService,
13+
private readonly xinitrcService: XinitrcAutostartService,
14+
private readonly launchAgentService: LaunchAgentAutostartService,
15+
) {}
16+
17+
async getAvailableServices(): Promise<AutostartServiceInterface[]> {
18+
const services = [
19+
this.cronService,
20+
this.systemdService,
21+
this.xinitrcService,
22+
this.launchAgentService,
23+
];
24+
const res = await Promise.all(
25+
services.map((service) => service.canActivate()),
26+
);
27+
return services.filter((_, i) => res[i]);
28+
}
29+
30+
async isInstalled(
31+
services: AutostartServiceInterface[],
32+
): Promise<AutostartServiceInterface | null> {
33+
for (const service of services)
34+
if (await service.isInstalled('s-bit-agent')) {
35+
return service;
36+
}
37+
return null;
38+
}
39+
40+
install(
41+
command: string,
42+
service: AutostartServiceInterface,
43+
): Promise<boolean> {
44+
return service.install(command, 's.BitAgent', 's-bit-agent');
45+
}
46+
47+
uninstall(service: AutostartServiceInterface): Promise<boolean> {
48+
return service.uninstall('s-bit-agent');
49+
}
50+
}

src/autostart/cron.service.ts

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,67 @@
11
import { Injectable } from '@nestjs/common';
2-
import * as child_process from 'child_process';
32
import { AutostartService } from './autostart.interface';
3+
import * as child_process from 'child_process';
44

55
@Injectable()
66
export class CronAutostartService implements AutostartService {
7-
name = 'Cron';
8-
readonly COMMAND_MARKER = 's-bit-agent';
7+
name = 'Cron Autostart';
8+
9+
async canActivate(): Promise<boolean> {
10+
return child_process.spawnSync('which', ['crontab']).status === 0;
11+
}
912

10-
canActivate(): boolean {
13+
async install(
14+
command: string,
15+
name: string,
16+
marker: string,
17+
): Promise<boolean> {
1118
try {
12-
child_process.execSync('crontab -l');
19+
// Get existing crontab
20+
const currentCrontab = child_process.execSync('crontab -l').toString();
21+
22+
// Check if entry already exists (avoid duplicates)
23+
if (currentCrontab.includes(marker)) {
24+
console.warn('Autostart entry already exists in crontab');
25+
return false;
26+
}
27+
28+
// Append the new entry with the marker
29+
const newCrontab =
30+
currentCrontab + `@reboot ${command} > /dev/null 2>&1 # ${marker}\n`;
31+
child_process.execSync(`echo "${newCrontab}" | crontab -`);
32+
1333
return true;
1434
} catch (error) {
35+
console.error('Error installing cron autostart:', error);
1536
return false;
1637
}
1738
}
1839

19-
async install(command: string): Promise<boolean> {
40+
async uninstall(marker: string): Promise<boolean> {
2041
try {
21-
const modifiedCommand = `${command} # ${this.COMMAND_MARKER}`;
22-
child_process.execSync(
23-
`(crontab -l; echo "@reboot ${modifiedCommand}") | crontab -`,
24-
);
42+
const currentCrontab = child_process.execSync('crontab -l').toString();
43+
44+
// Remove lines containing the marker
45+
const newCrontab = currentCrontab
46+
.split('\n')
47+
.filter((line) => !line.includes(marker))
48+
.join('\n');
49+
50+
child_process.execSync(`echo "${newCrontab}" | crontab -`);
51+
2552
return true;
2653
} catch (error) {
27-
console.error('Cron install error:', error);
54+
console.error('Error uninstalling cron autostart:', error);
2855
return false;
2956
}
3057
}
3158

32-
async uninstall(): Promise<boolean> {
59+
async isInstalled(marker: string): Promise<boolean> {
3360
try {
34-
let crontabContents = child_process.execSync('crontab -l').toString();
35-
crontabContents = crontabContents
36-
.split('\n')
37-
.filter((line) => !line.includes(this.COMMAND_MARKER))
38-
.join('\n');
39-
child_process.execSync(`echo "${crontabContents}" | crontab -`);
40-
return true;
61+
const currentCrontab = child_process.execSync('crontab -l').toString();
62+
return currentCrontab.includes(marker);
4163
} catch (error) {
42-
console.error('Cron uninstall error:', error);
64+
console.error('Error checking if cron autostart is installed:', error);
4365
return false;
4466
}
4567
}

0 commit comments

Comments
 (0)