@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 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(password: string): Promise<void>;
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 password - Password to decrypt
86
+ * @param combinedKey - Combined master key and machine ID for decryption
84
87
  */
85
88
  private loadStorage;
86
89
  /**
@@ -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(password) {
112
+ async initialize() {
107
113
  try {
108
- // Store password for re-encryption
109
- this.currentPassword = password;
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(password);
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(password, salt);
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 password - Password to decrypt
217
+ * @param combinedKey - Combined master key and machine ID for decryption
203
218
  */
204
- async loadStorage(password) {
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 password and stored salt
223
+ // Derive key from combined key and stored salt
209
224
  const salt = Buffer.from(encryptedData.salt, 'base64');
210
- this.encryptionKey = await BrokerAuthEncryption.deriveKey(password, salt);
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.currentPassword) {
227
- throw new BrokerAuthError('Password not set', BrokerAuthErrorCode.NOT_INITIALIZED);
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(this.currentPassword, newSalt);
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.2.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
  }