mirror of
https://github.com/coder/code-server.git
synced 2024-12-04 23:03:06 +08:00
7f12fab3ca
Previously, we used argon2 to verify the hash with the password. If the hash didn't start with a $, then it would enter the catch block. Now we check the hash before trying to verify it and we also throw an Error if the verify fails. This makes the isHashMatch function more robust.
448 lines
16 KiB
TypeScript
448 lines
16 KiB
TypeScript
import * as cp from "child_process"
|
|
import { generateUuid } from "../../../src/common/util"
|
|
import * as util from "../../../src/node/util"
|
|
|
|
describe("getEnvPaths", () => {
|
|
describe("on darwin", () => {
|
|
let ORIGINAL_PLATFORM = ""
|
|
|
|
beforeAll(() => {
|
|
ORIGINAL_PLATFORM = process.platform
|
|
|
|
Object.defineProperty(process, "platform", {
|
|
value: "darwin",
|
|
})
|
|
})
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules()
|
|
jest.mock("env-paths", () => {
|
|
return () => ({
|
|
data: "/home/envPath/.local/share",
|
|
config: "/home/envPath/.config",
|
|
temp: "/tmp/envPath/runtime",
|
|
})
|
|
})
|
|
})
|
|
|
|
afterAll(() => {
|
|
// Restore old platform
|
|
|
|
Object.defineProperty(process, "platform", {
|
|
value: ORIGINAL_PLATFORM,
|
|
})
|
|
})
|
|
|
|
it("should return the env paths using xdgBasedir", () => {
|
|
jest.mock("xdg-basedir", () => ({
|
|
data: "/home/usr/.local/share",
|
|
config: "/home/usr/.config",
|
|
runtime: "/tmp/runtime",
|
|
}))
|
|
const getEnvPaths = require("../../../src/node/util").getEnvPaths
|
|
const envPaths = getEnvPaths()
|
|
|
|
expect(envPaths.data).toEqual("/home/usr/.local/share/code-server")
|
|
expect(envPaths.config).toEqual("/home/usr/.config/code-server")
|
|
expect(envPaths.runtime).toEqual("/tmp/runtime/code-server")
|
|
})
|
|
|
|
it("should return the env paths using envPaths when xdgBasedir is undefined", () => {
|
|
jest.mock("xdg-basedir", () => ({}))
|
|
const getEnvPaths = require("../../../src/node/util").getEnvPaths
|
|
const envPaths = getEnvPaths()
|
|
|
|
expect(envPaths.data).toEqual("/home/envPath/.local/share")
|
|
expect(envPaths.config).toEqual("/home/envPath/.config")
|
|
expect(envPaths.runtime).toEqual("/tmp/envPath/runtime")
|
|
})
|
|
})
|
|
describe("on win32", () => {
|
|
let ORIGINAL_PLATFORM = ""
|
|
|
|
beforeAll(() => {
|
|
ORIGINAL_PLATFORM = process.platform
|
|
|
|
Object.defineProperty(process, "platform", {
|
|
value: "win32",
|
|
})
|
|
})
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules()
|
|
jest.mock("env-paths", () => {
|
|
return () => ({
|
|
data: "/windows/envPath/.local/share",
|
|
config: "/windows/envPath/.config",
|
|
temp: "/tmp/envPath/runtime",
|
|
})
|
|
})
|
|
})
|
|
|
|
afterAll(() => {
|
|
// Restore old platform
|
|
|
|
Object.defineProperty(process, "platform", {
|
|
value: ORIGINAL_PLATFORM,
|
|
})
|
|
})
|
|
|
|
it("should return the env paths using envPaths", () => {
|
|
const getEnvPaths = require("../../../src/node/util").getEnvPaths
|
|
const envPaths = getEnvPaths()
|
|
|
|
expect(envPaths.data).toEqual("/windows/envPath/.local/share")
|
|
expect(envPaths.config).toEqual("/windows/envPath/.config")
|
|
expect(envPaths.runtime).toEqual("/tmp/envPath/runtime")
|
|
})
|
|
})
|
|
describe("on other platforms", () => {
|
|
let ORIGINAL_PLATFORM = ""
|
|
|
|
beforeAll(() => {
|
|
ORIGINAL_PLATFORM = process.platform
|
|
|
|
Object.defineProperty(process, "platform", {
|
|
value: "linux",
|
|
})
|
|
})
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules()
|
|
jest.mock("env-paths", () => {
|
|
return () => ({
|
|
data: "/linux/envPath/.local/share",
|
|
config: "/linux/envPath/.config",
|
|
temp: "/tmp/envPath/runtime",
|
|
})
|
|
})
|
|
})
|
|
|
|
afterAll(() => {
|
|
// Restore old platform
|
|
|
|
Object.defineProperty(process, "platform", {
|
|
value: ORIGINAL_PLATFORM,
|
|
})
|
|
})
|
|
|
|
it("should return the runtime using xdgBasedir if it exists", () => {
|
|
jest.mock("xdg-basedir", () => ({
|
|
runtime: "/tmp/runtime",
|
|
}))
|
|
const getEnvPaths = require("../../../src/node/util").getEnvPaths
|
|
const envPaths = getEnvPaths()
|
|
|
|
expect(envPaths.data).toEqual("/linux/envPath/.local/share")
|
|
expect(envPaths.config).toEqual("/linux/envPath/.config")
|
|
expect(envPaths.runtime).toEqual("/tmp/runtime/code-server")
|
|
})
|
|
|
|
it("should return the env paths using envPaths when xdgBasedir is undefined", () => {
|
|
jest.mock("xdg-basedir", () => ({}))
|
|
const getEnvPaths = require("../../../src/node/util").getEnvPaths
|
|
const envPaths = getEnvPaths()
|
|
|
|
expect(envPaths.data).toEqual("/linux/envPath/.local/share")
|
|
expect(envPaths.config).toEqual("/linux/envPath/.config")
|
|
expect(envPaths.runtime).toEqual("/tmp/envPath/runtime")
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("hash", () => {
|
|
it("should return a hash of the string passed in", async () => {
|
|
const plainTextPassword = "mySecretPassword123"
|
|
const hashed = await util.hash(plainTextPassword)
|
|
expect(hashed).not.toBe(plainTextPassword)
|
|
})
|
|
})
|
|
|
|
describe("isHashMatch", () => {
|
|
it("should return true if the password matches the hash", async () => {
|
|
const password = "codeserver1234"
|
|
const _hash = await util.hash(password)
|
|
const actual = await util.isHashMatch(password, _hash)
|
|
expect(actual).toBe(true)
|
|
})
|
|
it("should return false if the password does not match the hash", async () => {
|
|
const password = "password123"
|
|
const _hash = await util.hash(password)
|
|
const actual = await util.isHashMatch("otherPassword123", _hash)
|
|
expect(actual).toBe(false)
|
|
})
|
|
it("should return true with actual hash", async () => {
|
|
const password = "password123"
|
|
const _hash = "$argon2i$v=19$m=4096,t=3,p=1$EAoczTxVki21JDfIZpTUxg$rkXgyrW4RDGoDYrxBFD4H2DlSMEhP4h+Api1hXnGnFY"
|
|
const actual = await util.isHashMatch(password, _hash)
|
|
expect(actual).toBe(true)
|
|
})
|
|
it("should return false if the password is empty", async () => {
|
|
const password = ""
|
|
const _hash = "$argon2i$v=19$m=4096,t=3,p=1$EAoczTxVki21JDfIZpTUxg$rkXgyrW4RDGoDYrxBFD4H2DlSMEhP4h+Api1hXnGnFY"
|
|
const actual = await util.isHashMatch(password, _hash)
|
|
expect(actual).toBe(false)
|
|
})
|
|
it("should return false if the hash is empty", async () => {
|
|
const password = "hellowpasssword"
|
|
const _hash = ""
|
|
const actual = await util.isHashMatch(password, _hash)
|
|
expect(actual).toBe(false)
|
|
})
|
|
it("should return false and not throw an error if the hash doesn't start with a $", async () => {
|
|
const password = "hellowpasssword"
|
|
const _hash = "n2i$v=19$m=4096,t=3,p=1$EAoczTxVki21JDfIZpTUxg$rkXgyrW4RDGoDYrxBFD4H2DlSMEhP4h+Api1hXnGnFY"
|
|
expect(async () => await util.isHashMatch(password, _hash)).not.toThrow()
|
|
expect(await util.isHashMatch(password, _hash)).toBe(false)
|
|
})
|
|
it("should reject the promise and throw if error", async () => {
|
|
const password = "hellowpasssword"
|
|
const _hash = "$ar2i"
|
|
expect(async () => await util.isHashMatch(password, _hash)).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe("hashLegacy", () => {
|
|
it("should return a hash of the string passed in", () => {
|
|
const plainTextPassword = "mySecretPassword123"
|
|
const hashed = util.hashLegacy(plainTextPassword)
|
|
expect(hashed).not.toBe(plainTextPassword)
|
|
})
|
|
})
|
|
|
|
describe("isHashLegacyMatch", () => {
|
|
it("should return true if is match", () => {
|
|
const password = "password123"
|
|
const _hash = util.hashLegacy(password)
|
|
expect(util.isHashLegacyMatch(password, _hash)).toBe(true)
|
|
})
|
|
it("should return false if is match", () => {
|
|
const password = "password123"
|
|
const _hash = util.hashLegacy(password)
|
|
expect(util.isHashLegacyMatch("otherPassword123", _hash)).toBe(false)
|
|
})
|
|
it("should return true if hashed from command line", () => {
|
|
const password = "password123"
|
|
// Hashed using printf "password123" | sha256sum | cut -d' ' -f1
|
|
const _hash = "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"
|
|
expect(util.isHashLegacyMatch(password, _hash)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe("getPasswordMethod", () => {
|
|
it("should return PLAIN_TEXT for no hashed password", () => {
|
|
const hashedPassword = undefined
|
|
const passwordMethod = util.getPasswordMethod(hashedPassword)
|
|
const expected: util.PasswordMethod = "PLAIN_TEXT"
|
|
expect(passwordMethod).toEqual(expected)
|
|
})
|
|
it("should return ARGON2 for password with 'argon2'", () => {
|
|
const hashedPassword =
|
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY"
|
|
const passwordMethod = util.getPasswordMethod(hashedPassword)
|
|
const expected: util.PasswordMethod = "ARGON2"
|
|
expect(passwordMethod).toEqual(expected)
|
|
})
|
|
it("should return SHA256 for password with legacy hash", () => {
|
|
const hashedPassword = "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af"
|
|
const passwordMethod = util.getPasswordMethod(hashedPassword)
|
|
const expected: util.PasswordMethod = "SHA256"
|
|
expect(passwordMethod).toEqual(expected)
|
|
})
|
|
})
|
|
|
|
describe("handlePasswordValidation", () => {
|
|
it("should return true with a hashedPassword for a PLAIN_TEXT password", async () => {
|
|
const p = "password"
|
|
const passwordValidation = await util.handlePasswordValidation({
|
|
passwordMethod: "PLAIN_TEXT",
|
|
passwordFromRequestBody: p,
|
|
passwordFromArgs: p,
|
|
hashedPasswordFromArgs: undefined,
|
|
})
|
|
|
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
|
|
|
expect(passwordValidation.isPasswordValid).toBe(true)
|
|
expect(matchesHash).toBe(true)
|
|
})
|
|
it("should return false when PLAIN_TEXT password doesn't match args", async () => {
|
|
const p = "password"
|
|
const passwordValidation = await util.handlePasswordValidation({
|
|
passwordMethod: "PLAIN_TEXT",
|
|
passwordFromRequestBody: "password1",
|
|
passwordFromArgs: p,
|
|
hashedPasswordFromArgs: undefined,
|
|
})
|
|
|
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
|
|
|
expect(passwordValidation.isPasswordValid).toBe(false)
|
|
expect(matchesHash).toBe(false)
|
|
})
|
|
it("should return true with a hashedPassword for a SHA256 password", async () => {
|
|
const p = "helloworld"
|
|
const passwordValidation = await util.handlePasswordValidation({
|
|
passwordMethod: "SHA256",
|
|
passwordFromRequestBody: p,
|
|
passwordFromArgs: undefined,
|
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
|
})
|
|
|
|
const matchesHash = util.isHashLegacyMatch(p, passwordValidation.hashedPassword)
|
|
|
|
expect(passwordValidation.isPasswordValid).toBe(true)
|
|
expect(matchesHash).toBe(true)
|
|
})
|
|
it("should return false when SHA256 password doesn't match hash", async () => {
|
|
const p = "helloworld1"
|
|
const passwordValidation = await util.handlePasswordValidation({
|
|
passwordMethod: "SHA256",
|
|
passwordFromRequestBody: p,
|
|
passwordFromArgs: undefined,
|
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
|
})
|
|
|
|
const matchesHash = util.isHashLegacyMatch(p, passwordValidation.hashedPassword)
|
|
|
|
expect(passwordValidation.isPasswordValid).toBe(false)
|
|
expect(matchesHash).toBe(false)
|
|
})
|
|
it("should return true with a hashedPassword for a ARGON2 password", async () => {
|
|
const p = "password"
|
|
const passwordValidation = await util.handlePasswordValidation({
|
|
passwordMethod: "ARGON2",
|
|
passwordFromRequestBody: p,
|
|
passwordFromArgs: undefined,
|
|
hashedPasswordFromArgs:
|
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
|
})
|
|
|
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
|
|
|
expect(passwordValidation.isPasswordValid).toBe(true)
|
|
expect(matchesHash).toBe(true)
|
|
})
|
|
it("should return false when ARGON2 password doesn't match hash", async () => {
|
|
const p = "password1"
|
|
const passwordValidation = await util.handlePasswordValidation({
|
|
passwordMethod: "ARGON2",
|
|
passwordFromRequestBody: p,
|
|
passwordFromArgs: undefined,
|
|
hashedPasswordFromArgs:
|
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
|
})
|
|
|
|
const matchesHash = await util.isHashMatch(p, passwordValidation.hashedPassword)
|
|
|
|
expect(passwordValidation.isPasswordValid).toBe(false)
|
|
expect(matchesHash).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("isCookieValid", () => {
|
|
it("should be valid if hashed-password for SHA256 matches cookie.key", async () => {
|
|
const isValid = await util.isCookieValid({
|
|
passwordMethod: "SHA256",
|
|
cookieKey: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
|
passwordFromArgs: undefined,
|
|
})
|
|
expect(isValid).toBe(true)
|
|
})
|
|
it("should be invalid if hashed-password for SHA256 does not match cookie.key", async () => {
|
|
const isValid = await util.isCookieValid({
|
|
passwordMethod: "SHA256",
|
|
cookieKey: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb9442bb6f8f8f07af",
|
|
hashedPasswordFromArgs: "936a185caaa266bb9cbe981e9e05cb78cd732b0b3280eb944412bb6f8f8f07af",
|
|
passwordFromArgs: undefined,
|
|
})
|
|
expect(isValid).toBe(false)
|
|
})
|
|
it("should be valid if hashed-password for ARGON2 matches cookie.key", async () => {
|
|
const isValid = await util.isCookieValid({
|
|
passwordMethod: "ARGON2",
|
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
|
hashedPasswordFromArgs:
|
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
|
passwordFromArgs: undefined,
|
|
})
|
|
expect(isValid).toBe(true)
|
|
})
|
|
it("should be invalid if hashed-password for ARGON2 does not match cookie.key", async () => {
|
|
const isValid = await util.isCookieValid({
|
|
passwordMethod: "ARGON2",
|
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9H",
|
|
hashedPasswordFromArgs:
|
|
"$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
|
passwordFromArgs: undefined,
|
|
})
|
|
expect(isValid).toBe(false)
|
|
})
|
|
it("should be valid if password for PLAIN_TEXT matches cookie.key", async () => {
|
|
const isValid = await util.isCookieValid({
|
|
passwordMethod: "PLAIN_TEXT",
|
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9HYY",
|
|
passwordFromArgs: "password",
|
|
hashedPasswordFromArgs: undefined,
|
|
})
|
|
expect(isValid).toBe(true)
|
|
})
|
|
it("should be invalid if hashed-password for PLAIN_TEXT does not match cookie.key", async () => {
|
|
const isValid = await util.isCookieValid({
|
|
passwordMethod: "PLAIN_TEXT",
|
|
cookieKey: "$argon2i$v=19$m=4096,t=3,p=1$0qR/o+0t00hsbJFQCKSfdQ$oFcM4rL6o+B7oxpuA4qlXubypbBPsf+8L531U7P9H",
|
|
passwordFromArgs: "password1234",
|
|
hashedPasswordFromArgs: undefined,
|
|
})
|
|
expect(isValid).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("sanitizeString", () => {
|
|
it("should return an empty string if passed a type other than a string", () => {
|
|
expect(util.sanitizeString({} as string)).toBe("")
|
|
})
|
|
it("should trim whitespace", () => {
|
|
expect(util.sanitizeString(" hello ")).toBe("hello")
|
|
})
|
|
it("should always return an empty string", () => {
|
|
expect(util.sanitizeString(" ")).toBe("")
|
|
})
|
|
})
|
|
|
|
describe("onLine", () => {
|
|
// Spawn a process that outputs anything given on stdin.
|
|
let proc: cp.ChildProcess | undefined
|
|
|
|
beforeAll(() => {
|
|
proc = cp.spawn("node", ["-e", 'process.stdin.setEncoding("utf8");process.stdin.on("data", console.log)'])
|
|
})
|
|
|
|
afterAll(() => {
|
|
proc?.kill()
|
|
})
|
|
|
|
it("should call with individual lines", async () => {
|
|
const size = 100
|
|
const received = new Promise<string[]>((resolve) => {
|
|
const lines: string[] = []
|
|
util.onLine(proc!, (line) => {
|
|
lines.push(line)
|
|
if (lines.length === size) {
|
|
resolve(lines)
|
|
}
|
|
})
|
|
})
|
|
|
|
const expected: string[] = []
|
|
for (let i = 0; i < size; ++i) {
|
|
expected.push(generateUuid(i))
|
|
}
|
|
|
|
proc?.stdin?.write(expected.join("\n"))
|
|
|
|
expect(await received).toEqual(expected)
|
|
})
|
|
})
|