@dishantlangayan/sc-cli-core 0.2.0 → 0.3.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 +3 -0
- package/lib/auth/auth-manager.d.ts +9 -6
- package/lib/auth/auth-manager.js +34 -18
- package/lib/auth/keychain.d.ts +47 -0
- package/lib/auth/keychain.js +71 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -20,6 +20,9 @@ The ScCommand abstract class extends [@oclif/core's Command class](https://githu
|
|
|
20
20
|
## ScConnection Class
|
|
21
21
|
The ScConnection class provide abstraction functions for Solace Cloud API REST calls. It handles the access token and base URL for each REST call, avoiding the need to set these on each Command.
|
|
22
22
|
|
|
23
|
+
## BrokerAuthManager
|
|
24
|
+
The BrokerAuthManager class provides utility functions to store and retrieve broker SEMP management authentication information from user's home directory: `~/.sf/` or `%USERPROFILE%\sf\`. The implementation uses AES-256-GCM for authenticated encryption and provides machine-bound encryption that combines OS-level security (keychain) with machine-specific identifiers, making credentials non-transferable between machines.
|
|
25
|
+
|
|
23
26
|
# Contributing
|
|
24
27
|
Contributions are encouraged! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
|
|
25
28
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ScConnection } from '../util/sc-connection.js';
|
|
2
2
|
import { type BrokerAuth } from './auth-types.js';
|
|
3
|
+
import { KeychainService } from './keychain.js';
|
|
3
4
|
/**
|
|
4
5
|
* Manager for broker authentication storage
|
|
5
6
|
* Handles encrypted storage of broker credentials
|
|
@@ -8,14 +9,17 @@ export declare class BrokerAuthManager {
|
|
|
8
9
|
private static instance;
|
|
9
10
|
private readonly configDir;
|
|
10
11
|
private readonly configFile;
|
|
11
|
-
private currentPassword;
|
|
12
12
|
private encryptionKey;
|
|
13
|
+
private readonly keychainService;
|
|
14
|
+
private machineId;
|
|
15
|
+
private masterKey;
|
|
13
16
|
private storage;
|
|
14
17
|
private constructor();
|
|
15
18
|
/**
|
|
16
19
|
* Get singleton instance
|
|
20
|
+
* @param keychainService - Optional keychain service for testing
|
|
17
21
|
*/
|
|
18
|
-
static getInstance(): BrokerAuthManager;
|
|
22
|
+
static getInstance(keychainService?: KeychainService): BrokerAuthManager;
|
|
19
23
|
/**
|
|
20
24
|
* Add a new broker configuration
|
|
21
25
|
* @param broker - Broker authentication configuration
|
|
@@ -50,10 +54,9 @@ export declare class BrokerAuthManager {
|
|
|
50
54
|
*/
|
|
51
55
|
getBroker(name: string): Promise<BrokerAuth | null>;
|
|
52
56
|
/**
|
|
53
|
-
* Initialize the auth manager with encryption key
|
|
54
|
-
* @param password - Password to derive encryption key
|
|
57
|
+
* Initialize the auth manager with encryption key derived from OS keychain and machine ID
|
|
55
58
|
*/
|
|
56
|
-
initialize(
|
|
59
|
+
initialize(): Promise<void>;
|
|
57
60
|
/**
|
|
58
61
|
* List all broker names
|
|
59
62
|
* @returns Array of broker names
|
|
@@ -80,7 +83,7 @@ export declare class BrokerAuthManager {
|
|
|
80
83
|
private fileExists;
|
|
81
84
|
/**
|
|
82
85
|
* Load storage from encrypted file
|
|
83
|
-
* @param
|
|
86
|
+
* @param combinedKey - Combined master key and machine ID for decryption
|
|
84
87
|
*/
|
|
85
88
|
private loadStorage;
|
|
86
89
|
/**
|
package/lib/auth/auth-manager.js
CHANGED
|
@@ -4,6 +4,9 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { ScConnection } from '../util/sc-connection.js';
|
|
5
5
|
import { BrokerAuthEncryption } from './auth-encryption.js';
|
|
6
6
|
import { AuthType, BrokerAuthError, BrokerAuthErrorCode, } from './auth-types.js';
|
|
7
|
+
import { KeychainService } from './keychain.js';
|
|
8
|
+
const SERVICE_NAME = 'local';
|
|
9
|
+
const KEY_NAME = 'sc-cli';
|
|
7
10
|
/**
|
|
8
11
|
* Manager for broker authentication storage
|
|
9
12
|
* Handles encrypted storage of broker credentials
|
|
@@ -12,20 +15,24 @@ export class BrokerAuthManager {
|
|
|
12
15
|
static instance = null;
|
|
13
16
|
configDir;
|
|
14
17
|
configFile;
|
|
15
|
-
currentPassword = null;
|
|
16
18
|
encryptionKey = null;
|
|
19
|
+
keychainService;
|
|
20
|
+
machineId = null;
|
|
21
|
+
masterKey = null;
|
|
17
22
|
storage = null;
|
|
18
|
-
constructor() {
|
|
23
|
+
constructor(keychainService) {
|
|
19
24
|
const homeDirectory = homedir();
|
|
20
25
|
this.configDir = join(homeDirectory, '.sc');
|
|
21
26
|
this.configFile = join(this.configDir, 'brokers.json');
|
|
27
|
+
this.keychainService = keychainService ?? new KeychainService();
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
30
|
* Get singleton instance
|
|
31
|
+
* @param keychainService - Optional keychain service for testing
|
|
25
32
|
*/
|
|
26
|
-
static getInstance() {
|
|
33
|
+
static getInstance(keychainService) {
|
|
27
34
|
if (!BrokerAuthManager.instance) {
|
|
28
|
-
BrokerAuthManager.instance = new BrokerAuthManager();
|
|
35
|
+
BrokerAuthManager.instance = new BrokerAuthManager(keychainService);
|
|
29
36
|
}
|
|
30
37
|
return BrokerAuthManager.instance;
|
|
31
38
|
}
|
|
@@ -100,23 +107,31 @@ export class BrokerAuthManager {
|
|
|
100
107
|
return broker ?? null;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
|
-
* Initialize the auth manager with encryption key
|
|
104
|
-
* @param password - Password to derive encryption key
|
|
110
|
+
* Initialize the auth manager with encryption key derived from OS keychain and machine ID
|
|
105
111
|
*/
|
|
106
|
-
async initialize(
|
|
112
|
+
async initialize() {
|
|
107
113
|
try {
|
|
108
|
-
//
|
|
109
|
-
this.
|
|
114
|
+
// Get machine ID
|
|
115
|
+
this.machineId = this.keychainService.getMachineId();
|
|
116
|
+
// Get or create master key from OS keychain
|
|
117
|
+
this.masterKey = await this.keychainService.getPassword(KEY_NAME, SERVICE_NAME);
|
|
118
|
+
if (!this.masterKey) {
|
|
119
|
+
// Generate new master key and store in OS keychain
|
|
120
|
+
this.masterKey = this.keychainService.generateMasterKey();
|
|
121
|
+
await this.keychainService.setPassword(KEY_NAME, SERVICE_NAME, this.masterKey);
|
|
122
|
+
}
|
|
123
|
+
// Combine master key with machine ID for encryption
|
|
124
|
+
const combinedKey = `${this.masterKey}:${this.machineId}`;
|
|
110
125
|
// Try to load existing storage
|
|
111
126
|
const fileExists = await this.fileExists();
|
|
112
127
|
if (fileExists) {
|
|
113
128
|
// Load existing file and derive key from stored salt
|
|
114
|
-
await this.loadStorage(
|
|
129
|
+
await this.loadStorage(combinedKey);
|
|
115
130
|
}
|
|
116
131
|
else {
|
|
117
132
|
// Create new storage with new salt
|
|
118
133
|
const salt = BrokerAuthEncryption.generateSalt();
|
|
119
|
-
this.encryptionKey = await BrokerAuthEncryption.deriveKey(
|
|
134
|
+
this.encryptionKey = await BrokerAuthEncryption.deriveKey(combinedKey, salt);
|
|
120
135
|
this.storage = {
|
|
121
136
|
brokers: [],
|
|
122
137
|
version: '1.0.0',
|
|
@@ -199,15 +214,15 @@ export class BrokerAuthManager {
|
|
|
199
214
|
}
|
|
200
215
|
/**
|
|
201
216
|
* Load storage from encrypted file
|
|
202
|
-
* @param
|
|
217
|
+
* @param combinedKey - Combined master key and machine ID for decryption
|
|
203
218
|
*/
|
|
204
|
-
async loadStorage(
|
|
219
|
+
async loadStorage(combinedKey) {
|
|
205
220
|
try {
|
|
206
221
|
const fileContent = await readFile(this.configFile, 'utf8');
|
|
207
222
|
const encryptedData = JSON.parse(fileContent);
|
|
208
|
-
// Derive key from
|
|
223
|
+
// Derive key from combined key and stored salt
|
|
209
224
|
const salt = Buffer.from(encryptedData.salt, 'base64');
|
|
210
|
-
this.encryptionKey = await BrokerAuthEncryption.deriveKey(
|
|
225
|
+
this.encryptionKey = await BrokerAuthEncryption.deriveKey(combinedKey, salt);
|
|
211
226
|
// Decrypt storage
|
|
212
227
|
this.storage = await BrokerAuthEncryption.decrypt(encryptedData, this.encryptionKey);
|
|
213
228
|
}
|
|
@@ -223,16 +238,17 @@ export class BrokerAuthManager {
|
|
|
223
238
|
*/
|
|
224
239
|
async saveStorage() {
|
|
225
240
|
try {
|
|
226
|
-
if (!this.
|
|
227
|
-
throw new BrokerAuthError('
|
|
241
|
+
if (!this.masterKey || !this.machineId) {
|
|
242
|
+
throw new BrokerAuthError('Auth manager not initialized', BrokerAuthErrorCode.NOT_INITIALIZED);
|
|
228
243
|
}
|
|
229
244
|
// Ensure directory exists
|
|
230
245
|
await mkdir(this.configDir, { mode: 0o700, recursive: true });
|
|
231
246
|
// Encrypt data
|
|
232
247
|
const encrypted = await BrokerAuthEncryption.encrypt(this.storage, this.encryptionKey);
|
|
233
248
|
// Re-derive key with new salt for next save
|
|
249
|
+
const combinedKey = `${this.masterKey}:${this.machineId}`;
|
|
234
250
|
const newSalt = Buffer.from(encrypted.salt, 'base64');
|
|
235
|
-
this.encryptionKey = await BrokerAuthEncryption.deriveKey(
|
|
251
|
+
this.encryptionKey = await BrokerAuthEncryption.deriveKey(combinedKey, newSalt);
|
|
236
252
|
// Write to temp file first (atomic write)
|
|
237
253
|
const jsonData = JSON.stringify(encrypted, null, 2);
|
|
238
254
|
const tempFile = `${this.configFile}.tmp`;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keychain service for storing and retrieving encryption keys
|
|
3
|
+
* This abstraction allows for easier testing and mocking
|
|
4
|
+
*/
|
|
5
|
+
export declare class KeychainService {
|
|
6
|
+
/**
|
|
7
|
+
* Delete a password from the keychain
|
|
8
|
+
* @param service - Service name
|
|
9
|
+
* @param account - Account name
|
|
10
|
+
* @returns True if deleted, false if not found
|
|
11
|
+
*/
|
|
12
|
+
deletePassword(service: string, account: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Generate a random master key
|
|
15
|
+
* @returns Base64-encoded random key
|
|
16
|
+
*/
|
|
17
|
+
generateMasterKey(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Get unique machine identifier
|
|
20
|
+
* @returns Machine ID string
|
|
21
|
+
*/
|
|
22
|
+
getMachineId(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Get a password from the keychain
|
|
25
|
+
* @param service - Service name
|
|
26
|
+
* @param account - Account name
|
|
27
|
+
* @returns Password string or null if not found
|
|
28
|
+
*/
|
|
29
|
+
getPassword(service: string, account: string): Promise<null | string>;
|
|
30
|
+
/**
|
|
31
|
+
* Set a password in the keychain
|
|
32
|
+
* @param service - Service name
|
|
33
|
+
* @param account - Account name
|
|
34
|
+
* @param password - Password to store
|
|
35
|
+
*/
|
|
36
|
+
setPassword(service: string, account: string, password: string): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Lazy load keytar module
|
|
39
|
+
* @returns keytar module
|
|
40
|
+
*/
|
|
41
|
+
private loadKeytar;
|
|
42
|
+
/**
|
|
43
|
+
* Lazy load node-machine-id module
|
|
44
|
+
* @returns node-machine-id module
|
|
45
|
+
*/
|
|
46
|
+
private loadMachineId;
|
|
47
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
/**
|
|
5
|
+
* Keychain service for storing and retrieving encryption keys
|
|
6
|
+
* This abstraction allows for easier testing and mocking
|
|
7
|
+
*/
|
|
8
|
+
export class KeychainService {
|
|
9
|
+
/**
|
|
10
|
+
* Delete a password from the keychain
|
|
11
|
+
* @param service - Service name
|
|
12
|
+
* @param account - Account name
|
|
13
|
+
* @returns True if deleted, false if not found
|
|
14
|
+
*/
|
|
15
|
+
async deletePassword(service, account) {
|
|
16
|
+
const keytar = await this.loadKeytar();
|
|
17
|
+
return keytar.deletePassword(service, account);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Generate a random master key
|
|
21
|
+
* @returns Base64-encoded random key
|
|
22
|
+
*/
|
|
23
|
+
generateMasterKey() {
|
|
24
|
+
return randomBytes(32).toString('base64');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get unique machine identifier
|
|
28
|
+
* @returns Machine ID string
|
|
29
|
+
*/
|
|
30
|
+
getMachineId() {
|
|
31
|
+
const machineIdPkg = this.loadMachineId();
|
|
32
|
+
return machineIdPkg.machineIdSync();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get a password from the keychain
|
|
36
|
+
* @param service - Service name
|
|
37
|
+
* @param account - Account name
|
|
38
|
+
* @returns Password string or null if not found
|
|
39
|
+
*/
|
|
40
|
+
async getPassword(service, account) {
|
|
41
|
+
const keytar = await this.loadKeytar();
|
|
42
|
+
return keytar.getPassword(service, account);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Set a password in the keychain
|
|
46
|
+
* @param service - Service name
|
|
47
|
+
* @param account - Account name
|
|
48
|
+
* @param password - Password to store
|
|
49
|
+
*/
|
|
50
|
+
async setPassword(service, account, password) {
|
|
51
|
+
const keytar = await this.loadKeytar();
|
|
52
|
+
return keytar.setPassword(service, account, password);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Lazy load keytar module
|
|
56
|
+
* @returns keytar module
|
|
57
|
+
*/
|
|
58
|
+
async loadKeytar() {
|
|
59
|
+
const keytar = await import('keytar');
|
|
60
|
+
return keytar.default;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Lazy load node-machine-id module
|
|
64
|
+
* @returns node-machine-id module
|
|
65
|
+
*/
|
|
66
|
+
loadMachineId() {
|
|
67
|
+
// Use require for CommonJS module (node-machine-id)
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
69
|
+
return require('node-machine-id');
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dishantlangayan/sc-cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Base library for the Solace Cloud CLI and plugins",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dishant Langayan",
|
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@oclif/core": "^4.8.0",
|
|
56
56
|
"axios": "^1.13.2",
|
|
57
|
+
"keytar": "^7.9.0",
|
|
58
|
+
"node-machine-id": "^1.1.12",
|
|
57
59
|
"table": "^6.9.0"
|
|
58
60
|
}
|
|
59
61
|
}
|