diff --git a/.github/workflows/stale-bot b/.github/workflows/stale-bot new file mode 100644 index 00000000..5dc50136 --- /dev/null +++ b/.github/workflows/stale-bot @@ -0,0 +1,22 @@ +name: 'Automatically close stale issues and PRs' +on: + schedule: + - cron: '0 0 * * *' +#Run once a day at midnight + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + stale-issue-message: 'We are clearing up our old issues and your ticket has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.' + stale-pr-message: 'We are clearing up our old Pull Requests and yours has been open for 6 months with no activity. Remove stale label or comment or this will be closed in 7 days.' + close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' + close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.' + days-before-stale: 180 + days-before-close: 7 + exempt-issue-labels: 'News,Medium,High,discussion,bug,doc,' + exempt-pr-labels: 'awaiting-approval,work-in-progress,enhancement,' + exempt-issue-assignees: 'louislam' + exempt-pr-assignees: 'louislam' diff --git a/server/server.js b/server/server.js index e853378e..8156e4e3 100644 --- a/server/server.js +++ b/server/server.js @@ -328,7 +328,7 @@ exports.entryPage = "dashboard"; ]); if (user.twofa_status == 0) { - let newSecret = await genSecret(); + let newSecret = genSecret(); let encodedSecret = base32.encode(newSecret); // Google authenticator doesn't like equal signs diff --git a/src/util.js b/src/util.js index 8ea555a2..b2df7ac7 100644 --- a/src/util.js +++ b/src/util.js @@ -7,7 +7,7 @@ // Backend uses the compiled file util.js // Frontend uses util.ts Object.defineProperty(exports, "__esModule", { value: true }); -exports.getMonitorRelativeURL = exports.genSecret = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; +exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isDev = void 0; const _dayjs = require("dayjs"); const dayjs = _dayjs; exports.isDev = process.env.NODE_ENV === "development"; @@ -102,12 +102,61 @@ function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } exports.getRandomInt = getRandomInt; +/** + * Returns either the NodeJS crypto.randomBytes() function or its + * browser equivalent implemented via window.crypto.getRandomValues() + */ +let getRandomBytes = ((typeof window !== 'undefined' && window.crypto) + // Browsers + ? function () { + return (numBytes) => { + let randomBytes = new Uint8Array(numBytes); + for (let i = 0; i < numBytes; i += 65536) { + window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536))); + } + return randomBytes; + }; + } + // Node + : function () { + return require("crypto").randomBytes; + })(); +function getCryptoRandomInt(min, max) { + // synchronous version of: https://github.com/joepie91/node-random-number-csprng + const range = max - min; + if (range >= Math.pow(2, 32)) + console.log("Warning! Range is too large."); + let tmpRange = range; + let bitsNeeded = 0; + let bytesNeeded = 0; + let mask = 1; + while (tmpRange > 0) { + if (bitsNeeded % 8 === 0) + bytesNeeded += 1; + bitsNeeded += 1; + mask = mask << 1 | 1; + tmpRange = tmpRange >>> 1; + } + const randomBytes = getRandomBytes(bytesNeeded); + let randomValue = 0; + for (let i = 0; i < bytesNeeded; i++) { + randomValue |= randomBytes[i] << 8 * i; + } + randomValue = randomValue & mask; + if (randomValue <= range) { + return min + randomValue; + } + else { + return getCryptoRandomInt(min, max); + } +} +exports.getCryptoRandomInt = getCryptoRandomInt; function genSecret(length = 64) { let secret = ""; - let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let charsLength = chars.length; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charsLength = chars.length; for (let i = 0; i < length; i++) { - secret += chars.charAt(Math.floor(Math.random() * charsLength)); + secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1)); } return secret; } diff --git a/src/util.ts b/src/util.ts index 6e911998..633d933e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -114,12 +114,72 @@ export function getRandomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } +/** + * Returns either the NodeJS crypto.randomBytes() function or its + * browser equivalent implemented via window.crypto.getRandomValues() + */ +let getRandomBytes = ( + (typeof window !== 'undefined' && window.crypto) + + // Browsers + ? function () { + return (numBytes: number) => { + let randomBytes = new Uint8Array(numBytes); + for (let i = 0; i < numBytes; i += 65536) { + window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536))); + } + return randomBytes; + }; + } + + // Node + : function() { + return require("crypto").randomBytes; + } +)(); + +export function getCryptoRandomInt(min: number, max: number):number { + + // synchronous version of: https://github.com/joepie91/node-random-number-csprng + + const range = max - min + if (range >= Math.pow(2, 32)) + console.log("Warning! Range is too large.") + + let tmpRange = range + let bitsNeeded = 0 + let bytesNeeded = 0 + let mask = 1 + + while (tmpRange > 0) { + if (bitsNeeded % 8 === 0) bytesNeeded += 1 + bitsNeeded += 1 + mask = mask << 1 | 1 + tmpRange = tmpRange >>> 1 + } + + const randomBytes = getRandomBytes(bytesNeeded) + let randomValue = 0 + + for (let i = 0; i < bytesNeeded; i++) { + randomValue |= randomBytes[i] << 8 * i + } + + randomValue = randomValue & mask; + + if (randomValue <= range) { + return min + randomValue + } else { + return getCryptoRandomInt(min, max) + } +} + export function genSecret(length = 64) { let secret = ""; - let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let charsLength = chars.length; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charsLength = chars.length; for ( let i = 0; i < length; i++ ) { - secret += chars.charAt(Math.floor(Math.random() * charsLength)); + secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1)); } return secret; }