Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LICENSE-THIRD-PARTY.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5235,6 +5235,7 @@ The following npm packages may be included in this product:
- csv-stringify@6.6.0
- degenerator@5.0.1
- isarray@1.0.0
- keychain@1.5.0
- netmask@2.0.2
- tr46@0.0.3
- undici-types@5.26.5
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"fs-extra": "^10.1.0",
"inquirer": "^8.2.7",
"js-yaml": "^4.1.1",
"keychain": "^1.5.0",
"keytar": "^7.9.0",
"lodash": "^4.17.13",
"mkdirp": "^3.0.1",
Expand Down
118 changes: 84 additions & 34 deletions src/box-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,7 @@ const pkg = require('../package.json');
const inquirer = require('./inquirer');
const { stringifyStream } = require('@discoveryjs/json-ext');
const progress = require('cli-progress');
let keytar = null;
try {
keytar = require('keytar');
} catch {
// keytar cannot be imported because the library is not provided for this operating system / architecture
}
const secureStorage = require('./secure-storage');

const DEBUG = require('./debug');
const stream = require('node:stream');
Expand Down Expand Up @@ -96,9 +91,29 @@ const ENVIRONMENTS_FILE_PATH = path.join(
CONFIG_FOLDER_PATH,
'box_environments.json'
);
const ENVIRONMENTS_KEYCHAIN_SERVICE = 'boxcli';
const ENVIRONMENTS_KEYCHAIN_ACCOUNT = 'Box';

const DEFAULT_ANALYTICS_CLIENT_NAME = 'box-cli';

/**
* Convert error objects to a stable debug-safe shape.
*
* @param {unknown} error A caught error object
* @returns {Object} A reduced object for DEBUG logging
*/
function getDebugErrorDetails(error) {
if (!error || typeof error !== 'object') {
return { message: String(error) };
}
return {
name: error.name || 'Error',
code: error.code,
message: error.message || String(error),
stack: error.stack,
};
}

/**
* Parse a string value from CSV into the correct boolean value
* @param {string|boolean} value The value to parse
Expand Down Expand Up @@ -300,8 +315,7 @@ class BoxCommand extends Command {
this.disableRequiredArgsAndFlags();
}

this.supportsSecureStorage =
keytar && ['darwin', 'win32', 'linux'].includes(process.platform);
this.supportsSecureStorage = secureStorage.available;

let { flags, args } = await this.parse(this.constructor);

Expand Down Expand Up @@ -1809,34 +1823,62 @@ class BoxCommand extends Command {
* @returns {Object} The parsed environment information
*/
async getEnvironments() {
// Try secure storage first on supported platforms
if (this.supportsSecureStorage) {
DEBUG.init(
'Attempting secure storage read via %s service="%s" account="%s"',
secureStorage.backend,
ENVIRONMENTS_KEYCHAIN_SERVICE,
ENVIRONMENTS_KEYCHAIN_ACCOUNT
);
try {
const password = await keytar.getPassword(
'boxcli' /* service */,
'Box' /* account */
const password = await secureStorage.getPassword(
ENVIRONMENTS_KEYCHAIN_SERVICE,
ENVIRONMENTS_KEYCHAIN_ACCOUNT
);
if (password) {
DEBUG.init(
'Successfully loaded environments from secure storage (%s)',
secureStorage.backend
);
return JSON.parse(password);
}
DEBUG.init(
'Secure storage returned empty result for service="%s" account="%s"',
ENVIRONMENTS_KEYCHAIN_SERVICE,
ENVIRONMENTS_KEYCHAIN_ACCOUNT
);
} catch (error) {
DEBUG.init(
'Failed to read from secure storage, falling back to file: %s',
error.message
'Failed to read from secure storage (%s), falling back to file: %O',
secureStorage.backend,
getDebugErrorDetails(error)
);
// fallback to env file
}
} else {
DEBUG.init(
'Skipping secure storage read: platform=%s available=%s',
process.platform,
secureStorage.available
);
}

// Try to read from file (fallback or no secure storage)
try {
if (fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
DEBUG.init(
'Attempting environments fallback file read at %s',
ENVIRONMENTS_FILE_PATH
);
return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
}
DEBUG.init(
'Environments fallback file does not exist at %s',
ENVIRONMENTS_FILE_PATH
);
} catch (error) {
DEBUG.init(
'Failed to read environments from file: %s',
error.message
'Failed to read environments from file: %O',
getDebugErrorDetails(error)
);
}

Expand All @@ -1861,32 +1903,43 @@ class BoxCommand extends Command {

let storedInSecureStorage = false;

// Try secure storage first on supported platforms
if (this.supportsSecureStorage) {
DEBUG.init(
'Attempting secure storage write via %s service="%s" account="%s"',
secureStorage.backend,
ENVIRONMENTS_KEYCHAIN_SERVICE,
ENVIRONMENTS_KEYCHAIN_ACCOUNT
);
try {
await keytar.setPassword(
'boxcli' /* service */,
'Box' /* account */,
JSON.stringify(environments) /* password */
await secureStorage.setPassword(
ENVIRONMENTS_KEYCHAIN_SERVICE,
ENVIRONMENTS_KEYCHAIN_ACCOUNT,
JSON.stringify(environments)
);
storedInSecureStorage = true;
DEBUG.init(
'Stored environment configuration in secure storage'
'Stored environment configuration in secure storage (%s)',
secureStorage.backend
);
// Successfully stored in secure storage, remove the file
if (fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
fs.unlinkSync(ENVIRONMENTS_FILE_PATH);
DEBUG.init(
'Removed environment configuration file after migrating to secure storage'
);
}
} catch (keytarError) {
// fallback to file storage if secure storage fails
} catch (error) {
DEBUG.init(
'Could not store credentials in secure storage, falling back to file: %s',
keytarError.message
'Could not store credentials in secure storage (%s), falling back to file: %O',
secureStorage.backend,
getDebugErrorDetails(error)
);
}
} else {
DEBUG.init(
'Skipping secure storage write: platform=%s available=%s',
process.platform,
secureStorage.available
);
}

// Write to file if secure storage failed or not available
Expand All @@ -1895,13 +1948,10 @@ class BoxCommand extends Command {
let fileContents = JSON.stringify(environments, null, 4);
fs.writeFileSync(ENVIRONMENTS_FILE_PATH, fileContents, 'utf8');

// Show warning to user if secure storage was attempted but failed
if (this.supportsSecureStorage) {
if (process.platform === 'linux' && this.supportsSecureStorage) {
this.info(
`Could not store credentials in secure storage, falling back to file.` +
(process.platform === 'linux'
? ' To enable secure storage on Linux, install libsecret-1-dev package.'
: '')
'Could not store credentials in secure storage, falling back to file.' +
' To enable secure storage on Linux, install libsecret-1-dev package.'
);
}
} catch (error) {
Expand Down
Loading
Loading