ssh-picker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/paths.d.ts +17 -0
- package/dist/config/paths.js +47 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/db/connection.d.ts +4 -0
- package/dist/db/connection.js +17 -0
- package/dist/db/connection.js.map +1 -0
- package/dist/db/migrations.d.ts +4 -0
- package/dist/db/migrations.js +45 -0
- package/dist/db/migrations.js.map +1 -0
- package/dist/db/repositories/serverRepository.d.ts +13 -0
- package/dist/db/repositories/serverRepository.js +83 -0
- package/dist/db/repositories/serverRepository.js.map +1 -0
- package/dist/db/repositories/settingsRepository.d.ts +8 -0
- package/dist/db/repositories/settingsRepository.js +20 -0
- package/dist/db/repositories/settingsRepository.js.map +1 -0
- package/dist/import-export/archive.d.ts +3 -0
- package/dist/import-export/archive.js +38 -0
- package/dist/import-export/archive.js.map +1 -0
- package/dist/sftp/client.d.ts +21 -0
- package/dist/sftp/client.js +123 -0
- package/dist/sftp/client.js.map +1 -0
- package/dist/shared/credentials.d.ts +2 -0
- package/dist/shared/credentials.js +9 -0
- package/dist/shared/credentials.js.map +1 -0
- package/dist/shared/errors.d.ts +17 -0
- package/dist/shared/errors.js +36 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/types.d.ts +56 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/ssh/client.d.ts +6 -0
- package/dist/ssh/client.js +45 -0
- package/dist/ssh/client.js.map +1 -0
- package/dist/tui/App.d.ts +5 -0
- package/dist/tui/App.js +27 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/screens/Dashboard.d.ts +8 -0
- package/dist/tui/screens/Dashboard.js +42 -0
- package/dist/tui/screens/Dashboard.js.map +1 -0
- package/dist/tui/screens/FileManager.d.ts +7 -0
- package/dist/tui/screens/FileManager.js +120 -0
- package/dist/tui/screens/FileManager.js.map +1 -0
- package/dist/vault/crypto.d.ts +14 -0
- package/dist/vault/crypto.js +43 -0
- package/dist/vault/crypto.js.map +1 -0
- package/dist/vault/vault.d.ts +7 -0
- package/dist/vault/vault.js +67 -0
- package/dist/vault/vault.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export const KDF = 'scrypt';
|
|
3
|
+
export const CIPHER = 'aes-256-gcm';
|
|
4
|
+
export const KEY_LENGTH = 32;
|
|
5
|
+
export const SCRYPT_OPTIONS = { N: 2 ** 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
|
|
6
|
+
export function generateSalt() {
|
|
7
|
+
return randomBytes(16).toString('base64');
|
|
8
|
+
}
|
|
9
|
+
export function deriveKey(masterPassword, saltBase64) {
|
|
10
|
+
return scryptSync(masterPassword, Buffer.from(saltBase64, 'base64'), KEY_LENGTH, SCRYPT_OPTIONS);
|
|
11
|
+
}
|
|
12
|
+
export function encryptString(plaintext, key) {
|
|
13
|
+
const iv = randomBytes(12);
|
|
14
|
+
const cipher = createCipheriv(CIPHER, key, iv);
|
|
15
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
16
|
+
const value = {
|
|
17
|
+
version: 1,
|
|
18
|
+
algorithm: CIPHER,
|
|
19
|
+
iv: iv.toString('base64'),
|
|
20
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
21
|
+
ciphertext: ciphertext.toString('base64')
|
|
22
|
+
};
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
export function decryptString(serialized, key) {
|
|
26
|
+
const value = JSON.parse(serialized);
|
|
27
|
+
if (value.version !== 1 || value.algorithm !== CIPHER) {
|
|
28
|
+
throw new Error('Unsupported encrypted value format.');
|
|
29
|
+
}
|
|
30
|
+
const decipher = createDecipheriv(CIPHER, key, Buffer.from(value.iv, 'base64'));
|
|
31
|
+
decipher.setAuthTag(Buffer.from(value.tag, 'base64'));
|
|
32
|
+
const plaintext = Buffer.concat([
|
|
33
|
+
decipher.update(Buffer.from(value.ciphertext, 'base64')),
|
|
34
|
+
decipher.final()
|
|
35
|
+
]);
|
|
36
|
+
return plaintext.toString('utf8');
|
|
37
|
+
}
|
|
38
|
+
export function safeEquals(a, b) {
|
|
39
|
+
const left = Buffer.from(a);
|
|
40
|
+
const right = Buffer.from(b);
|
|
41
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/vault/crypto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Database } from '../db/connection.js';
|
|
2
|
+
import type { VaultContext } from '../shared/types.js';
|
|
3
|
+
export declare function vaultExists(dataDir?: string): boolean;
|
|
4
|
+
export declare function isVaultInitialized(db: Database): boolean;
|
|
5
|
+
export declare function initVault(masterPassword: string, dataDir?: string): VaultContext;
|
|
6
|
+
export declare function unlockVault(masterPassword: string, dataDir?: string): VaultContext;
|
|
7
|
+
export declare function openVaultDatabase(vault: VaultContext): Database;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { ensureDataDir, resolveDataDir, resolveDbPath } from '../config/paths.js';
|
|
3
|
+
import { openMigratedDatabase } from '../db/connection.js';
|
|
4
|
+
import { SettingsRepository } from '../db/repositories/settingsRepository.js';
|
|
5
|
+
import { InvalidMasterPasswordError, MissingVaultError, VaultExistsError } from '../shared/errors.js';
|
|
6
|
+
import { decryptString, deriveKey, encryptString, generateSalt, KDF } from './crypto.js';
|
|
7
|
+
const VAULT_VERSION = '1';
|
|
8
|
+
const VERIFIER_TEXT = 'sshp-vault-verifier';
|
|
9
|
+
export function vaultExists(dataDir = resolveDataDir()) {
|
|
10
|
+
return existsSync(resolveDbPath(dataDir));
|
|
11
|
+
}
|
|
12
|
+
export function isVaultInitialized(db) {
|
|
13
|
+
const settings = new SettingsRepository(db);
|
|
14
|
+
return settings.get('vault.version') === VAULT_VERSION;
|
|
15
|
+
}
|
|
16
|
+
export function initVault(masterPassword, dataDir = resolveDataDir()) {
|
|
17
|
+
ensureDataDir(dataDir);
|
|
18
|
+
const dbPath = resolveDbPath(dataDir);
|
|
19
|
+
const db = openMigratedDatabase(dbPath);
|
|
20
|
+
try {
|
|
21
|
+
if (isVaultInitialized(db))
|
|
22
|
+
throw new VaultExistsError(dataDir);
|
|
23
|
+
const salt = generateSalt();
|
|
24
|
+
const key = deriveKey(masterPassword, salt);
|
|
25
|
+
const settings = new SettingsRepository(db);
|
|
26
|
+
settings.set('vault.version', VAULT_VERSION);
|
|
27
|
+
settings.set('vault.kdf', KDF);
|
|
28
|
+
settings.set('vault.kdfSalt', salt);
|
|
29
|
+
settings.set('vault.verifier', encryptString(VERIFIER_TEXT, key));
|
|
30
|
+
return { dataDir, dbPath, key };
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
db.close();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function unlockVault(masterPassword, dataDir = resolveDataDir()) {
|
|
37
|
+
const dbPath = resolveDbPath(dataDir);
|
|
38
|
+
if (!existsSync(dbPath))
|
|
39
|
+
throw new MissingVaultError(dataDir);
|
|
40
|
+
const db = openMigratedDatabase(dbPath);
|
|
41
|
+
try {
|
|
42
|
+
const settings = new SettingsRepository(db);
|
|
43
|
+
if (!isVaultInitialized(db))
|
|
44
|
+
throw new MissingVaultError(dataDir);
|
|
45
|
+
const salt = settings.get('vault.kdfSalt');
|
|
46
|
+
const verifier = settings.get('vault.verifier');
|
|
47
|
+
if (!salt || !verifier)
|
|
48
|
+
throw new MissingVaultError(dataDir);
|
|
49
|
+
const key = deriveKey(masterPassword, salt);
|
|
50
|
+
try {
|
|
51
|
+
const plaintext = decryptString(verifier, key);
|
|
52
|
+
if (plaintext !== VERIFIER_TEXT)
|
|
53
|
+
throw new InvalidMasterPasswordError();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new InvalidMasterPasswordError();
|
|
57
|
+
}
|
|
58
|
+
return { dataDir, dbPath, key };
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
db.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function openVaultDatabase(vault) {
|
|
65
|
+
return openMigratedDatabase(vault.dbPath);
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=vault.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vault.js","sourceRoot":"","sources":["../../src/vault/vault.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ssh-picker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SSHP - a portable encrypted SSH/SFTP picker for the terminal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/adittanu/ssh-picker.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/adittanu/ssh-picker/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/adittanu/ssh-picker#readme",
|
|
15
|
+
"bin": {
|
|
16
|
+
"sshp": "dist/cli/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"dev": "tsx src/cli/index.tsx",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
|
+
"prepack": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=24.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@inquirer/prompts": "^7.10.1",
|
|
34
|
+
"commander": "^14.0.2",
|
|
35
|
+
"ink": "^6.5.1",
|
|
36
|
+
"react": "^19.2.3",
|
|
37
|
+
"ssh2": "^1.17.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^24.10.1",
|
|
41
|
+
"@types/react": "^19.2.7",
|
|
42
|
+
"@types/ssh2": "^1.15.5",
|
|
43
|
+
"tsx": "^4.21.0",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vitest": "^4.0.15"
|
|
46
|
+
}
|
|
47
|
+
}
|