Skip to content
Closed
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
19 changes: 17 additions & 2 deletions script/auth/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import AccountManager = require("../management-sdk");

const packageJson = require("../../../package.json");

const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".revopush.config");
export const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".revopush.config");

// The file holds the access key, so only its owner may read it
const OWNER_ONLY = 0o600;

const CLI_HEADERS: Headers = {
"X-CodePush-CLI-Version": packageJson.version,
Expand Down Expand Up @@ -39,6 +42,7 @@ export function deserializeConnectionInfo(): ILoginConnectionInfo {
const savedConnection: string = fs.readFileSync(configFilePath, {
encoding: "utf8",
});
restrictToOwner();
let connectionInfo: ILegacyLoginConnectionInfo | ILoginConnectionInfo = JSON.parse(savedConnection);

// If the connection info is in the legacy format, convert it to the modern format
Expand All @@ -54,6 +58,15 @@ export function deserializeConnectionInfo(): ILoginConnectionInfo {
}
}

// Also fixes files saved by older versions, which other users could read
function restrictToOwner(): void {
try {
fs.chmodSync(configFilePath, OWNER_ONLY);
} catch {
// The file doesn't exist yet, or belongs to another user
}
}

export function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLogout: boolean, customServerUrl?: string): void {
const connectionInfo: ILoginConnectionInfo = {
accessKey: accessKey,
Expand All @@ -64,7 +77,9 @@ export function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLo
}

const json: string = JSON.stringify(connectionInfo);
fs.writeFileSync(configFilePath, json, { encoding: "utf8" });
// `mode` applies only when the file is created; an existing file is restricted before the key is written to it
restrictToOwner();
fs.writeFileSync(configFilePath, json, { encoding: "utf8", mode: OWNER_ONLY });

log(
`\r\nSuccessfully logged-in. Your session file was written to ${chalk.cyan(configFilePath)}. You can run the ${chalk.cyan(
Expand Down
51 changes: 51 additions & 0 deletions test/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { strict as assert } from "assert";
import * as fs from "fs";
import * as sinon from "sinon";
import {
configFilePath as configFile,
deleteConnectionInfoCache,
deserializeConnectionInfo,
serializeConnectionInfo,
} from "../script/auth/connection";
import * as terminal from "../script/ui/terminal";

// File modes don't apply on Windows
const describeOnPosix = process.platform === "win32" ? describe.skip : describe;

describeOnPosix("connection", () => {
// test/setup.ts points HOME at a temp folder
const mode = () => fs.statSync(configFile).mode & 0o777;
// Set the mode explicitly: the `mode` option is masked by the umask, which may already strip the read bits
const writeReadableByOthers = (content: string) => {
fs.writeFileSync(configFile, content);
fs.chmodSync(configFile, 0o644);
};

beforeEach(() => sinon.stub(terminal, "log"));
afterEach(() => {
sinon.restore();
deleteConnectionInfoCache(/* printMessage */ false);
});

it("saves the access key readable only by the owner", () => {
serializeConnectionInfo("key", false);

assert.equal(mode(), 0o600);
assert.equal(deserializeConnectionInfo().accessKey, "key");
});

it("restricts a config file that other users could read before saving the key", () => {
writeReadableByOthers("{}");

serializeConnectionInfo("key", false);

assert.equal(mode(), 0o600);
});

it("restricts a config file saved by an older version when reading it", () => {
writeReadableByOthers(JSON.stringify({ accessKey: "key" }));

assert.equal(deserializeConnectionInfo().accessKey, "key");
assert.equal(mode(), 0o600);
});
});
Loading