Skip to content

Commit 6b2ed36

Browse files
feat: Unify secure storage backend across platforms (#647)
Co-authored-by: Łukasz Socha <31014760+lukaszsocha2@users.noreply.github.com>
1 parent e303644 commit 6b2ed36

7 files changed

Lines changed: 472 additions & 106 deletions

File tree

‎LICENSE-THIRD-PARTY.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5235,6 +5235,7 @@ The following npm packages may be included in this product:
52355235
- csv-stringify@6.6.0
52365236
- degenerator@5.0.1
52375237
- isarray@1.0.0
5238+
- keychain@1.5.0
52385239
- netmask@2.0.2
52395240
- tr46@0.0.3
52405241
- undici-types@5.26.5

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"fs-extra": "^10.1.0",
4343
"inquirer": "^8.2.7",
4444
"js-yaml": "^4.1.1",
45+
"keychain": "^1.5.0",
4546
"keytar": "^7.9.0",
4647
"lodash": "^4.17.13",
4748
"mkdirp": "^3.0.1",

‎src/box-command.js‎

Lines changed: 84 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,7 @@ const pkg = require('../package.json');
3535
const inquirer = require('./inquirer');
3636
const { stringifyStream } = require('@discoveryjs/json-ext');
3737
const progress = require('cli-progress');
38-
let keytar = null;
39-
try {
40-
keytar = require('keytar');
41-
} catch {
42-
// keytar cannot be imported because the library is not provided for this operating system / architecture
43-
}
38+
const secureStorage = require('./secure-storage');
4439

4540
const DEBUG = require('./debug');
4641
const stream = require('node:stream');
@@ -96,9 +91,29 @@ const ENVIRONMENTS_FILE_PATH = path.join(
9691
CONFIG_FOLDER_PATH,
9792
'box_environments.json'
9893
);
94+
const ENVIRONMENTS_KEYCHAIN_SERVICE = 'boxcli';
95+
const ENVIRONMENTS_KEYCHAIN_ACCOUNT = 'Box';
9996

10097
const DEFAULT_ANALYTICS_CLIENT_NAME = 'box-cli';
10198

99+
/**
100+
* Convert error objects to a stable debug-safe shape.
101+
*
102+
* @param {unknown} error A caught error object
103+
* @returns {Object} A reduced object for DEBUG logging
104+
*/
105+
function getDebugErrorDetails(error) {
106+
if (!error || typeof error !== 'object') {
107+
return { message: String(error) };
108+
}
109+
return {
110+
name: error.name || 'Error',
111+
code: error.code,
112+
message: error.message || String(error),
113+
stack: error.stack,
114+
};
115+
}
116+
102117
/**
103118
* Parse a string value from CSV into the correct boolean value
104119
* @param {string|boolean} value The value to parse
@@ -300,8 +315,7 @@ class BoxCommand extends Command {
300315
this.disableRequiredArgsAndFlags();
301316
}
302317

303-
this.supportsSecureStorage =
304-
keytar && ['darwin', 'win32', 'linux'].includes(process.platform);
318+
this.supportsSecureStorage = secureStorage.available;
305319

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

@@ -1809,34 +1823,62 @@ class BoxCommand extends Command {
18091823
* @returns {Object} The parsed environment information
18101824
*/
18111825
async getEnvironments() {
1812-
// Try secure storage first on supported platforms
18131826
if (this.supportsSecureStorage) {
1827+
DEBUG.init(
1828+
'Attempting secure storage read via %s service="%s" account="%s"',
1829+
secureStorage.backend,
1830+
ENVIRONMENTS_KEYCHAIN_SERVICE,
1831+
ENVIRONMENTS_KEYCHAIN_ACCOUNT
1832+
);
18141833
try {
1815-
const password = await keytar.getPassword(
1816-
'boxcli' /* service */,
1817-
'Box' /* account */
1834+
const password = await secureStorage.getPassword(
1835+
ENVIRONMENTS_KEYCHAIN_SERVICE,
1836+
ENVIRONMENTS_KEYCHAIN_ACCOUNT
18181837
);
18191838
if (password) {
1839+
DEBUG.init(
1840+
'Successfully loaded environments from secure storage (%s)',
1841+
secureStorage.backend
1842+
);
18201843
return JSON.parse(password);
18211844
}
1845+
DEBUG.init(
1846+
'Secure storage returned empty result for service="%s" account="%s"',
1847+
ENVIRONMENTS_KEYCHAIN_SERVICE,
1848+
ENVIRONMENTS_KEYCHAIN_ACCOUNT
1849+
);
18221850
} catch (error) {
18231851
DEBUG.init(
1824-
'Failed to read from secure storage, falling back to file: %s',
1825-
error.message
1852+
'Failed to read from secure storage (%s), falling back to file: %O',
1853+
secureStorage.backend,
1854+
getDebugErrorDetails(error)
18261855
);
1827-
// fallback to env file
18281856
}
1857+
} else {
1858+
DEBUG.init(
1859+
'Skipping secure storage read: platform=%s available=%s',
1860+
process.platform,
1861+
secureStorage.available
1862+
);
18291863
}
18301864

18311865
// Try to read from file (fallback or no secure storage)
18321866
try {
18331867
if (fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
1868+
DEBUG.init(
1869+
'Attempting environments fallback file read at %s',
1870+
ENVIRONMENTS_FILE_PATH
1871+
);
18341872
return JSON.parse(fs.readFileSync(ENVIRONMENTS_FILE_PATH));
18351873
}
1874+
DEBUG.init(
1875+
'Environments fallback file does not exist at %s',
1876+
ENVIRONMENTS_FILE_PATH
1877+
);
18361878
} catch (error) {
18371879
DEBUG.init(
1838-
'Failed to read environments from file: %s',
1839-
error.message
1880+
'Failed to read environments from file: %O',
1881+
getDebugErrorDetails(error)
18401882
);
18411883
}
18421884

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

18621904
let storedInSecureStorage = false;
18631905

1864-
// Try secure storage first on supported platforms
18651906
if (this.supportsSecureStorage) {
1907+
DEBUG.init(
1908+
'Attempting secure storage write via %s service="%s" account="%s"',
1909+
secureStorage.backend,
1910+
ENVIRONMENTS_KEYCHAIN_SERVICE,
1911+
ENVIRONMENTS_KEYCHAIN_ACCOUNT
1912+
);
18661913
try {
1867-
await keytar.setPassword(
1868-
'boxcli' /* service */,
1869-
'Box' /* account */,
1870-
JSON.stringify(environments) /* password */
1914+
await secureStorage.setPassword(
1915+
ENVIRONMENTS_KEYCHAIN_SERVICE,
1916+
ENVIRONMENTS_KEYCHAIN_ACCOUNT,
1917+
JSON.stringify(environments)
18711918
);
18721919
storedInSecureStorage = true;
18731920
DEBUG.init(
1874-
'Stored environment configuration in secure storage'
1921+
'Stored environment configuration in secure storage (%s)',
1922+
secureStorage.backend
18751923
);
1876-
// Successfully stored in secure storage, remove the file
18771924
if (fs.existsSync(ENVIRONMENTS_FILE_PATH)) {
18781925
fs.unlinkSync(ENVIRONMENTS_FILE_PATH);
18791926
DEBUG.init(
18801927
'Removed environment configuration file after migrating to secure storage'
18811928
);
18821929
}
1883-
} catch (keytarError) {
1884-
// fallback to file storage if secure storage fails
1930+
} catch (error) {
18851931
DEBUG.init(
1886-
'Could not store credentials in secure storage, falling back to file: %s',
1887-
keytarError.message
1932+
'Could not store credentials in secure storage (%s), falling back to file: %O',
1933+
secureStorage.backend,
1934+
getDebugErrorDetails(error)
18881935
);
18891936
}
1937+
} else {
1938+
DEBUG.init(
1939+
'Skipping secure storage write: platform=%s available=%s',
1940+
process.platform,
1941+
secureStorage.available
1942+
);
18901943
}
18911944

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

1898-
// Show warning to user if secure storage was attempted but failed
1899-
if (this.supportsSecureStorage) {
1951+
if (process.platform === 'linux' && this.supportsSecureStorage) {
19001952
this.info(
1901-
`Could not store credentials in secure storage, falling back to file.` +
1902-
(process.platform === 'linux'
1903-
? ' To enable secure storage on Linux, install libsecret-1-dev package.'
1904-
: '')
1953+
'Could not store credentials in secure storage, falling back to file.' +
1954+
' To enable secure storage on Linux, install libsecret-1-dev package.'
19051955
);
19061956
}
19071957
} catch (error) {

0 commit comments

Comments
 (0)