mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
Move to libs
This commit is contained in:
15
libs/node/jest.config.js
Normal file
15
libs/node/jest.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("./tsconfig");
|
||||
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testMatch: ["**/+(*.)+(spec).+(ts)"],
|
||||
setupFilesAfterEnv: ["<rootDir>/spec/test.ts"],
|
||||
collectCoverage: true,
|
||||
coverageReporters: ["html", "lcov"],
|
||||
coverageDirectory: "coverage",
|
||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
||||
prefix: "<rootDir>/",
|
||||
}),
|
||||
};
|
||||
1183
libs/node/package-lock.json
generated
Normal file
1183
libs/node/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
libs/node/package.json
Normal file
38
libs/node/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@bitwarden/jslib-node",
|
||||
"version": "0.0.0",
|
||||
"description": "Common code used across Bitwarden JavaScript projects.",
|
||||
"keywords": [
|
||||
"bitwarden"
|
||||
],
|
||||
"author": "Bitwarden Inc.",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/bitwarden/jslib"
|
||||
},
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/**/*",
|
||||
"build": "npm run clean && tsc",
|
||||
"build:watch": "npm run clean && tsc -watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/inquirer": "^7.3.1",
|
||||
"@types/lowdb": "^1.0.10",
|
||||
"@types/node": "^16.11.12",
|
||||
"@types/node-fetch": "^2.5.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "4.3.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bitwarden/jslib-common": "file:../common",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "7.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"inquirer": "8.0.0",
|
||||
"lowdb": "1.0.0",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
45
libs/node/spec/cli/consoleLog.service.spec.ts
Normal file
45
libs/node/spec/cli/consoleLog.service.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
interceptConsole,
|
||||
restoreConsole,
|
||||
} from "jslib-common/../spec/services/consolelog.service.spec";
|
||||
|
||||
import { ConsoleLogService } from "jslib-node/cli/services/consoleLog.service";
|
||||
|
||||
let caughtMessage: any = {};
|
||||
|
||||
describe("CLI Console log service", () => {
|
||||
let logService: ConsoleLogService;
|
||||
beforeEach(() => {
|
||||
caughtMessage = {};
|
||||
interceptConsole(caughtMessage);
|
||||
logService = new ConsoleLogService(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
restoreConsole();
|
||||
});
|
||||
|
||||
it("should redirect all console to error if BW_RESPONSE env is true", () => {
|
||||
process.env.BW_RESPONSE = "true";
|
||||
|
||||
logService.debug("this is a debug message");
|
||||
expect(caughtMessage).toMatchObject({
|
||||
error: { 0: "this is a debug message" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should not redirect console to error if BW_RESPONSE != true", () => {
|
||||
process.env.BW_RESPONSE = "false";
|
||||
|
||||
logService.debug("debug");
|
||||
logService.info("info");
|
||||
logService.warning("warning");
|
||||
logService.error("error");
|
||||
|
||||
expect(caughtMessage).toMatchObject({
|
||||
log: { 0: "info" },
|
||||
warn: { 0: "warning" },
|
||||
error: { 0: "error" },
|
||||
});
|
||||
});
|
||||
});
|
||||
518
libs/node/spec/services/nodeCryptoFunction.service.spec.ts
Normal file
518
libs/node/spec/services/nodeCryptoFunction.service.spec.ts
Normal file
@@ -0,0 +1,518 @@
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||
import { NodeCryptoFunctionService } from "jslib-node/services/nodeCryptoFunction.service";
|
||||
|
||||
const RsaPublicKey =
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" +
|
||||
"4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" +
|
||||
"RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN" +
|
||||
"084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc" +
|
||||
"xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB";
|
||||
const RsaPrivateKey =
|
||||
"MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz" +
|
||||
"YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L" +
|
||||
"nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/" +
|
||||
"YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK" +
|
||||
"PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q" +
|
||||
"Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj" +
|
||||
"WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh" +
|
||||
"5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk" +
|
||||
"1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU" +
|
||||
"BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf" +
|
||||
"TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU" +
|
||||
"q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv" +
|
||||
"q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX" +
|
||||
"5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1" +
|
||||
"eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE" +
|
||||
"Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8" +
|
||||
"+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ" +
|
||||
"BokBGnjFnTnKcs7nv/O8=";
|
||||
|
||||
const Sha1Mac = "4d4c223f95dc577b665ec4ccbcb680b80a397038";
|
||||
const Sha256Mac = "6be3caa84922e12aaaaa2f16c40d44433bb081ef323db584eb616333ab4e874f";
|
||||
const Sha512Mac =
|
||||
"21910e341fa12106ca35758a2285374509326c9fbe0bd64e7b99c898f841dc948c58ce66d3504d8883c" +
|
||||
"5ea7817a0b7c5d4d9b00364ccd214669131fc17fe4aca";
|
||||
|
||||
describe("NodeCrypto Function Service", () => {
|
||||
describe("pbkdf2", () => {
|
||||
const regular256Key = "pj9prw/OHPleXI6bRdmlaD+saJS4awrMiQsQiDjeu2I=";
|
||||
const utf8256Key = "yqvoFXgMRmHR3QPYr5pyR4uVuoHkltv9aHUP63p8n7I=";
|
||||
const unicode256Key = "ZdeOata6xoRpB4DLp8zHhXz5kLmkWtX5pd+TdRH8w8w=";
|
||||
|
||||
const regular512Key =
|
||||
"liTi/Ke8LPU1Qv+Vl7NGEVt/XMbsBVJ2kQxtVG/Z1/JFHFKQW3ZkI81qVlwTiCpb+cFXzs+57" +
|
||||
"eyhhx5wfKo5Cg==";
|
||||
const utf8512Key =
|
||||
"df0KdvIBeCzD/kyXptwQohaqUa4e7IyFUyhFQjXCANu5T+scq55hCcE4dG4T/MhAk2exw8j7ixRN" +
|
||||
"zXANiVZpnw==";
|
||||
const unicode512Key =
|
||||
"FE+AnUJaxv8jh+zUDtZz4mjjcYk0/PZDZm+SLJe3XtxtnpdqqpblX6JjuMZt/dYYNMOrb2+mD" +
|
||||
"L3FiQDTROh1lg==";
|
||||
|
||||
testPbkdf2("sha256", regular256Key, utf8256Key, unicode256Key);
|
||||
testPbkdf2("sha512", regular512Key, utf8512Key, unicode512Key);
|
||||
});
|
||||
|
||||
describe("hkdf", () => {
|
||||
const regular256Key = "qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw=";
|
||||
const utf8256Key = "6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU=";
|
||||
const unicode256Key = "gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A=";
|
||||
|
||||
const regular512Key = "xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM=";
|
||||
const utf8512Key = "XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY=";
|
||||
const unicode512Key = "148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc=";
|
||||
|
||||
testHkdf("sha256", regular256Key, utf8256Key, unicode256Key);
|
||||
testHkdf("sha512", regular512Key, utf8512Key, unicode512Key);
|
||||
});
|
||||
|
||||
describe("hkdfExpand", () => {
|
||||
const prk16Byte = "criAmKtfzxanbgea5/kelQ==";
|
||||
const prk32Byte = "F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y=";
|
||||
const prk64Byte =
|
||||
"ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+" +
|
||||
"gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==";
|
||||
|
||||
testHkdfExpand("sha256", prk32Byte, 32, "BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8=");
|
||||
testHkdfExpand(
|
||||
"sha256",
|
||||
prk32Byte,
|
||||
64,
|
||||
"BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+" +
|
||||
"/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA=="
|
||||
);
|
||||
testHkdfExpand("sha512", prk64Byte, 32, "uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk=");
|
||||
testHkdfExpand(
|
||||
"sha512",
|
||||
prk64Byte,
|
||||
64,
|
||||
"uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+" +
|
||||
"MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w=="
|
||||
);
|
||||
|
||||
it("should fail with prk too small", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const f = cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(prk16Byte),
|
||||
"info",
|
||||
32,
|
||||
"sha256"
|
||||
);
|
||||
await expect(f).rejects.toEqual(new Error("prk is too small."));
|
||||
});
|
||||
|
||||
it("should fail with outputByteSize is too large", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const f = cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(prk32Byte),
|
||||
"info",
|
||||
8161,
|
||||
"sha256"
|
||||
);
|
||||
await expect(f).rejects.toEqual(new Error("outputByteSize is too large."));
|
||||
});
|
||||
});
|
||||
|
||||
describe("hash", () => {
|
||||
const regular1Hash = "2a241604fb921fad12bf877282457268e1dccb70";
|
||||
const utf81Hash = "85672798dc5831e96d6c48655d3d39365a9c88b6";
|
||||
const unicode1Hash = "39c975935054a3efc805a9709b60763a823a6ad4";
|
||||
|
||||
const regular256Hash = "2b8e96031d352a8655d733d7a930b5ffbea69dc25cf65c7bca7dd946278908b2";
|
||||
const utf8256Hash = "25fe8440f5b01ed113b0a0e38e721b126d2f3f77a67518c4a04fcde4e33eeb9d";
|
||||
const unicode256Hash = "adc1c0c2afd6e92cefdf703f9b6eb2c38e0d6d1a040c83f8505c561fea58852e";
|
||||
|
||||
const regular512Hash =
|
||||
"c15cf11d43bde333647e3f559ec4193bb2edeaa0e8b902772f514cdf3f785a3f49a6e02a4b87b3" +
|
||||
"b47523271ad45b7e0aebb5cdcc1bc54815d256eb5dcb80da9d";
|
||||
const utf8512Hash =
|
||||
"035c31a877a291af09ed2d3a1a293e69c3e079ea2cecc00211f35e6bce10474ca3ad6e30b59e26118" +
|
||||
"37463f20969c5bc95282965a051a88f8cdf2e166549fcdd";
|
||||
const unicode512Hash =
|
||||
"2b16a5561af8ad6fe414cc103fc8036492e1fc6d9aabe1b655497054f760fe0e34c5d100ac773d" +
|
||||
"9f3030438284f22dbfa20cb2e9b019f2c98dfe38ce1ef41bae";
|
||||
|
||||
const regularMd5 = "5eceffa53a5fd58c44134211e2c5f522";
|
||||
const utf8Md5 = "3abc9433c09551b939c80aa0aa3174e1";
|
||||
const unicodeMd5 = "85ae134072c8d81257933f7045ba17ca";
|
||||
|
||||
testHash("sha1", regular1Hash, utf81Hash, unicode1Hash);
|
||||
testHash("sha256", regular256Hash, utf8256Hash, unicode256Hash);
|
||||
testHash("sha512", regular512Hash, utf8512Hash, unicode512Hash);
|
||||
testHash("md5", regularMd5, utf8Md5, unicodeMd5);
|
||||
});
|
||||
|
||||
describe("hmac", () => {
|
||||
testHmac("sha1", Sha1Mac);
|
||||
testHmac("sha256", Sha256Mac);
|
||||
testHmac("sha512", Sha512Mac);
|
||||
});
|
||||
|
||||
describe("compare", () => {
|
||||
testCompare(false);
|
||||
});
|
||||
|
||||
describe("hmacFast", () => {
|
||||
testHmac("sha1", Sha1Mac, true);
|
||||
testHmac("sha256", Sha256Mac, true);
|
||||
testHmac("sha512", Sha512Mac, true);
|
||||
});
|
||||
|
||||
describe("compareFast", () => {
|
||||
testCompare(true);
|
||||
});
|
||||
|
||||
describe("aesEncrypt", () => {
|
||||
it("should successfully encrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const iv = makeStaticByteArray(16);
|
||||
const key = makeStaticByteArray(32);
|
||||
const data = Utils.fromUtf8ToArray("EncryptMe!");
|
||||
const encValue = await nodeCryptoFunctionService.aesEncrypt(
|
||||
data.buffer,
|
||||
iv.buffer,
|
||||
key.buffer
|
||||
);
|
||||
expect(Utils.fromBufferToB64(encValue)).toBe("ByUF8vhyX4ddU9gcooznwA==");
|
||||
});
|
||||
|
||||
it("should successfully encrypt and then decrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const iv = makeStaticByteArray(16);
|
||||
const key = makeStaticByteArray(32);
|
||||
const value = "EncryptMe!";
|
||||
const data = Utils.fromUtf8ToArray(value);
|
||||
const encValue = await nodeCryptoFunctionService.aesEncrypt(
|
||||
data.buffer,
|
||||
iv.buffer,
|
||||
key.buffer
|
||||
);
|
||||
const decValue = await nodeCryptoFunctionService.aesDecrypt(encValue, iv.buffer, key.buffer);
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecryptFast", () => {
|
||||
it("should successfully decrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer);
|
||||
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer);
|
||||
const data = "ByUF8vhyX4ddU9gcooznwA==";
|
||||
const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
|
||||
const decValue = await nodeCryptoFunctionService.aesDecryptFast(params);
|
||||
expect(decValue).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("aesDecrypt", () => {
|
||||
it("should successfully decrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const iv = makeStaticByteArray(16);
|
||||
const key = makeStaticByteArray(32);
|
||||
const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA==");
|
||||
const decValue = await nodeCryptoFunctionService.aesDecrypt(
|
||||
data.buffer,
|
||||
iv.buffer,
|
||||
key.buffer
|
||||
);
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsaEncrypt", () => {
|
||||
it("should successfully encrypt and then decrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const pubKey = Utils.fromB64ToArray(RsaPublicKey);
|
||||
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
|
||||
const value = "EncryptMe!";
|
||||
const data = Utils.fromUtf8ToArray(value);
|
||||
const encValue = await nodeCryptoFunctionService.rsaEncrypt(
|
||||
data.buffer,
|
||||
pubKey.buffer,
|
||||
"sha1"
|
||||
);
|
||||
const decValue = await nodeCryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, "sha1");
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsaDecrypt", () => {
|
||||
it("should successfully decrypt data", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
|
||||
const data = Utils.fromB64ToArray(
|
||||
"A1/p8BQzN9UrbdYxUY2Va5+kPLyfZXF9JsZrjeEXcaclsnHurdxVAJcnbEqYMP3UXV" +
|
||||
"4YAS/mpf+Rxe6/X0WS1boQdA0MAHSgx95hIlAraZYpiMLLiJRKeo2u8YivCdTM9V5vuAEJwf9Tof/qFsFci3sApdbATkorCT" +
|
||||
"zFOIEPF2S1zgperEP23M01mr4dWVdYN18B32YF67xdJHMbFhp5dkQwv9CmscoWq7OE5HIfOb+JAh7BEZb+CmKhM3yWJvoR/D" +
|
||||
"/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw=="
|
||||
);
|
||||
const decValue = await nodeCryptoFunctionService.rsaDecrypt(
|
||||
data.buffer,
|
||||
privKey.buffer,
|
||||
"sha1"
|
||||
);
|
||||
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsaExtractPublicKey", () => {
|
||||
it("should successfully extract key", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
|
||||
const publicKey = await nodeCryptoFunctionService.rsaExtractPublicKey(privKey.buffer);
|
||||
expect(Utils.fromBufferToB64(publicKey)).toBe(RsaPublicKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsaGenerateKeyPair", () => {
|
||||
testRsaGenerateKeyPair(1024);
|
||||
testRsaGenerateKeyPair(2048);
|
||||
|
||||
// Generating 4096 bit keys is really slow with Forge lib.
|
||||
// Maybe move to something else if we ever want to generate keys of this size.
|
||||
// testRsaGenerateKeyPair(4096);
|
||||
});
|
||||
|
||||
describe("randomBytes", () => {
|
||||
it("should make a value of the correct length", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const randomData = await nodeCryptoFunctionService.randomBytes(16);
|
||||
expect(randomData.byteLength).toBe(16);
|
||||
});
|
||||
|
||||
it("should not make the same value twice", async () => {
|
||||
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const randomData = await nodeCryptoFunctionService.randomBytes(16);
|
||||
const randomData2 = await nodeCryptoFunctionService.randomBytes(16);
|
||||
expect(
|
||||
randomData.byteLength === randomData2.byteLength && randomData !== randomData2
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function testPbkdf2(
|
||||
algorithm: "sha256" | "sha512",
|
||||
regularKey: string,
|
||||
utf8Key: string,
|
||||
unicodeKey: string
|
||||
) {
|
||||
const regularEmail = "user@example.com";
|
||||
const utf8Email = "üser@example.com";
|
||||
|
||||
const regularPassword = "password";
|
||||
const utf8Password = "pǻssword";
|
||||
const unicodePassword = "😀password🙏";
|
||||
|
||||
it("should create valid " + algorithm + " key from regular input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.pbkdf2(regularPassword, regularEmail, algorithm, 5000);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(regularKey);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " key from utf8 input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.pbkdf2(utf8Password, utf8Email, algorithm, 5000);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(utf8Key);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " key from unicode input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.pbkdf2(unicodePassword, regularEmail, algorithm, 5000);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(unicodeKey);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " key from array buffer input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.pbkdf2(
|
||||
Utils.fromUtf8ToArray(regularPassword).buffer,
|
||||
Utils.fromUtf8ToArray(regularEmail).buffer,
|
||||
algorithm,
|
||||
5000
|
||||
);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(regularKey);
|
||||
});
|
||||
}
|
||||
|
||||
function testHkdf(
|
||||
algorithm: "sha256" | "sha512",
|
||||
regularKey: string,
|
||||
utf8Key: string,
|
||||
unicodeKey: string
|
||||
) {
|
||||
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==");
|
||||
|
||||
const regularSalt = "salt";
|
||||
const utf8Salt = "üser_salt";
|
||||
const unicodeSalt = "😀salt🙏";
|
||||
|
||||
const regularInfo = "info";
|
||||
const utf8Info = "üser_info";
|
||||
const unicodeInfo = "😀info🙏";
|
||||
|
||||
it("should create valid " + algorithm + " key from regular input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.hkdf(ikm, regularSalt, regularInfo, 32, algorithm);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(regularKey);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " key from utf8 input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.hkdf(ikm, utf8Salt, utf8Info, 32, algorithm);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(utf8Key);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " key from unicode input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.hkdf(ikm, unicodeSalt, unicodeInfo, 32, algorithm);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(unicodeKey);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " key from array buffer input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const key = await cryptoFunctionService.hkdf(
|
||||
ikm,
|
||||
Utils.fromUtf8ToArray(regularSalt).buffer,
|
||||
Utils.fromUtf8ToArray(regularInfo).buffer,
|
||||
32,
|
||||
algorithm
|
||||
);
|
||||
expect(Utils.fromBufferToB64(key)).toBe(regularKey);
|
||||
});
|
||||
}
|
||||
|
||||
function testHkdfExpand(
|
||||
algorithm: "sha256" | "sha512",
|
||||
b64prk: string,
|
||||
outputByteSize: number,
|
||||
b64ExpectedOkm: string
|
||||
) {
|
||||
const info = "info";
|
||||
|
||||
it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const okm = await cryptoFunctionService.hkdfExpand(
|
||||
Utils.fromB64ToArray(b64prk),
|
||||
info,
|
||||
outputByteSize,
|
||||
algorithm
|
||||
);
|
||||
expect(Utils.fromBufferToB64(okm)).toBe(b64ExpectedOkm);
|
||||
});
|
||||
}
|
||||
|
||||
function testHash(
|
||||
algorithm: "sha1" | "sha256" | "sha512" | "md5",
|
||||
regularHash: string,
|
||||
utf8Hash: string,
|
||||
unicodeHash: string
|
||||
) {
|
||||
const regularValue = "HashMe!!";
|
||||
const utf8Value = "HǻshMe!!";
|
||||
const unicodeValue = "😀HashMe!!!🙏";
|
||||
|
||||
it("should create valid " + algorithm + " hash from regular input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const hash = await cryptoFunctionService.hash(regularValue, algorithm);
|
||||
expect(Utils.fromBufferToHex(hash)).toBe(regularHash);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " hash from utf8 input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const hash = await cryptoFunctionService.hash(utf8Value, algorithm);
|
||||
expect(Utils.fromBufferToHex(hash)).toBe(utf8Hash);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " hash from unicode input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const hash = await cryptoFunctionService.hash(unicodeValue, algorithm);
|
||||
expect(Utils.fromBufferToHex(hash)).toBe(unicodeHash);
|
||||
});
|
||||
|
||||
it("should create valid " + algorithm + " hash from array buffer input", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const hash = await cryptoFunctionService.hash(
|
||||
Utils.fromUtf8ToArray(regularValue).buffer,
|
||||
algorithm
|
||||
);
|
||||
expect(Utils.fromBufferToHex(hash)).toBe(regularHash);
|
||||
});
|
||||
}
|
||||
|
||||
function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string, fast = false) {
|
||||
it("should create valid " + algorithm + " hmac", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const value = Utils.fromUtf8ToArray("SignMe!!").buffer;
|
||||
const key = Utils.fromUtf8ToArray("secretkey").buffer;
|
||||
let computedMac: ArrayBuffer = null;
|
||||
if (fast) {
|
||||
computedMac = await cryptoFunctionService.hmacFast(value, key, algorithm);
|
||||
} else {
|
||||
computedMac = await cryptoFunctionService.hmac(value, key, algorithm);
|
||||
}
|
||||
expect(Utils.fromBufferToHex(computedMac)).toBe(mac);
|
||||
});
|
||||
}
|
||||
|
||||
function testCompare(fast = false) {
|
||||
it("should successfully compare two of the same values", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const equal = fast
|
||||
? await cryptoFunctionService.compareFast(a.buffer, a.buffer)
|
||||
: await cryptoFunctionService.compare(a.buffer, a.buffer);
|
||||
expect(equal).toBe(true);
|
||||
});
|
||||
|
||||
it("should successfully compare two different values of the same length", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
b[1] = 4;
|
||||
const equal = fast
|
||||
? await cryptoFunctionService.compareFast(a.buffer, b.buffer)
|
||||
: await cryptoFunctionService.compare(a.buffer, b.buffer);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
|
||||
it("should successfully compare two different values of different lengths", async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const a = new Uint8Array(2);
|
||||
a[0] = 1;
|
||||
a[1] = 2;
|
||||
const b = new Uint8Array(2);
|
||||
b[0] = 3;
|
||||
const equal = fast
|
||||
? await cryptoFunctionService.compareFast(a.buffer, b.buffer)
|
||||
: await cryptoFunctionService.compare(a.buffer, b.buffer);
|
||||
expect(equal).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
function testRsaGenerateKeyPair(length: 1024 | 2048 | 4096) {
|
||||
it(
|
||||
"should successfully generate a " + length + " bit key pair",
|
||||
async () => {
|
||||
const cryptoFunctionService = new NodeCryptoFunctionService();
|
||||
const keyPair = await cryptoFunctionService.rsaGenerateKeyPair(length);
|
||||
expect(keyPair[0] == null || keyPair[1] == null).toBe(false);
|
||||
const publicKey = await cryptoFunctionService.rsaExtractPublicKey(keyPair[1]);
|
||||
expect(Utils.fromBufferToB64(keyPair[0])).toBe(Utils.fromBufferToB64(publicKey));
|
||||
},
|
||||
30000
|
||||
);
|
||||
}
|
||||
|
||||
function makeStaticByteArray(length: number) {
|
||||
const arr = new Uint8Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
arr[i] = i;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
0
libs/node/spec/test.ts
Normal file
0
libs/node/spec/test.ts
Normal file
113
libs/node/src/cli/baseProgram.ts
Normal file
113
libs/node/src/cli/baseProgram.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import * as chalk from "chalk";
|
||||
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
import { Response } from "./models/response";
|
||||
import { ListResponse } from "./models/response/listResponse";
|
||||
import { MessageResponse } from "./models/response/messageResponse";
|
||||
import { StringResponse } from "./models/response/stringResponse";
|
||||
|
||||
export abstract class BaseProgram {
|
||||
constructor(
|
||||
protected stateService: StateService,
|
||||
private writeLn: (s: string, finalLine: boolean, error: boolean) => void
|
||||
) {}
|
||||
|
||||
protected processResponse(
|
||||
response: Response,
|
||||
exitImmediately = false,
|
||||
dataProcessor: () => string = null
|
||||
) {
|
||||
if (!response.success) {
|
||||
if (process.env.BW_QUIET !== "true") {
|
||||
if (process.env.BW_RESPONSE === "true") {
|
||||
this.writeLn(this.getJson(response), true, false);
|
||||
} else {
|
||||
this.writeLn(chalk.redBright(response.message), true, true);
|
||||
}
|
||||
}
|
||||
const exitCode = process.env.BW_CLEANEXIT ? 0 : 1;
|
||||
if (exitImmediately) {
|
||||
process.exit(exitCode);
|
||||
} else {
|
||||
process.exitCode = exitCode;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.BW_RESPONSE === "true") {
|
||||
this.writeLn(this.getJson(response), true, false);
|
||||
} else if (response.data != null) {
|
||||
let out: string = dataProcessor != null ? dataProcessor() : null;
|
||||
if (out == null) {
|
||||
if (response.data.object === "string") {
|
||||
const data = (response.data as StringResponse).data;
|
||||
if (data != null) {
|
||||
out = data;
|
||||
}
|
||||
} else if (response.data.object === "list") {
|
||||
out = this.getJson((response.data as ListResponse).data);
|
||||
} else if (response.data.object === "message") {
|
||||
out = this.getMessage(response);
|
||||
} else {
|
||||
out = this.getJson(response.data);
|
||||
}
|
||||
}
|
||||
|
||||
if (out != null && process.env.BW_QUIET !== "true") {
|
||||
this.writeLn(out, true, false);
|
||||
}
|
||||
}
|
||||
if (exitImmediately) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exitCode = 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected getJson(obj: any): string {
|
||||
if (process.env.BW_PRETTY === "true") {
|
||||
return JSON.stringify(obj, null, " ");
|
||||
} else {
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
}
|
||||
|
||||
protected getMessage(response: Response): string {
|
||||
const message = response.data as MessageResponse;
|
||||
if (process.env.BW_RAW === "true") {
|
||||
return message.raw;
|
||||
}
|
||||
|
||||
let out = "";
|
||||
if (message.title != null) {
|
||||
if (message.noColor) {
|
||||
out = message.title;
|
||||
} else {
|
||||
out = chalk.greenBright(message.title);
|
||||
}
|
||||
}
|
||||
if (message.message != null) {
|
||||
if (message.title != null) {
|
||||
out += "\n";
|
||||
}
|
||||
out += message.message;
|
||||
}
|
||||
return out.trim() === "" ? null : out;
|
||||
}
|
||||
|
||||
protected async exitIfAuthed() {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (authed) {
|
||||
const email = await this.stateService.getEmail();
|
||||
this.processResponse(Response.error("You are already logged in as " + email + "."), true);
|
||||
}
|
||||
}
|
||||
|
||||
protected async exitIfNotAuthed() {
|
||||
const authed = await this.stateService.getIsAuthenticated();
|
||||
if (!authed) {
|
||||
this.processResponse(Response.error("You are not logged in."), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
624
libs/node/src/cli/commands/login.command.ts
Normal file
624
libs/node/src/cli/commands/login.command.ts
Normal file
@@ -0,0 +1,624 @@
|
||||
import * as http from "http";
|
||||
|
||||
import * as program from "commander";
|
||||
import * as inquirer from "inquirer";
|
||||
import Separator from "inquirer/lib/objects/separator";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
||||
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "jslib-common/models/domain/logInCredentials";
|
||||
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequestTwoFactor";
|
||||
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
|
||||
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/messageResponse";
|
||||
|
||||
export class LoginCommand {
|
||||
protected validatedParams: () => Promise<any>;
|
||||
protected success: () => Promise<MessageResponse>;
|
||||
protected logout: () => Promise<void>;
|
||||
protected canInteract: boolean;
|
||||
protected clientId: string;
|
||||
protected clientSecret: string;
|
||||
protected email: string;
|
||||
|
||||
private ssoRedirectUri: string = null;
|
||||
|
||||
constructor(
|
||||
protected authService: AuthService,
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationService,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected stateService: StateService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected policyService: PolicyService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
clientId: string
|
||||
) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
async run(email: string, password: string, options: program.OptionValues) {
|
||||
this.canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||
|
||||
let ssoCodeVerifier: string = null;
|
||||
let ssoCode: string = null;
|
||||
let orgIdentifier: string = null;
|
||||
|
||||
let clientId: string = null;
|
||||
let clientSecret: string = null;
|
||||
|
||||
let selectedProvider: any = null;
|
||||
|
||||
if (options.apikey != null) {
|
||||
const apiIdentifiers = await this.apiIdentifiers();
|
||||
clientId = apiIdentifiers.clientId;
|
||||
clientSecret = apiIdentifiers.clientSecret;
|
||||
} else if (options.sso != null && this.canInteract) {
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
try {
|
||||
const ssoParams = await this.openSsoPrompt(codeChallenge, state);
|
||||
ssoCode = ssoParams.ssoCode;
|
||||
orgIdentifier = ssoParams.orgIdentifier;
|
||||
} catch {
|
||||
return Response.badRequest("Something went wrong. Try again.");
|
||||
}
|
||||
} else {
|
||||
if ((email == null || email === "") && this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "email",
|
||||
message: "Email address:",
|
||||
});
|
||||
email = answer.email;
|
||||
}
|
||||
if (email == null || email.trim() === "") {
|
||||
return Response.badRequest("Email address is required.");
|
||||
}
|
||||
if (email.indexOf("@") === -1) {
|
||||
return Response.badRequest("Email address is invalid.");
|
||||
}
|
||||
this.email = email;
|
||||
|
||||
if (password == null || password === "") {
|
||||
if (options.passwordfile) {
|
||||
password = await NodeUtils.readFirstLine(options.passwordfile);
|
||||
} else if (options.passwordenv && process.env[options.passwordenv]) {
|
||||
password = process.env[options.passwordenv];
|
||||
} else if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: "Master password:",
|
||||
});
|
||||
password = answer.password;
|
||||
}
|
||||
}
|
||||
|
||||
if (password == null || password === "") {
|
||||
return Response.badRequest("Master password is required.");
|
||||
}
|
||||
}
|
||||
|
||||
let twoFactorToken: string = options.code;
|
||||
let twoFactorMethod: TwoFactorProviderType = null;
|
||||
try {
|
||||
if (options.method != null) {
|
||||
twoFactorMethod = parseInt(options.method, null);
|
||||
}
|
||||
} catch (e) {
|
||||
return Response.error("Invalid two-step login method.");
|
||||
}
|
||||
|
||||
const twoFactor =
|
||||
twoFactorToken == null
|
||||
? null
|
||||
: new TokenRequestTwoFactor(twoFactorMethod, twoFactorToken, false);
|
||||
|
||||
try {
|
||||
if (this.validatedParams != null) {
|
||||
await this.validatedParams();
|
||||
}
|
||||
|
||||
let response: AuthResult = null;
|
||||
if (clientId != null && clientSecret != null) {
|
||||
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
|
||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.authService.logIn(
|
||||
new SsoLogInCredentials(
|
||||
ssoCode,
|
||||
ssoCodeVerifier,
|
||||
this.ssoRedirectUri,
|
||||
orgIdentifier,
|
||||
twoFactor
|
||||
)
|
||||
);
|
||||
} else {
|
||||
response = await this.authService.logIn(
|
||||
new PasswordLogInCredentials(email, password, null, twoFactor)
|
||||
);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const credentials = new PasswordLogInCredentials(email, password);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
||||
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
if (response.requiresTwoFactor) {
|
||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
return Response.badRequest("No providers available for this client.");
|
||||
}
|
||||
|
||||
if (twoFactorMethod != null) {
|
||||
try {
|
||||
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
|
||||
} catch (e) {
|
||||
return Response.error("Invalid two-step login method.");
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedProvider == null) {
|
||||
if (twoFactorProviders.length === 1) {
|
||||
selectedProvider = twoFactorProviders[0];
|
||||
} else if (this.canInteract) {
|
||||
const twoFactorOptions: (string | Separator)[] = twoFactorProviders.map((p) => p.name);
|
||||
twoFactorOptions.push(new inquirer.Separator());
|
||||
twoFactorOptions.push("Cancel");
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "list",
|
||||
name: "method",
|
||||
message: "Two-step login method:",
|
||||
choices: twoFactorOptions,
|
||||
});
|
||||
const i = twoFactorOptions.indexOf(answer.method);
|
||||
if (i === twoFactorOptions.length - 1) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
selectedProvider = twoFactorProviders[i];
|
||||
}
|
||||
if (selectedProvider == null) {
|
||||
return Response.error("Login failed. No provider selected.");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
twoFactorToken == null &&
|
||||
response.twoFactorProviders.size > 1 &&
|
||||
selectedProvider.type === TwoFactorProviderType.Email
|
||||
) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = this.authService.email;
|
||||
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
|
||||
await this.apiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
|
||||
if (twoFactorToken == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "token",
|
||||
message: "Two-step login code:",
|
||||
});
|
||||
twoFactorToken = answer.token;
|
||||
}
|
||||
if (twoFactorToken == null || twoFactorToken === "") {
|
||||
return Response.badRequest("Code is required.");
|
||||
}
|
||||
}
|
||||
|
||||
response = await this.authService.logInTwoFactor(
|
||||
new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (response.captchaSiteKey) {
|
||||
const twoFactorRequest = new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken);
|
||||
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
|
||||
|
||||
// Error Response
|
||||
if (handledResponse instanceof Response) {
|
||||
return handledResponse;
|
||||
} else {
|
||||
response = handledResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.requiresTwoFactor) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
|
||||
if (response.resetMasterPassword) {
|
||||
return Response.error(
|
||||
"In order to log in with SSO from the CLI, you must first log in" +
|
||||
" through the web vault to set your master password."
|
||||
);
|
||||
}
|
||||
|
||||
// Handle Updating Temp Password if NOT using an API Key for authentication
|
||||
if (response.forcePasswordReset && clientId == null && clientSecret == null) {
|
||||
return await this.updateTempPassword();
|
||||
}
|
||||
|
||||
return await this.handleSuccessResponse();
|
||||
} catch (e) {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSuccessResponse(): Promise<Response> {
|
||||
if (this.success != null) {
|
||||
const res = await this.success();
|
||||
return Response.success(res);
|
||||
} else {
|
||||
const res = new MessageResponse("You are logged in!", null);
|
||||
return Response.success(res);
|
||||
}
|
||||
}
|
||||
|
||||
private async updateTempPassword(error?: string): Promise<Response> {
|
||||
// If no interaction available, alert user to use web vault
|
||||
if (!this.canInteract) {
|
||||
await this.logout();
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
return Response.error(
|
||||
new MessageResponse(
|
||||
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now via the web vault. You have been logged out.",
|
||||
null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.email == null || this.email === "undefined") {
|
||||
this.email = await this.stateService.getEmail();
|
||||
}
|
||||
|
||||
// Get New Master Password
|
||||
const baseMessage =
|
||||
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.\n" +
|
||||
"Master password: ";
|
||||
const firstMessage = error != null ? error + baseMessage : baseMessage;
|
||||
const mp: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: firstMessage,
|
||||
});
|
||||
const masterPassword = mp.password;
|
||||
|
||||
// Master Password Validation
|
||||
if (masterPassword == null || masterPassword === "") {
|
||||
return this.updateTempPassword("Master password is required.\n");
|
||||
}
|
||||
|
||||
if (masterPassword.length < 8) {
|
||||
return this.updateTempPassword("Master password must be at least 8 characters long.\n");
|
||||
}
|
||||
|
||||
// Strength & Policy Validation
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
||||
masterPassword,
|
||||
this.getPasswordStrengthUserInput()
|
||||
);
|
||||
|
||||
// Get New Master Password Re-type
|
||||
const reTypeMessage = "Re-type New Master password (Strength: " + strengthResult.score + ")";
|
||||
const retype: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: "password",
|
||||
name: "password",
|
||||
message: reTypeMessage,
|
||||
});
|
||||
const masterPasswordRetype = retype.password;
|
||||
|
||||
// Re-type Validation
|
||||
if (masterPassword !== masterPasswordRetype) {
|
||||
return this.updateTempPassword("Master password confirmation does not match.\n");
|
||||
}
|
||||
|
||||
// Get Hint (optional)
|
||||
const hint: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
||||
type: "input",
|
||||
name: "input",
|
||||
message: "Master Password Hint (optional):",
|
||||
});
|
||||
const masterPasswordHint = hint.input;
|
||||
|
||||
// Retrieve details for key generation
|
||||
const enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
|
||||
const kdf = await this.stateService.getKdfType();
|
||||
const kdfIterations = await this.stateService.getKdfIterations();
|
||||
|
||||
if (
|
||||
enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
masterPassword,
|
||||
enforcedPolicyOptions
|
||||
)
|
||||
) {
|
||||
return this.updateTempPassword(
|
||||
"Your new master password does not meet the policy requirements.\n"
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Create new key and hash new password
|
||||
const newKey = await this.cryptoService.makeKey(
|
||||
masterPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
kdf,
|
||||
kdfIterations
|
||||
);
|
||||
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey);
|
||||
|
||||
// Grab user's current enc key
|
||||
const userEncKey = await this.cryptoService.getEncKey();
|
||||
|
||||
// Create new encKey for the User
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
|
||||
|
||||
// Create request
|
||||
const request = new UpdateTempPasswordRequest();
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
request.newMasterPasswordHash = newPasswordHash;
|
||||
request.masterPasswordHint = masterPasswordHint;
|
||||
|
||||
// Update user's password
|
||||
await this.apiService.putUpdateTempPassword(request);
|
||||
return this.handleSuccessResponse();
|
||||
} catch (e) {
|
||||
await this.logout();
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCaptchaRequired(
|
||||
twoFactorRequest: TokenRequestTwoFactor,
|
||||
credentials: PasswordLogInCredentials = null
|
||||
): Promise<AuthResult | Response> {
|
||||
const badCaptcha = Response.badRequest(
|
||||
"Your authentication request has been flagged and will require user interaction to proceed.\n" +
|
||||
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
|
||||
"(https://bitwarden.com/help/cli-auth-challenges)"
|
||||
);
|
||||
|
||||
try {
|
||||
const captchaClientSecret = await this.apiClientSecret(true);
|
||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
||||
return badCaptcha;
|
||||
}
|
||||
|
||||
let authResultResponse: AuthResult = null;
|
||||
if (credentials != null) {
|
||||
credentials.captchaToken = captchaClientSecret;
|
||||
credentials.twoFactor = twoFactorRequest;
|
||||
authResultResponse = await this.authService.logIn(credentials);
|
||||
} else {
|
||||
authResultResponse = await this.authService.logInTwoFactor(
|
||||
twoFactorRequest,
|
||||
captchaClientSecret
|
||||
);
|
||||
}
|
||||
|
||||
return authResultResponse;
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof ErrorResponse ||
|
||||
(e.constructor.name === ErrorResponse.name &&
|
||||
(e as ErrorResponse).message.includes("Captcha is invalid"))
|
||||
) {
|
||||
return badCaptcha;
|
||||
} else {
|
||||
return Response.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf("@");
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(
|
||||
this.email
|
||||
.substr(0, atPosition)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split(/[^A-Za-z0-9]/)
|
||||
);
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
|
||||
private async apiClientId(): Promise<string> {
|
||||
let clientId: string = null;
|
||||
|
||||
const storedClientId: string = process.env.BW_CLIENTID;
|
||||
if (storedClientId == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "clientId",
|
||||
message: "client_id:",
|
||||
});
|
||||
clientId = answer.clientId;
|
||||
} else {
|
||||
clientId = null;
|
||||
}
|
||||
} else {
|
||||
clientId = storedClientId;
|
||||
}
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
private async apiClientSecret(isAdditionalAuthentication = false): Promise<string> {
|
||||
const additionalAuthenticationMessage = "Additional authentication required.\nAPI key ";
|
||||
let clientSecret: string = null;
|
||||
|
||||
const storedClientSecret: string = this.clientSecret || process.env.BW_CLIENTSECRET;
|
||||
if (this.canInteract && storedClientSecret == null) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "clientSecret",
|
||||
message:
|
||||
(isAdditionalAuthentication ? additionalAuthenticationMessage : "") + "client_secret:",
|
||||
});
|
||||
clientSecret = answer.clientSecret;
|
||||
} else {
|
||||
clientSecret = storedClientSecret;
|
||||
}
|
||||
|
||||
return clientSecret;
|
||||
}
|
||||
|
||||
private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> {
|
||||
return {
|
||||
clientId: await this.apiClientId(),
|
||||
clientSecret: await this.apiClientSecret(),
|
||||
};
|
||||
}
|
||||
|
||||
private async openSsoPrompt(
|
||||
codeChallenge: string,
|
||||
state: string
|
||||
): Promise<{ ssoCode: string; orgIdentifier: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const callbackServer = http.createServer((req, res) => {
|
||||
const urlString = "http://localhost" + req.url;
|
||||
const url = new URL(urlString);
|
||||
const code = url.searchParams.get("code");
|
||||
const receivedState = url.searchParams.get("state");
|
||||
const orgIdentifier = this.getOrgIdentifierFromState(receivedState);
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
if (code != null && receivedState != null && this.checkState(receivedState, state)) {
|
||||
res.writeHead(200);
|
||||
res.end(
|
||||
"<html><head><title>Success | Bitwarden CLI</title></head><body>" +
|
||||
"<h1>Successfully authenticated with the Bitwarden CLI</h1>" +
|
||||
"<p>You may now close this tab and return to the terminal.</p>" +
|
||||
"</body></html>"
|
||||
);
|
||||
callbackServer.close(() =>
|
||||
resolve({
|
||||
ssoCode: code,
|
||||
orgIdentifier: orgIdentifier,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
res.writeHead(400);
|
||||
res.end(
|
||||
"<html><head><title>Failed | Bitwarden CLI</title></head><body>" +
|
||||
"<h1>Something went wrong logging into the Bitwarden CLI</h1>" +
|
||||
"<p>You may now close this tab and return to the terminal.</p>" +
|
||||
"</body></html>"
|
||||
);
|
||||
callbackServer.close(() => reject());
|
||||
}
|
||||
});
|
||||
let foundPort = false;
|
||||
const webUrl = this.environmentService.getWebVaultUrl();
|
||||
for (let port = 8065; port <= 8070; port++) {
|
||||
try {
|
||||
this.ssoRedirectUri = "http://localhost:" + port;
|
||||
callbackServer.listen(port, () => {
|
||||
this.platformUtilsService.launchUri(
|
||||
webUrl +
|
||||
"/#/sso?clientId=" +
|
||||
this.clientId +
|
||||
"&redirectUri=" +
|
||||
encodeURIComponent(this.ssoRedirectUri) +
|
||||
"&state=" +
|
||||
state +
|
||||
"&codeChallenge=" +
|
||||
codeChallenge
|
||||
);
|
||||
});
|
||||
foundPort = true;
|
||||
break;
|
||||
} catch {
|
||||
// Ignore error since we run the same command up to 5 times.
|
||||
}
|
||||
}
|
||||
if (!foundPort) {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getOrgIdentifierFromState(state: string): string {
|
||||
if (state === null || state === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stateSplit = state.split("_identifier=");
|
||||
return stateSplit.length > 1 ? stateSplit[1] : null;
|
||||
}
|
||||
|
||||
private checkState(state: string, checkState: string): boolean {
|
||||
if (state === null || state === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (checkState === null || checkState === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stateSplit = state.split("_identifier=");
|
||||
const checkStateSplit = checkState.split("_identifier=");
|
||||
return stateSplit[0] === checkStateSplit[0];
|
||||
}
|
||||
}
|
||||
22
libs/node/src/cli/commands/logout.command.ts
Normal file
22
libs/node/src/cli/commands/logout.command.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/messageResponse";
|
||||
|
||||
export class LogoutCommand {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private i18nService: I18nService,
|
||||
private logoutCallback: () => Promise<void>
|
||||
) {}
|
||||
|
||||
async run() {
|
||||
await this.logoutCallback();
|
||||
this.authService.logOut(() => {
|
||||
/* Do nothing */
|
||||
});
|
||||
const res = new MessageResponse("You have logged out.", null);
|
||||
return Response.success(res);
|
||||
}
|
||||
}
|
||||
104
libs/node/src/cli/commands/update.command.ts
Normal file
104
libs/node/src/cli/commands/update.command.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as fetch from "node-fetch";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { MessageResponse } from "../models/response/messageResponse";
|
||||
|
||||
export class UpdateCommand {
|
||||
inPkg = false;
|
||||
|
||||
constructor(
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private repoName: string,
|
||||
private executableName: string,
|
||||
private showExtendedMessage: boolean
|
||||
) {
|
||||
this.inPkg = !!(process as any).pkg;
|
||||
}
|
||||
|
||||
async run(): Promise<Response> {
|
||||
const currentVersion = await this.platformUtilsService.getApplicationVersion();
|
||||
|
||||
const response = await fetch.default(
|
||||
"https://api.github.com/repos/bitwarden/" + this.repoName + "/releases/latest"
|
||||
);
|
||||
if (response.status === 200) {
|
||||
const responseJson = await response.json();
|
||||
const res = new MessageResponse(null, null);
|
||||
|
||||
const tagName: string = responseJson.tag_name;
|
||||
if (tagName === "v" + currentVersion) {
|
||||
res.title = "No update available.";
|
||||
res.noColor = true;
|
||||
return Response.success(res);
|
||||
}
|
||||
|
||||
let downloadUrl: string = null;
|
||||
if (responseJson.assets != null) {
|
||||
for (const a of responseJson.assets) {
|
||||
const download: string = a.browser_download_url;
|
||||
if (download == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (download.indexOf(".zip") === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
download.indexOf(this.executableName + "-windows") > -1
|
||||
) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
} else if (
|
||||
process.platform === "darwin" &&
|
||||
download.indexOf(this.executableName + "-macos") > -1
|
||||
) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
} else if (
|
||||
process.platform === "linux" &&
|
||||
download.indexOf(this.executableName + "-linux") > -1
|
||||
) {
|
||||
downloadUrl = download;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.title = "A new version is available: " + tagName;
|
||||
if (downloadUrl == null) {
|
||||
downloadUrl = "https://github.com/bitwarden/" + this.repoName + "/releases";
|
||||
} else {
|
||||
res.raw = downloadUrl;
|
||||
}
|
||||
res.message = "";
|
||||
if (responseJson.body != null && responseJson.body !== "") {
|
||||
res.message = responseJson.body + "\n\n";
|
||||
}
|
||||
|
||||
res.message += "You can download this update at " + downloadUrl;
|
||||
|
||||
if (this.showExtendedMessage) {
|
||||
if (this.inPkg) {
|
||||
res.message +=
|
||||
"\n\nIf you installed this CLI through a package manager " +
|
||||
"you should probably update using its update command instead.";
|
||||
} else {
|
||||
res.message +=
|
||||
"\n\nIf you installed this CLI through NPM " +
|
||||
"you should update using `npm install -g @bitwarden/" +
|
||||
this.repoName +
|
||||
"`";
|
||||
}
|
||||
}
|
||||
return Response.success(res);
|
||||
} else {
|
||||
return Response.error("Error contacting update API: " + response.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
libs/node/src/cli/models/response.ts
Normal file
50
libs/node/src/cli/models/response.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BaseResponse } from "./response/baseResponse";
|
||||
|
||||
export class Response {
|
||||
static error(error: any, data?: any): Response {
|
||||
const res = new Response();
|
||||
res.success = false;
|
||||
if (typeof error === "string") {
|
||||
res.message = error;
|
||||
} else {
|
||||
res.message =
|
||||
error.message != null
|
||||
? error.message
|
||||
: error.toString() === "[object Object]"
|
||||
? JSON.stringify(error)
|
||||
: error.toString();
|
||||
}
|
||||
res.data = data;
|
||||
return res;
|
||||
}
|
||||
|
||||
static notFound(): Response {
|
||||
return Response.error("Not found.");
|
||||
}
|
||||
|
||||
static badRequest(message: string): Response {
|
||||
return Response.error(message);
|
||||
}
|
||||
|
||||
static multipleResults(ids: string[]): Response {
|
||||
let msg =
|
||||
"More than one result was found. Try getting a specific object by `id` instead. " +
|
||||
"The following objects were found:";
|
||||
ids.forEach((id) => {
|
||||
msg += "\n" + id;
|
||||
});
|
||||
return Response.error(msg, ids);
|
||||
}
|
||||
|
||||
static success(data?: BaseResponse): Response {
|
||||
const res = new Response();
|
||||
res.success = true;
|
||||
res.data = data;
|
||||
return res;
|
||||
}
|
||||
|
||||
success: boolean;
|
||||
message: string;
|
||||
errorCode: number;
|
||||
data: BaseResponse;
|
||||
}
|
||||
3
libs/node/src/cli/models/response/baseResponse.ts
Normal file
3
libs/node/src/cli/models/response/baseResponse.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface BaseResponse {
|
||||
object: string;
|
||||
}
|
||||
13
libs/node/src/cli/models/response/fileResponse.ts
Normal file
13
libs/node/src/cli/models/response/fileResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class FileResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: Buffer;
|
||||
fileName: string;
|
||||
|
||||
constructor(data: Buffer, fileName: string) {
|
||||
this.object = "file";
|
||||
this.data = data;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
}
|
||||
11
libs/node/src/cli/models/response/listResponse.ts
Normal file
11
libs/node/src/cli/models/response/listResponse.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class ListResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: BaseResponse[];
|
||||
|
||||
constructor(data: BaseResponse[]) {
|
||||
this.object = "list";
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
15
libs/node/src/cli/models/response/messageResponse.ts
Normal file
15
libs/node/src/cli/models/response/messageResponse.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class MessageResponse implements BaseResponse {
|
||||
object: string;
|
||||
title: string;
|
||||
message: string;
|
||||
raw: string;
|
||||
noColor = false;
|
||||
|
||||
constructor(title: string, message: string) {
|
||||
this.object = "message";
|
||||
this.title = title;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
11
libs/node/src/cli/models/response/stringResponse.ts
Normal file
11
libs/node/src/cli/models/response/stringResponse.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseResponse } from "./baseResponse";
|
||||
|
||||
export class StringResponse implements BaseResponse {
|
||||
object: string;
|
||||
data: string;
|
||||
|
||||
constructor(data: string) {
|
||||
this.object = "string";
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
166
libs/node/src/cli/services/cliPlatformUtils.service.ts
Normal file
166
libs/node/src/cli/services/cliPlatformUtils.service.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as child_process from "child_process";
|
||||
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { ClientType } from "jslib-common/enums/clientType";
|
||||
import { DeviceType } from "jslib-common/enums/deviceType";
|
||||
import { ThemeType } from "jslib-common/enums/themeType";
|
||||
|
||||
// eslint-disable-next-line
|
||||
const open = require("open");
|
||||
|
||||
export class CliPlatformUtilsService implements PlatformUtilsService {
|
||||
clientType: ClientType;
|
||||
|
||||
private deviceCache: DeviceType = null;
|
||||
|
||||
constructor(clientType: ClientType, private packageJson: any) {
|
||||
this.clientType = clientType;
|
||||
}
|
||||
|
||||
getDevice(): DeviceType {
|
||||
if (!this.deviceCache) {
|
||||
switch (process.platform) {
|
||||
case "win32":
|
||||
this.deviceCache = DeviceType.WindowsDesktop;
|
||||
break;
|
||||
case "darwin":
|
||||
this.deviceCache = DeviceType.MacOsDesktop;
|
||||
break;
|
||||
case "linux":
|
||||
default:
|
||||
this.deviceCache = DeviceType.LinuxDesktop;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.deviceCache;
|
||||
}
|
||||
|
||||
getDeviceString(): string {
|
||||
const device = DeviceType[this.getDevice()].toLowerCase();
|
||||
return device.replace("desktop", "");
|
||||
}
|
||||
|
||||
getClientType() {
|
||||
return this.clientType;
|
||||
}
|
||||
|
||||
isFirefox() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isChrome() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isEdge() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isOpera() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isVivaldi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isSafari() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isMacAppStore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isViewOpen() {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
launchUri(uri: string, options?: any): void {
|
||||
if (process.platform === "linux") {
|
||||
child_process.spawnSync("xdg-open", [uri]);
|
||||
} else {
|
||||
open(uri);
|
||||
}
|
||||
}
|
||||
|
||||
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
getApplicationVersion(): Promise<string> {
|
||||
return Promise.resolve(this.packageJson.version);
|
||||
}
|
||||
|
||||
getApplicationVersionSync(): string {
|
||||
return this.packageJson.version;
|
||||
}
|
||||
|
||||
supportsWebAuthn(win: Window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
supportsDuo(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
showToast(
|
||||
type: "error" | "success" | "warning" | "info",
|
||||
title: string,
|
||||
text: string | string[],
|
||||
options?: any
|
||||
): void {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
showDialog(
|
||||
text: string,
|
||||
title?: string,
|
||||
confirmText?: string,
|
||||
cancelText?: string,
|
||||
type?: string
|
||||
): Promise<boolean> {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
isDev(): boolean {
|
||||
return process.env.BWCLI_ENV === "development";
|
||||
}
|
||||
|
||||
isSelfHost(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
copyToClipboard(text: string, options?: any): void {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
readFromClipboard(options?: any): Promise<string> {
|
||||
throw new Error("Not implemented.");
|
||||
}
|
||||
|
||||
supportsBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
authenticateBiometric(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
getDefaultSystemTheme() {
|
||||
return Promise.resolve(ThemeType.Light as ThemeType.Light | ThemeType.Dark);
|
||||
}
|
||||
|
||||
onDefaultSystemThemeChange() {
|
||||
/* noop */
|
||||
}
|
||||
|
||||
getEffectiveTheme() {
|
||||
return Promise.resolve(ThemeType.Light);
|
||||
}
|
||||
|
||||
supportsSecureStorage(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
22
libs/node/src/cli/services/consoleLog.service.ts
Normal file
22
libs/node/src/cli/services/consoleLog.service.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { LogLevelType } from "jslib-common/enums/logLevelType";
|
||||
import { ConsoleLogService as BaseConsoleLogService } from "jslib-common/services/consoleLog.service";
|
||||
|
||||
export class ConsoleLogService extends BaseConsoleLogService {
|
||||
constructor(isDev: boolean, filter: (level: LogLevelType) => boolean = null) {
|
||||
super(isDev, filter);
|
||||
}
|
||||
|
||||
write(level: LogLevelType, message: string) {
|
||||
if (this.filter != null && this.filter(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.BW_RESPONSE === "true") {
|
||||
// eslint-disable-next-line
|
||||
console.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
super.write(level, message);
|
||||
}
|
||||
}
|
||||
0
libs/node/src/globals.d.ts
vendored
Normal file
0
libs/node/src/globals.d.ts
vendored
Normal file
148
libs/node/src/services/lowdbStorage.service.ts
Normal file
148
libs/node/src/services/lowdbStorage.service.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import * as lowdb from "lowdb";
|
||||
import * as FileSync from "lowdb/adapters/FileSync";
|
||||
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { StorageService } from "jslib-common/abstractions/storage.service";
|
||||
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
||||
import { sequentialize } from "jslib-common/misc/sequentialize";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
export class LowdbStorageService implements StorageService {
|
||||
protected dataFilePath: string;
|
||||
private db: lowdb.LowdbSync<any>;
|
||||
private defaults: any;
|
||||
private ready = false;
|
||||
|
||||
constructor(
|
||||
protected logService: LogService,
|
||||
defaults?: any,
|
||||
private dir?: string,
|
||||
private allowCache = false
|
||||
) {
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
@sequentialize(() => "lowdbStorageInit")
|
||||
async init() {
|
||||
if (this.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info("Initializing lowdb storage service.");
|
||||
let adapter: lowdb.AdapterSync<any>;
|
||||
if (Utils.isNode && this.dir != null) {
|
||||
if (!fs.existsSync(this.dir)) {
|
||||
this.logService.warning(`Could not find dir, "${this.dir}"; creating it instead.`);
|
||||
NodeUtils.mkdirpSync(this.dir, "700");
|
||||
this.logService.info(`Created dir "${this.dir}".`);
|
||||
}
|
||||
this.dataFilePath = path.join(this.dir, "data.json");
|
||||
if (!fs.existsSync(this.dataFilePath)) {
|
||||
this.logService.warning(
|
||||
`Could not find data file, "${this.dataFilePath}"; creating it instead.`
|
||||
);
|
||||
fs.writeFileSync(this.dataFilePath, "", { mode: 0o600 });
|
||||
fs.chmodSync(this.dataFilePath, 0o600);
|
||||
this.logService.info(`Created data file "${this.dataFilePath}" with chmod 600.`);
|
||||
} else {
|
||||
this.logService.info(`db file "${this.dataFilePath} already exists"; using existing db`);
|
||||
}
|
||||
await this.lockDbFile(() => {
|
||||
adapter = new FileSync(this.dataFilePath);
|
||||
});
|
||||
}
|
||||
try {
|
||||
this.logService.info("Attempting to create lowdb storage adapter.");
|
||||
this.db = lowdb(adapter);
|
||||
this.logService.info("Successfully created lowdb storage adapter.");
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
this.logService.warning(
|
||||
`Error creating lowdb storage adapter, "${e.message}"; emptying data file.`
|
||||
);
|
||||
if (fs.existsSync(this.dataFilePath)) {
|
||||
const backupPath = this.dataFilePath + ".bak";
|
||||
this.logService.warning(`Writing backup of data file to ${backupPath}`);
|
||||
await fs.copyFile(this.dataFilePath, backupPath, () => {
|
||||
this.logService.warning(
|
||||
`Error while creating data file backup, "${e.message}". No backup may have been created.`
|
||||
);
|
||||
});
|
||||
}
|
||||
adapter.write({});
|
||||
this.db = lowdb(adapter);
|
||||
} else {
|
||||
this.logService.error(`Error creating lowdb storage adapter, "${e.message}".`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.defaults != null) {
|
||||
this.lockDbFile(() => {
|
||||
this.logService.info("Writing defaults.");
|
||||
this.readForNoCache();
|
||||
this.db.defaults(this.defaults).write();
|
||||
this.logService.info("Successfully wrote defaults to db.");
|
||||
});
|
||||
}
|
||||
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
async get<T>(key: string): Promise<T> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
const val = this.db.get(key).value();
|
||||
this.logService.debug(`Successfully read ${key} from db`);
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
return val as T;
|
||||
});
|
||||
}
|
||||
|
||||
has(key: string): Promise<boolean> {
|
||||
return this.get(key).then((v) => v != null);
|
||||
}
|
||||
|
||||
async save(key: string, obj: any): Promise<any> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.set(key, obj).write();
|
||||
this.logService.debug(`Successfully wrote ${key} to db`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<any> {
|
||||
await this.waitForReady();
|
||||
return this.lockDbFile(() => {
|
||||
this.readForNoCache();
|
||||
this.db.unset(key).write();
|
||||
this.logService.debug(`Successfully removed ${key} from db`);
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
protected async lockDbFile<T>(action: () => T): Promise<T> {
|
||||
// Lock methods implemented in clients
|
||||
return Promise.resolve(action());
|
||||
}
|
||||
|
||||
private readForNoCache() {
|
||||
if (!this.allowCache) {
|
||||
this.db.read();
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForReady() {
|
||||
if (!this.ready) {
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
43
libs/node/src/services/nodeApi.service.ts
Normal file
43
libs/node/src/services/nodeApi.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as FormData from "form-data";
|
||||
import { HttpsProxyAgent } from "https-proxy-agent";
|
||||
import * as fe from "node-fetch";
|
||||
|
||||
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||
import { ApiService } from "jslib-common/services/api.service";
|
||||
|
||||
(global as any).fetch = fe.default;
|
||||
(global as any).Request = fe.Request;
|
||||
(global as any).Response = fe.Response;
|
||||
(global as any).Headers = fe.Headers;
|
||||
(global as any).FormData = FormData;
|
||||
|
||||
export class NodeApiService extends ApiService {
|
||||
constructor(
|
||||
tokenService: TokenService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
environmentService: EnvironmentService,
|
||||
appIdService: AppIdService,
|
||||
logoutCallback: (expired: boolean) => Promise<void>,
|
||||
customUserAgent: string = null
|
||||
) {
|
||||
super(
|
||||
tokenService,
|
||||
platformUtilsService,
|
||||
environmentService,
|
||||
appIdService,
|
||||
logoutCallback,
|
||||
customUserAgent
|
||||
);
|
||||
}
|
||||
|
||||
nativeFetch(request: Request): Promise<Response> {
|
||||
const proxy = process.env.http_proxy || process.env.https_proxy;
|
||||
if (proxy) {
|
||||
(request as any).agent = new HttpsProxyAgent(proxy);
|
||||
}
|
||||
return fetch(request);
|
||||
}
|
||||
}
|
||||
301
libs/node/src/services/nodeCryptoFunction.service.ts
Normal file
301
libs/node/src/services/nodeCryptoFunction.service.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import * as crypto from "crypto";
|
||||
|
||||
import * as forge from "node-forge";
|
||||
|
||||
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { DecryptParameters } from "jslib-common/models/domain/decryptParameters";
|
||||
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||
|
||||
export class NodeCryptoFunctionService implements CryptoFunctionService {
|
||||
pbkdf2(
|
||||
password: string | ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
algorithm: "sha256" | "sha512",
|
||||
iterations: number
|
||||
): Promise<ArrayBuffer> {
|
||||
const len = algorithm === "sha256" ? 32 : 64;
|
||||
const nodePassword = this.toNodeValue(password);
|
||||
const nodeSalt = this.toNodeValue(salt);
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
crypto.pbkdf2(nodePassword, nodeSalt, iterations, len, algorithm, (error, key) => {
|
||||
if (error != null) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(this.toArrayBuffer(key));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
async hkdf(
|
||||
ikm: ArrayBuffer,
|
||||
salt: string | ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
const saltBuf = this.toArrayBuffer(salt);
|
||||
const prk = await this.hmac(ikm, saltBuf, algorithm);
|
||||
return this.hkdfExpand(prk, info, outputByteSize, algorithm);
|
||||
}
|
||||
|
||||
// ref: https://tools.ietf.org/html/rfc5869
|
||||
async hkdfExpand(
|
||||
prk: ArrayBuffer,
|
||||
info: string | ArrayBuffer,
|
||||
outputByteSize: number,
|
||||
algorithm: "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
const hashLen = algorithm === "sha256" ? 32 : 64;
|
||||
if (outputByteSize > 255 * hashLen) {
|
||||
throw new Error("outputByteSize is too large.");
|
||||
}
|
||||
const prkArr = new Uint8Array(prk);
|
||||
if (prkArr.length < hashLen) {
|
||||
throw new Error("prk is too small.");
|
||||
}
|
||||
const infoBuf = this.toArrayBuffer(info);
|
||||
const infoArr = new Uint8Array(infoBuf);
|
||||
let runningOkmLength = 0;
|
||||
let previousT = new Uint8Array(0);
|
||||
const n = Math.ceil(outputByteSize / hashLen);
|
||||
const okm = new Uint8Array(n * hashLen);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const t = new Uint8Array(previousT.length + infoArr.length + 1);
|
||||
t.set(previousT);
|
||||
t.set(infoArr, previousT.length);
|
||||
t.set([i + 1], t.length - 1);
|
||||
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
|
||||
okm.set(previousT, runningOkmLength);
|
||||
runningOkmLength += previousT.length;
|
||||
if (runningOkmLength >= outputByteSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return okm.slice(0, outputByteSize).buffer;
|
||||
}
|
||||
|
||||
hash(
|
||||
value: string | ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256" | "sha512" | "md5"
|
||||
): Promise<ArrayBuffer> {
|
||||
const nodeValue = this.toNodeValue(value);
|
||||
const hash = crypto.createHash(algorithm);
|
||||
hash.update(nodeValue);
|
||||
return Promise.resolve(this.toArrayBuffer(hash.digest()));
|
||||
}
|
||||
|
||||
hmac(
|
||||
value: ArrayBuffer,
|
||||
key: ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
const nodeValue = this.toNodeBuffer(value);
|
||||
const nodeKey = this.toNodeBuffer(key);
|
||||
const hmac = crypto.createHmac(algorithm, nodeKey);
|
||||
hmac.update(nodeValue);
|
||||
return Promise.resolve(this.toArrayBuffer(hmac.digest()));
|
||||
}
|
||||
|
||||
async compare(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
|
||||
const key = await this.randomBytes(32);
|
||||
const mac1 = await this.hmac(a, key, "sha256");
|
||||
const mac2 = await this.hmac(b, key, "sha256");
|
||||
if (mac1.byteLength !== mac2.byteLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const arr1 = new Uint8Array(mac1);
|
||||
const arr2 = new Uint8Array(mac2);
|
||||
for (let i = 0; i < arr2.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
hmacFast(
|
||||
value: ArrayBuffer,
|
||||
key: ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256" | "sha512"
|
||||
): Promise<ArrayBuffer> {
|
||||
return this.hmac(value, key, algorithm);
|
||||
}
|
||||
|
||||
compareFast(a: ArrayBuffer, b: ArrayBuffer): Promise<boolean> {
|
||||
return this.compare(a, b);
|
||||
}
|
||||
|
||||
aesEncrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const nodeData = this.toNodeBuffer(data);
|
||||
const nodeIv = this.toNodeBuffer(iv);
|
||||
const nodeKey = this.toNodeBuffer(key);
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", nodeKey, nodeIv);
|
||||
const encBuf = Buffer.concat([cipher.update(nodeData), cipher.final()]);
|
||||
return Promise.resolve(this.toArrayBuffer(encBuf));
|
||||
}
|
||||
|
||||
aesDecryptFastParameters(
|
||||
data: string,
|
||||
iv: string,
|
||||
mac: string,
|
||||
key: SymmetricCryptoKey
|
||||
): DecryptParameters<ArrayBuffer> {
|
||||
const p = new DecryptParameters<ArrayBuffer>();
|
||||
p.encKey = key.encKey;
|
||||
p.data = Utils.fromB64ToArray(data).buffer;
|
||||
p.iv = Utils.fromB64ToArray(iv).buffer;
|
||||
|
||||
const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength);
|
||||
macData.set(new Uint8Array(p.iv), 0);
|
||||
macData.set(new Uint8Array(p.data), p.iv.byteLength);
|
||||
p.macData = macData.buffer;
|
||||
|
||||
if (key.macKey != null) {
|
||||
p.macKey = key.macKey;
|
||||
}
|
||||
if (mac != null) {
|
||||
p.mac = Utils.fromB64ToArray(mac).buffer;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
async aesDecryptFast(parameters: DecryptParameters<ArrayBuffer>): Promise<string> {
|
||||
const decBuf = await this.aesDecrypt(parameters.data, parameters.iv, parameters.encKey);
|
||||
return Utils.fromBufferToUtf8(decBuf);
|
||||
}
|
||||
|
||||
aesDecrypt(data: ArrayBuffer, iv: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const nodeData = this.toNodeBuffer(data);
|
||||
const nodeIv = this.toNodeBuffer(iv);
|
||||
const nodeKey = this.toNodeBuffer(key);
|
||||
const decipher = crypto.createDecipheriv("aes-256-cbc", nodeKey, nodeIv);
|
||||
const decBuf = Buffer.concat([decipher.update(nodeData), decipher.final()]);
|
||||
return Promise.resolve(this.toArrayBuffer(decBuf));
|
||||
}
|
||||
|
||||
rsaEncrypt(
|
||||
data: ArrayBuffer,
|
||||
publicKey: ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256"
|
||||
): Promise<ArrayBuffer> {
|
||||
if (algorithm === "sha256") {
|
||||
throw new Error("Node crypto does not support RSA-OAEP SHA-256");
|
||||
}
|
||||
|
||||
const pem = this.toPemPublicKey(publicKey);
|
||||
const decipher = crypto.publicEncrypt(pem, this.toNodeBuffer(data));
|
||||
return Promise.resolve(this.toArrayBuffer(decipher));
|
||||
}
|
||||
|
||||
rsaDecrypt(
|
||||
data: ArrayBuffer,
|
||||
privateKey: ArrayBuffer,
|
||||
algorithm: "sha1" | "sha256"
|
||||
): Promise<ArrayBuffer> {
|
||||
if (algorithm === "sha256") {
|
||||
throw new Error("Node crypto does not support RSA-OAEP SHA-256");
|
||||
}
|
||||
|
||||
const pem = this.toPemPrivateKey(privateKey);
|
||||
const decipher = crypto.privateDecrypt(pem, this.toNodeBuffer(data));
|
||||
return Promise.resolve(this.toArrayBuffer(decipher));
|
||||
}
|
||||
|
||||
rsaExtractPublicKey(privateKey: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const privateKeyByteString = Utils.fromBufferToByteString(privateKey);
|
||||
const privateKeyAsn1 = forge.asn1.fromDer(privateKeyByteString);
|
||||
const forgePrivateKey: any = forge.pki.privateKeyFromAsn1(privateKeyAsn1);
|
||||
const forgePublicKey = (forge.pki as any).setRsaPublicKey(forgePrivateKey.n, forgePrivateKey.e);
|
||||
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(forgePublicKey);
|
||||
const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data;
|
||||
const publicKeyArray = Utils.fromByteStringToArray(publicKeyByteString);
|
||||
return Promise.resolve(publicKeyArray.buffer);
|
||||
}
|
||||
|
||||
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
|
||||
return new Promise<[ArrayBuffer, ArrayBuffer]>((resolve, reject) => {
|
||||
forge.pki.rsa.generateKeyPair(
|
||||
{
|
||||
bits: length,
|
||||
workers: -1,
|
||||
e: 0x10001, // 65537
|
||||
},
|
||||
(error, keyPair) => {
|
||||
if (error != null) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(keyPair.publicKey);
|
||||
const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).getBytes();
|
||||
const publicKey = Utils.fromByteStringToArray(publicKeyByteString);
|
||||
|
||||
const privateKeyAsn1 = forge.pki.privateKeyToAsn1(keyPair.privateKey);
|
||||
const privateKeyPkcs8 = forge.pki.wrapRsaPrivateKey(privateKeyAsn1);
|
||||
const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes();
|
||||
const privateKey = Utils.fromByteStringToArray(privateKeyByteString);
|
||||
|
||||
resolve([publicKey.buffer, privateKey.buffer]);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
randomBytes(length: number): Promise<ArrayBuffer> {
|
||||
return new Promise<ArrayBuffer>((resolve, reject) => {
|
||||
crypto.randomBytes(length, (error, bytes) => {
|
||||
if (error != null) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(this.toArrayBuffer(bytes));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private toNodeValue(value: string | ArrayBuffer): string | Buffer {
|
||||
let nodeValue: string | Buffer;
|
||||
if (typeof value === "string") {
|
||||
nodeValue = value;
|
||||
} else {
|
||||
nodeValue = this.toNodeBuffer(value);
|
||||
}
|
||||
return nodeValue;
|
||||
}
|
||||
|
||||
private toNodeBuffer(value: ArrayBuffer): Buffer {
|
||||
return Buffer.from(new Uint8Array(value) as any);
|
||||
}
|
||||
|
||||
private toArrayBuffer(value: Buffer | string | ArrayBuffer): ArrayBuffer {
|
||||
let buf: ArrayBuffer;
|
||||
if (typeof value === "string") {
|
||||
buf = Utils.fromUtf8ToArray(value).buffer;
|
||||
} else {
|
||||
buf = new Uint8Array(value).buffer;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
private toPemPrivateKey(key: ArrayBuffer): string {
|
||||
const byteString = Utils.fromBufferToByteString(key);
|
||||
const asn1 = forge.asn1.fromDer(byteString);
|
||||
const privateKey = forge.pki.privateKeyFromAsn1(asn1);
|
||||
const rsaPrivateKey = forge.pki.privateKeyToAsn1(privateKey);
|
||||
const privateKeyInfo = forge.pki.wrapRsaPrivateKey(rsaPrivateKey);
|
||||
return forge.pki.privateKeyInfoToPem(privateKeyInfo);
|
||||
}
|
||||
|
||||
private toPemPublicKey(key: ArrayBuffer): string {
|
||||
const byteString = Utils.fromBufferToByteString(key);
|
||||
const asn1 = forge.asn1.fromDer(byteString);
|
||||
const publicKey = forge.pki.publicKeyFromAsn1(asn1);
|
||||
return forge.pki.publicKeyToPem(publicKey);
|
||||
}
|
||||
}
|
||||
11
libs/node/tsconfig.json
Normal file
11
libs/node/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../shared/tsconfig",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"jslib-common/*": ["../common/src/*"],
|
||||
"jslib-node/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "spec"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3
libs/node/tsconfig.spec.json
Normal file
3
libs/node/tsconfig.spec.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "./tsconfig.json"
|
||||
}
|
||||
Reference in New Issue
Block a user