From fcd3aefc48b11b76d9286d7bee6264a5d5ab49fe Mon Sep 17 00:00:00 2001 From: paul-ion Date: Fri, 27 Dec 2024 14:02:11 -0500 Subject: [PATCH 01/24] Support crypto challenges within the portal --- trainingportal/challenges.js | 96 ++++++++++----- trainingportal/package-lock.json | 149 ++++++++++++++++++++++++ trainingportal/package.json | 3 +- trainingportal/qna.js | 98 ++++++++++++++++ trainingportal/server.js | 4 +- trainingportal/static/challenges.html | 25 +++- trainingportal/static/main-app.js | 2 +- trainingportal/static/submitCode.html | 14 ++- trainingportal/static/submitCodeCtrl.js | 33 ++++-- trainingportal/test/challenge.test.js | 80 +++++++++++++ trainingportal/test/util.test.js | 42 +++++++ trainingportal/util.js | 19 ++- 12 files changed, 513 insertions(+), 52 deletions(-) create mode 100644 trainingportal/qna.js create mode 100644 trainingportal/test/util.test.js diff --git a/trainingportal/challenges.js b/trainingportal/challenges.js index 31c5288b..e0b849be 100644 --- a/trainingportal/challenges.js +++ b/trainingportal/challenges.js @@ -24,6 +24,7 @@ const validator = require('validator'); const crypto = require('crypto'); const aescrypto = require(path.join(__dirname, 'aescrypto')); const https = require('https'); +const qna = require(path.join(__dirname, 'qna')); var modules = {}; var moduleVer = 0; @@ -33,7 +34,7 @@ var solutions = []; var descriptions = []; var masterSalt = ""; -loadModules = () => { +let loadModules = () => { let modsPath; if(!util.isNullOrUndefined(process.env.DATA_DIR)){ modsPath = path.join(process.env.DATA_DIR, "modules.json"); @@ -58,11 +59,11 @@ loadModules = () => { } -function getModulePath(moduleId){ +let getModulePath = (moduleId) => { return path.join('static/lessons/', moduleId); } -function getDefinitionsForModule(moduleId){ +let getDefinitionsForModule = (moduleId) => { try { var defs = Object.freeze(require(path.join(__dirname, getModulePath(moduleId), '/definitions.json'))); @@ -114,11 +115,10 @@ let init = async () => { init(); -getModules = function(){ return modules; } -getChallengeNames = function(){ return challengeNames; } -getChallengeDefinitions = function(){ return challengeDefinitions; } +let getModules = function(){ return modules; } +let getChallengeNames = function(){ return challengeNames; } -isPermittedModule = async (user, moduleId) => { +let isPermittedModule = async (user, moduleId) => { let badges = await db.fetchBadges(user.id); if(util.isNullOrUndefined(modules[moduleId])){ return false; @@ -144,7 +144,7 @@ isPermittedModule = async (user, moduleId) => { /** * Get the user level based on the amount of passed challenges */ -getUserLevelForModule = async (user,moduleId) => { +let getUserLevelForModule = async (user,moduleId) => { let moduleDefinitions = getDefinitionsForModule(moduleId); let passedChallenges = await db.fetchChallengeEntriesForUser(user); let userLevel=-1; @@ -170,7 +170,7 @@ getUserLevelForModule = async (user,moduleId) => { /** * Get permitted challenges for module */ -getPermittedChallengesForUser = async (user, moduleId) => { +let getPermittedChallengesForUser = async (user, moduleId) => { if(util.isNullOrUndefined(moduleId)) return []; if(util.isNullOrUndefined(modules[moduleId])) return []; @@ -189,10 +189,9 @@ getPermittedChallengesForUser = async (user, moduleId) => { /** * Construct the challenge definitions loaded on the client side based on the users level - * @param {Object} user The session user object * @param {Array} moduleIds The lesson module ids */ -getChallengeDefinitionsForUser = async (user, moduleId) => { +let getChallengeDefinitions = async (moduleId) => { var returnChallenges = []; if(util.isNullOrUndefined(moduleId)) return []; @@ -209,10 +208,13 @@ getChallengeDefinitionsForUser = async (user, moduleId) => { if (!util.isNullOrUndefined(playLink)) { challenge.playLink = playLink; } - var description = challenge.description; - if(!util.isNullOrUndefined(description) && description.indexOf(modulePath) === -1){ - challenge.description = path.join(modulePath, description); - } + } + var description = challenge.description; + if(!util.isNullOrUndefined(description) && description.indexOf(modulePath) === -1){ + challenge.description = path.join(modulePath, description); + } + if(challenge.type === "quiz"){ + challenge.question = qna.getCode(challenge.id); } } returnChallenges.push(level); @@ -227,7 +229,7 @@ getChallengeDefinitionsForUser = async (user, moduleId) => { * Returns the solution html (converted from markdown) * @param {The challenge id} challengeId */ -getSolution = function (challengeId) { +let getSolution = function (challengeId) { var solution = solutions[challengeId]; var solutionHtml = ""; if(!util.isNullOrUndefined(solution)){ @@ -242,7 +244,7 @@ getSolution = function (challengeId) { * Returns the description html (converted from markdown if applicable) * @param {The challenge id} challengeId */ -getDescription = function (challengeId) { +let getDescription = function (challengeId) { var description = descriptions[challengeId]; var descriptionHtml = ""; if(util.isNullOrUndefined(description)) return ""; @@ -265,7 +267,7 @@ getDescription = function (challengeId) { /** * Checks if the user has completed the module and issue a badge */ -verifyModuleCompletion = async (user, moduleId) => { +let verifyModuleCompletion = async (user, moduleId) => { var userLevel = await getUserLevelForModule(user, moduleId); let moduleDefinitions = getDefinitionsForModule(moduleId); var lastLevel = moduleDefinitions[moduleDefinitions.length-1]; @@ -293,7 +295,7 @@ verifyModuleCompletion = async (user, moduleId) => { /** * Iterates through the entire list of users to insert badges where needed */ -recreateBadgesOnModulesUpdate = async () => { +let recreateBadgesOnModulesUpdate = async () => { let users = await db.fetchUsersWithId(); @@ -320,7 +322,7 @@ recreateBadgesOnModulesUpdate = async () => { * Retrieves a code to verify completion of the level * @param {Badge} badge */ -getBadgeCode = (badge, user) => { +let getBadgeCode = (badge, user) => { let module = modules[badge.moduleId]; if(util.isNullOrUndefined(module) || util.isNullOrUndefined(module.badgeInfo)) return null; @@ -347,7 +349,7 @@ getBadgeCode = (badge, user) => { * Verifies a badge code and returns parsed info * @param {Base64} badgeCode */ -verifyBadgeCode = (badgeCode) => { +let verifyBadgeCode = (badgeCode) => { urlDecoded = decodeURIComponent(badgeCode); let parts = urlDecoded.split("."); if(parts.length !== 2) return null; @@ -370,7 +372,7 @@ verifyBadgeCode = (badgeCode) => { * @param {*} badgrInfo * @param {*} user */ -badgrCall = function(badgrInfo, user){ +let badgrCall = function(badgrInfo, user){ if(!util.isNullOrUndefined(badgrInfo) && !util.isNullOrUndefined(config.encBadgrToken)){ if(user.email===null){ util.log("Cannot issue badge for this user. E-mail is null.", user); @@ -418,26 +420,42 @@ badgrCall = function(badgrInfo, user){ /** * Logic to for the api challenge code */ -apiChallengeCode = async (req) => { +let apiChallengeCode = async (req) => { if(util.isNullOrUndefined(req.body.challengeId) || util.isNullOrUndefined(req.body.challengeCode) || util.isNullOrUndefined(req.body.moduleId)){ throw Error("invalidRequest"); } + var moduleId = req.body.moduleId.trim(); var challengeId = req.body.challengeId.trim(); var challengeCode = req.body.challengeCode.trim(); - if(util.isNullOrUndefined(challengeCode) || validator.isBase64(challengeCode) == false){ + let challengeType = "page"; + if(!util.isNullOrUndefined(req.body.challengeType)){ + challengeType = req.body.challengeType; + } + + if(["page","quiz"].indexOf(challengeType) === -1){ + throw Error("invalidChallengeType"); + } + + let answer = null; + if(!util.isNullOrUndefined(req.body.answer)){ + answer = req.body.answer.trim(); + } + + if(util.isNullOrUndefined(challengeCode) || + (validator.isAlphanumeric(challengeCode) === false && validator.isBase64(challengeCode) === false) ){ throw Error("invalidCode"); } - if(util.isNullOrUndefined(moduleId) || validator.isAlphanumeric(moduleId) == false){ + if(util.isNullOrUndefined(moduleId) || validator.isAlphanumeric(moduleId) === false){ throw Error("invalidModuleId"); } - if(util.isNullOrUndefined(challengeId) || util.isAlphanumericOrUnderscore(challengeId) == false){ + if(util.isNullOrUndefined(challengeId) || util.isAlphanumericOrUnderscore(challengeId) === false){ throw Error("invalidChallengeId"); } @@ -468,10 +486,26 @@ apiChallengeCode = async (req) => { } //either hex or base64 formats should work //we're looking at the first 10 characters only for situations where the challenge code may get truncated - pcaps, IPS logs - var verificationHashB64 = crypto.createHash('sha256').update(challengeId+req.user.codeSalt+ms).digest('base64').substr(0,10); - var verificationHashHex = crypto.createHash('sha256').update(challengeId+req.user.codeSalt+ms).digest('hex').substr(0,10); + let verificationHashB64; + let verificationHashHex; + + if(challengeType === "quiz"){ + verificationHashB64 = crypto.createHash('sha256').update(answer+ms).digest('base64').substr(0,10); + verificationHashHex = crypto.createHash('sha256').update(answer+ms).digest('hex').substr(0,10); + } + else{ + verificationHashB64 = crypto.createHash('sha256').update(challengeId+req.user.codeSalt+ms).digest('base64').substr(0,10); + verificationHashHex = crypto.createHash('sha256').update(challengeId+req.user.codeSalt+ms).digest('hex').substr(0,10); + } + + if(challengeCode.indexOf(verificationHashB64)!==0 && challengeCode.indexOf(verificationHashHex)!==0){ - throw Error("invalidCode"); + if(challengeType === "quiz"){ + throw Error("invalidAnswer"); + } + else{ + throw Error("invalidCode"); + } } //success update challenge curChallengeObj.moduleId = moduleId; @@ -482,7 +516,7 @@ apiChallengeCode = async (req) => { /** * Inserts a challenge entry */ -insertChallengeEntry = async (user,curChallengeObj, moduleId) => { +let insertChallengeEntry = async (user,curChallengeObj, moduleId) => { await db.getPromise(db.insertChallengeEntry, [user.id,curChallengeObj.id]); //issue badgr badge if enabled badgrCall(curChallengeObj.badgrInfo,user); @@ -509,14 +543,12 @@ insertChallengeEntry = async (user,curChallengeObj, moduleId) => { - module.exports = { apiChallengeCode, badgrCall, getBadgeCode, getChallengeNames, getChallengeDefinitions, - getChallengeDefinitionsForUser, getDescription, getModules, getPermittedChallengesForUser, diff --git a/trainingportal/package-lock.json b/trainingportal/package-lock.json index eb7bb201..8f4f9250 100644 --- a/trainingportal/package-lock.json +++ b/trainingportal/package-lock.json @@ -1470,6 +1470,12 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true + }, "bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -1713,6 +1719,22 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, "chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -2388,6 +2410,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2537,6 +2568,12 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, "import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2612,6 +2649,15 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-core-module": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", @@ -2621,6 +2667,12 @@ "hasown": "^2.0.2" } }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2632,6 +2684,15 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-lambda": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", @@ -4292,6 +4353,41 @@ "resolved": "https://registry.npmjs.org/node-truncate/-/node-truncate-0.1.0.tgz", "integrity": "sha512-auuR7QLtuYakWJVCYCXFwUx8nC3+COxoYO6Ug2d9KgkYoGj/sRiTbuHP53UyCLpkjriB8qxP00tbdlAAMTpybA==" }, + "nodemon": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -4755,6 +4851,12 @@ "ipaddr.js": "1.9.1" } }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, "pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -4833,6 +4935,15 @@ "util-deprecate": "^1.0.1" } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5048,6 +5159,15 @@ "simple-concat": "^1.0.0" } }, + "simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, "sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -5246,6 +5366,23 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + }, + "dependencies": { + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + } + } + }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5326,6 +5463,12 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, + "touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -5367,6 +5510,12 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, "undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", diff --git a/trainingportal/package.json b/trainingportal/package.json index cc647bea..78f2e03a 100644 --- a/trainingportal/package.json +++ b/trainingportal/package.json @@ -47,6 +47,7 @@ "validator": "^13.6.0" }, "devDependencies": { - "jest": "^29.7.0" + "jest": "^29.7.0", + "nodemon": "^3.1.9" } } diff --git a/trainingportal/qna.js b/trainingportal/qna.js new file mode 100644 index 00000000..d2ecd292 --- /dev/null +++ b/trainingportal/qna.js @@ -0,0 +1,98 @@ +const crypto = require('crypto'); +const uid = require('uid-safe'); +const path = require('path'); +const util = require(path.join(__dirname, 'util')); +const dictionary = ["lorem","ipsum","dolor","sit","amet","consectetur","adipiscing","elit","maecenas","mollis","nec","libero","non","venenatis","sed","dictum","vel","ligula","pharetra","viverra","nunc","vehicula","augue","vitae","cursus","dictum","magna","tortor","tempus","neque","vitae","viverra","mauris","ipsum","vel","eros","nulla","auctor","purus","eget","mattis","tempus","proin","in","aliquam","mi","etiam","massa","arcu","dapibus","vel","interdum","finibus","pretium","quis","ante","etiam","venenatis","neque","id","imperdiet","bibendum","nisi","enim","gravida","diam","sed","iaculis","urna","velit","id","ligula","quisque","eleifend","neque","in","vestibulum","venenatis","morbi","sit","amet","euismod","lectus","Sed","suscipit","velit","ac","magna","aliquam","eu","tincidunt","nisl","dictum","curabitur","ac","ante","ut","nibh","bibendum","faucibus","egestas","vehicula","neque","aenean","lorem","velit","maximus","nec","placerat","sed","hendrerit","vel","ligula","vivamus","laoreet","vitae","nisi","in","laoreet","curabitur","sit","amet","nulla","non","libero","porttitor","suscipit"] +const SECRET_WORD_COUNT = 10; +var masterSalt = ""; +if(!util.isNullOrUndefined(process.env.CHALLENGE_MASTER_SALT)){ + masterSalt=process.env.CHALLENGE_MASTER_SALT; +} + +let getSecretText = () => { + let min = 0; + let max = dictionary.length - 1; + secretText = ""; + for(let i=0;i { + let digest = crypto.createHash('sha256').update(mes+masterSalt).digest('hex'); + return res = { + code:code, + digest:digest, + } +} + +let getCode = (challengeId, key) => { + let mes = getSecretText(); + let code = DEFS[challengeId](mes, key); + return getRes(mes, code); +} + +let checkCode = (mes, digest) => { + let vfy = crypto.createHash('sha256').update(mes+masterSalt).digest('hex'); + return vfy === digest; +} + +let caesarEnc = (mes, key) => { + let diff; + if(util.isNullOrUndefined(key)){ + diff = util.getRandomInt(10,20); + } + else{ + diff = key; + } + + let shifted = ""; + for(let i=0;i "z".charCodeAt(0)){ + let diffToZ = newCode - "z".charCodeAt(0); + newCode = "a".charCodeAt(0) + diffToZ - 1; + } + shifted+= String.fromCharCode(newCode); + } + } + return shifted +} + +let asciiEnc = (mes) => { + let encoding = ""; + for(let i=0;i { + let code = util.btoa(mes); + return code; +} + + + +const DEFS = { + "caesar": caesarEnc, + "ascii": asciiEnc, + "base64": base64Enc +} + +module.exports = { + getCode, + checkCode +} + diff --git a/trainingportal/server.js b/trainingportal/server.js index 40ce83fe..1971e2af 100644 --- a/trainingportal/server.js +++ b/trainingportal/server.js @@ -270,7 +270,7 @@ app.get('/challenges/:moduleId', async (req, res) => { return util.apiResponse(req, res, 403, "Requested module id is not available."); } - var returnChallenges = await challenges.getChallengeDefinitionsForUser(req.user, moduleId); + var returnChallenges = await challenges.getChallengeDefinitions(moduleId); var response = { "challenges" : returnChallenges }; @@ -394,7 +394,9 @@ app.post('/api/user/challengeCode', async (req,res) => { switch(err.message){ case "invalidRequest":util.apiResponse(req, res, 400, "Invalid request."); break; case "invalidCode":util.apiResponse(req, res, 400, "Invalid challenge code."); break; + case "invalidAnswer":util.apiResponse(req, res, 400, "Invalid answer."); break; case "invalidChallengeId":util.apiResponse(req, res, 400, "Invalid challenge id."); break; + case "invalidChallengeType":util.apiResponse(req, res, 400, "Invalid challenge type."); break; case "invalidModuleId":util.apiResponse(req, res, 400, "Invalid module id."); break; case "challengeNotAvailable":util.apiResponse(req, res, 404, "Challenge not found for the current user level"); break; case "challengeSecretNotFound":util.apiResponse(req, res, 404, "Challenge secret not found."); break; diff --git a/trainingportal/static/challenges.html b/trainingportal/static/challenges.html index fbc7a9a1..546bdc3f 100644 --- a/trainingportal/static/challenges.html +++ b/trainingportal/static/challenges.html @@ -71,15 +71,30 @@

Play Link and Instructions

ng-href="{{targetUrl}}{{challenge.playLink}}" target="_blank" rel="noopener noreferrer" class="btn btn-warning btn-sm">Play

-

+

The play link has been provided to you when solving the previous module or challenge. If you have missed it read the challenge description carefully and try to figure out what it is.

+
+ +
+ -

- Once you were able to complete the challenge you can generate a code which you can submit below. -

- Submit Code + + +

+ Once you were able to complete the challenge you can generate a code which you can submit below. +

+ Submit Code +
+ + +

+ Once you were able to find the answer you can submit it below. +

+ Submit Answer +
+


diff --git a/trainingportal/static/main-app.js b/trainingportal/static/main-app.js index c0708c73..933d33b9 100644 --- a/trainingportal/static/main-app.js +++ b/trainingportal/static/main-app.js @@ -10,7 +10,7 @@ app.config(function($routeProvider) { templateUrl : "static/challenges.html", controller: "challengesCtrl" }) - .when("/submitCode/:moduleId/:challengeId", { + .when("/submitCode/:moduleId/:challengeId/:challengeType/:challengeCode", { templateUrl : "static/submitCode.html", controller: "submitCodeCtrl" }) diff --git a/trainingportal/static/submitCode.html b/trainingportal/static/submitCode.html index 439c0156..830a8301 100644 --- a/trainingportal/static/submitCode.html +++ b/trainingportal/static/submitCode.html @@ -3,15 +3,21 @@ {{codeErrorMessage}} -

When you have solved the challenge, take the salt below to obtain the verification code.

+

When you have solved the challenge, take the salt below to obtain the verification code.


-
+
+   + +
+
  -
-
+
diff --git a/trainingportal/static/submitCodeCtrl.js b/trainingportal/static/submitCodeCtrl.js index 5e09769d..79596f0a 100644 --- a/trainingportal/static/submitCodeCtrl.js +++ b/trainingportal/static/submitCodeCtrl.js @@ -17,12 +17,19 @@ app.controller("submitCodeCtrl", function($scope, $http, $routeParams) { $scope.init = function(){ - $http.get("/api/salt",window.getAjaxOpts()) - .then(function(response) { - if(response != null && response.data != null){ - $scope.salt = response.data; - } - }); + var challengeCodeValue = $routeParams.challengeCode; + $scope.challengeType = $routeParams.challengeType; + if(challengeCodeValue === '0'){ //this is the old style of challenge verification + $http.get("/api/salt",window.getAjaxOpts()) + .then(function(response) { + if(response != null && response.data != null){ + $scope.salt = response.data; + } + }); + } + + $scope.challengeCodeValue = challengeCodeValue; + } @@ -34,12 +41,24 @@ app.controller("submitCodeCtrl", function($scope, $http, $routeParams) { $scope.submitAnswer = function(){ var moduleId = $routeParams.moduleId; var challengeId = $routeParams.challengeId; + var challengeType = $routeParams.challengeType; + var answerValue = answer.value; + var challengeCodeValue = ""; + if(typeof challengeCode !== "undefined"){ + challengeCodeValue = challengeCode.value + } + else{ + challengeCodeValue = $routeParams.challengeCode; + } + $scope.isCodeErrorMessage = false; $scope.isCodeSuccessMessage = false; $http.post("/api/user/challengeCode",{ "moduleId":moduleId, "challengeId":challengeId, - "challengeCode":challengeCode.value + "challengeCode":challengeCodeValue, + "challengeType":challengeType, + "answer":answerValue }, window.getAjaxOpts()).then(function(response) { if(response != null && response.data != null){ if(response.data.status == 200){ diff --git a/trainingportal/test/challenge.test.js b/trainingportal/test/challenge.test.js index b027124b..80f964ec 100644 --- a/trainingportal/test/challenge.test.js +++ b/trainingportal/test/challenge.test.js @@ -87,6 +87,16 @@ describe('challengeTests', () => { }); }); + describe('#getChallengeDefinitions()', () => { + test('should return a non-zero count of challenges for securityCodeReviewMaster', async () => { + let defs = await challenges.getChallengeDefinitions("securityCodeReviewMaster"); + assert(defs.length > 0,"Unexpected number of challenges returned for securityCodeReviewMaster"); + }); + test('should return a 0 count of challenges for nonExistentModule', async () => { + let defs = await challenges.getChallengeDefinitions("nonExistentModule"); + assert(defs.length === 0,"Unexpected number of challenges returned for nonExistentModule"); + }); + }); describe('#verifyModuleCompletion() - issue badge', () => { @@ -274,6 +284,20 @@ describe('challengeTests', () => { return promise; }); + test('should return invalid challenge type for wrong challenge type',async () => { + let promise = challenges.apiChallengeCode({"body": + {"moduleId":"blackBelt","challengeCode":"afd","challengeId":"ch1","challengeType":"badType"}}); + try{ + await promise; + } + catch(err){ + assert.notEqual(err,null,"Error is null"); + assert.equal(err.message,"invalidChallengeType","Wrong error code returned"); + promise = new Promise((resolve)=>{resolve("ok");}); + } + return promise; + }); + test('should return challenge not available for incorrect user level',async () => { let promise = challenges.apiChallengeCode({ "user":user, @@ -343,6 +367,62 @@ describe('challengeTests', () => { return promise; }); + test('should fail the challenge for wrong answer',async () => { + let mockAnswer = "1234"; + let mockAnswerHash = crypto.createHash('sha256').update(mockAnswer+masterSalt).digest('hex'); + let response = null; + + let promise = challenges.apiChallengeCode( + { + "user":user, + "body": + { + "moduleId":"cryptoBreaker", + "challengeCode":mockAnswerHash, + "challengeId":"caesar", + "challengeType":"quiz", + "answer":"wrong" + } + }); + + try{ + response = await promise; + } + catch(err){ + assert.strictEqual(err.message,"invalidAnswer","Expected invalidCode"); + } + assert.strictEqual(response,null,"Should fail for wrong answer"); + + }); + + test('should pass the challenge for correct answer',async () => { + let mockAnswer = "1234"; + let mockAnswerHash = crypto.createHash('sha256').update(mockAnswer+masterSalt).digest('hex'); + let response = null; + + let promise = challenges.apiChallengeCode( + { + "user":user, + "body": + { + "moduleId":"cryptoBreaker", + "challengeCode":mockAnswerHash, + "challengeId":"caesar", + "challengeType":"quiz", + "answer":mockAnswer + } + }); + + try{ + response = await promise; + } + catch(err){ + assert.strictEqual(err,null,"Error is not null"); + } + assert.strictEqual(response.data.id, "caesar", "Wrong challenge id.") + }); + + test('should return updated level for user', async () => { await db.getPromise(db.insertChallengeEntry,[user.id, "cwe807"]); diff --git a/trainingportal/test/util.test.js b/trainingportal/test/util.test.js new file mode 100644 index 00000000..56942ff3 --- /dev/null +++ b/trainingportal/test/util.test.js @@ -0,0 +1,42 @@ +const util = require("../util"); + +describe('util', () => { + describe('getRandomInt', () => { + + test('throw error for incorrect parameters',()=>{ + let errThrown = false; + try { + util.getRandomInt(30,10); + } catch (error) { + errThrown = true; + } + expect(errThrown).toBeTruthy(); + }); + + test('return number within bounds',()=>{ + let min = 10; + let max = 20; + + let val = util.getRandomInt(min, max); + + expect(val >= min).toBeTruthy(); + expect(val <= max).toBeTruthy(); + + }); + + + + }); + + describe('bato-atob', () => { + + test('should decode-encode correctly',()=>{ + let mes = "ABCD"; + let encoded = util.btoa(mes); + let decoded = util.atob(encoded); + expect(decoded).toEqual(mes); + }); + + }); + +}); \ No newline at end of file diff --git a/trainingportal/util.js b/trainingportal/util.js index 78e28d8c..0c661b77 100644 --- a/trainingportal/util.js +++ b/trainingportal/util.js @@ -2,6 +2,7 @@ const crypto = require('crypto'); const path = require('path'); const fs = require('fs'); const markdown = require('markdown').markdown; + var config = null; exports.getDataDir = () => { @@ -117,4 +118,20 @@ exports.getPrivacyHtml = () => { let md = fs.readFileSync(polPath,"utf-8"); let html = exports.parseMarkdown(md); return html; -} \ No newline at end of file +} + +exports.getRandomInt = (min, max) => { + if(min >= max) throw Error("getRandomInt min can't be greater than max"); + let innerMax = max - min; + + let val = Math.floor(Math.random() * innerMax); + return min + val; +} + +exports.btoa = (s) => { + return Buffer.from(s).toString('base64'); +} + +exports.atob = (b) => { + return Buffer.from(b, 'base64').toString() +} From 6b8fa0e03e6bd0ca0d19bdb78551f448de8c8de3 Mon Sep 17 00:00:00 2001 From: paul-ion Date: Fri, 27 Dec 2024 14:03:00 -0500 Subject: [PATCH 02/24] First 3 lessons of the crypto module --- .../lessons/cryptoBreaker/crypto_ascii.md | 14 ++++++++ .../lessons/cryptoBreaker/crypto_base64.md | 27 ++++++++++++++++ .../lessons/cryptoBreaker/crypto_caesar.md | 14 ++++++++ .../lessons/cryptoBreaker/definitions.json | 32 +++++++++++++++++++ trainingportal/static/lessons/modules.json | 12 +++++++ trainingportal/test/qna.test.js | 22 +++++++++++++ 6 files changed, 121 insertions(+) create mode 100644 trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md create mode 100644 trainingportal/static/lessons/cryptoBreaker/crypto_base64.md create mode 100644 trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md create mode 100644 trainingportal/static/lessons/cryptoBreaker/definitions.json create mode 100644 trainingportal/test/qna.test.js diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md b/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md new file mode 100644 index 00000000..ae48226e --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md @@ -0,0 +1,14 @@ + +Computers store text as a sequence of numbers. +The ASCII encoding was one of the first methods of computer representation for English alphabet characters. + +#### Algorithm +Map each letter to a number. + +##### Example +ABCD becomes 65 66 67 68 + +##### Weakness +It can be easily deciphered using the well known ASCII character map. + + diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_base64.md b/trainingportal/static/lessons/cryptoBreaker/crypto_base64.md new file mode 100644 index 00000000..7dd4301c --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_base64.md @@ -0,0 +1,27 @@ + +Base64 is a method of encoding binary data, including but not limited to text in binary form. +The binary data is taken six bits at a time and mapped to a list of 64 unique printable characters. +These characters are numerals of the Base64. + +- Base2 contains two numerals: 0 and 1. +- Base10 contains ten numberals: 0-9. +- Base16 contains sixteen numberals: 0-9,A-F. +- Base64 contains 64 numerals: A-Z,a-z,0-9,+ and /. + +#### Algorithm +- Split data in groups of 6 bits +- Map each group of bits to a numeral of the base64 set +- Use padding (=) to fill in empty spaces, when the data doesn't fit exactly in groups of 6 bits + +##### Example +- ABCD becomes 65 66 67 68 in ASCII also represented as 0x41, 0x42, 0x43, 0x44 in hexadecimal (Base16) +- The binary (Base2) character sequence is 0100 0001, 0100 0010, 0100 0011, 0100 0100 (4 groups of 8 bits or 4 bytes) +- The sequence then is represented as 010000, 010100, 001001, 000011, 010001, 00 (5 groups of 6 bits, and 2 zero bites left) +- The Base64 representation for each of these codes is Q, U, J, D and R +- The Base64 representation for the last two bits is 00---- (A) with padding added for the empty spots +- The final code is QUJDRA== + +##### Weakness +It can be easily deciphered using the well known base64 character map, regrouping the bites in bytes and then converting to the character encoding representation. + + diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md new file mode 100644 index 00000000..7a0f0a7c --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md @@ -0,0 +1,14 @@ + +The Caesar cipher is an ancient method for hiding a message known to be used by Julius Caesar. + +#### Algorithm +Shift letters by a number of positions. The number of positions is the key. + +##### Example +ABCD becomes BCDE shifted right by one. +ABCD becomes ZACD shifted left by one. + +##### Weakness +It can be easily deciphered by trying all possible shifts and there are as many shifts as letters in the alphabet. + + diff --git a/trainingportal/static/lessons/cryptoBreaker/definitions.json b/trainingportal/static/lessons/cryptoBreaker/definitions.json new file mode 100644 index 00000000..c001bf99 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/definitions.json @@ -0,0 +1,32 @@ +[ + { + "level":0, + "name":"Crypto Breaker", + "challenges":[ + { + "id":"caesar", + "name":"Caesar Cipher", + "description": "crypto_caesar.md", + "type":"quiz", + "mission":"Decode the cipher below.", + "codeBlockIds":[] + }, + { + "id":"ascii", + "name":"ASCII Encoding", + "description": "crypto_ascii.md", + "type":"quiz", + "mission":"Decode the text below.", + "codeBlockIds":[] + }, + { + "id":"base64", + "name":"Base64 Encoding", + "description": "crypto_base64.md", + "type":"quiz", + "mission":"Decode the text below.", + "codeBlockIds":[] + } + ] + } +] \ No newline at end of file diff --git a/trainingportal/static/lessons/modules.json b/trainingportal/static/lessons/modules.json index ec1ed094..dd06b346 100644 --- a/trainingportal/static/lessons/modules.json +++ b/trainingportal/static/lessons/modules.json @@ -14,6 +14,18 @@ }, "requiredModules":[] }, + "cryptoBreaker":{ + "name":"Crypto Breaker", + "summary":"Cryptography challenges", + "description":"Learn about encryption algorithms and cryptographic attacks.", + "badgeInfo":{ + "line1":"Secure Coding Dojo", + "line2":"Crypto Breaker", + "line3":"", + "bg":"navy" + }, + "requiredModules":[] + }, "greenBelt":{ "name":"Green Belt", "summary":"Common software security flaws - part 1", diff --git a/trainingportal/test/qna.test.js b/trainingportal/test/qna.test.js new file mode 100644 index 00000000..b3746f97 --- /dev/null +++ b/trainingportal/test/qna.test.js @@ -0,0 +1,22 @@ +const qna = require("../qna"); +const util = require("../util"); + +describe("qna", () => { + describe("base64CaesarEnc", () => { + + test("false for incorrect code",()=>{ + let res = qna.getCode("caesar"); + let wrong = "1234"; + let check = qna.checkCode(wrong, res.digest); + expect(check).toBeFalsy(); + }); + + test("true for correct code",()=>{ + let shifted = qna.getCode("caesar",0); + //shifted did not change at all because the key is 0 + let check = qna.checkCode(shifted.code, res.digest); + expect(check).toBeTruthy(); + }); + + }); +}); \ No newline at end of file From 3a4bf4ae3853426bf02485239d82190b346d6f58 Mon Sep 17 00:00:00 2001 From: paul-ion Date: Fri, 27 Dec 2024 20:29:12 -0500 Subject: [PATCH 03/24] Hashing challenge --- trainingportal/qna.js | 13 ++++++++- trainingportal/static/challenges.html | 2 +- .../lessons/cryptoBreaker/crypto_hash.md | 28 +++++++++++++++++++ .../lessons/cryptoBreaker/definitions.json | 8 ++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 trainingportal/static/lessons/cryptoBreaker/crypto_hash.md diff --git a/trainingportal/qna.js b/trainingportal/qna.js index d2ecd292..9fc849cb 100644 --- a/trainingportal/qna.js +++ b/trainingportal/qna.js @@ -83,12 +83,23 @@ let base64Enc = (mes) => { return code; } +let hashEnc = (mes) => { + let words = mes.split(" "); + let hashedWords = []; + for(let word of words){ + let hash = crypto.createHash('md5').update(word).digest('hex'); + hashedWords.push(hash); + } + return hashedWords.join("\n"); +} + const DEFS = { "caesar": caesarEnc, "ascii": asciiEnc, - "base64": base64Enc + "base64": base64Enc, + "hash": hashEnc } module.exports = { diff --git a/trainingportal/static/challenges.html b/trainingportal/static/challenges.html index 546bdc3f..be4e09e9 100644 --- a/trainingportal/static/challenges.html +++ b/trainingportal/static/challenges.html @@ -76,7 +76,7 @@

Play Link and Instructions

If you have missed it read the challenge description carefully and try to figure out what it is.

- +
diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md new file mode 100644 index 00000000..b8aab0c9 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md @@ -0,0 +1,28 @@ + +A cryptographic hash function is a way of computing a code for a chunk of data. This code is also called a `digest`. + +A secure cryptographic hashing algorithm has the following properties: +- It cannot be reversed (one-way only) +- It consistenly produces the same digest for the same data +- It is unique for the provided data +- A small change in the data produces a significantly different digest + +Hash functions are being used in a variety of applications: +- Validating the integrity of a file or a message +- Storing a password +- Generating a cryptographic key from a password + +#### Algorithm +There are several classes of hashing algorithms: MD5, SHA1, SHA2, BLAKE. MD5 and SHA1 are known to be vulnerable. +Most algorithms leverage the characteristics of the data to arrive at a unique value. + +##### Example + +Using MD5 "ABCD" becomes cb08ca4a7bb5f9683c19133a84872ca7 +Using MD5 "ABCE" becomes 6b011b774af5377cba2ec2b8ecd0b63b + +##### Weaknesses + +Digests can be pre-calculated making them as easy to reverse as an ASCII code. Indeed websites like crackstation.net or hashes.com contain large databases of pre-calculated digests. The best way to prevent the reversing hashed words is to concatenate a random string to the text. This is known as adding a salt. Another mitigation involves hashing the message several times (adding iterations). This increases the ammount of computations necessary to compute the hash. + +Hashing algorithms are also vulnerable to collision attacks. Such attacks involve altering the input to arrive at the same digest. This is particularly dangerous when using hashing functions to ensure the integrity of executable files. Both MD5 and SHA1 algorithms are vulnerable to collision attacks. \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/definitions.json b/trainingportal/static/lessons/cryptoBreaker/definitions.json index c001bf99..a61add22 100644 --- a/trainingportal/static/lessons/cryptoBreaker/definitions.json +++ b/trainingportal/static/lessons/cryptoBreaker/definitions.json @@ -26,6 +26,14 @@ "type":"quiz", "mission":"Decode the text below.", "codeBlockIds":[] + }, + { + "id":"hash", + "name":"One-way crypto", + "description": "crypto_hash.md", + "type":"quiz", + "mission":"Find the text by cracking the digest of each word.", + "codeBlockIds":[] } ] } From 223c675eddd31182bb65abd0177e82ef47520c9e Mon Sep 17 00:00:00 2001 From: paul-ion Date: Fri, 27 Dec 2024 20:36:25 -0500 Subject: [PATCH 04/24] Hash challenge, fix formatting --- .../static/lessons/cryptoBreaker/crypto_hash.md | 11 ++++++++--- .../static/lessons/cryptoBreaker/definitions.json | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md index b8aab0c9..52a9061c 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md @@ -2,24 +2,29 @@ A cryptographic hash function is a way of computing a code for a chunk of data. This code is also called a `digest`. A secure cryptographic hashing algorithm has the following properties: + - It cannot be reversed (one-way only) - It consistenly produces the same digest for the same data - It is unique for the provided data - A small change in the data produces a significantly different digest Hash functions are being used in a variety of applications: + - Validating the integrity of a file or a message - Storing a password - Generating a cryptographic key from a password #### Algorithm -There are several classes of hashing algorithms: MD5, SHA1, SHA2, BLAKE. MD5 and SHA1 are known to be vulnerable. +There are several classes of hashing algorithms: MD5, SHA1, SHA2, BLAKE. + +MD5 and SHA1 are known to be vulnerable. + Most algorithms leverage the characteristics of the data to arrive at a unique value. ##### Example -Using MD5 "ABCD" becomes cb08ca4a7bb5f9683c19133a84872ca7 -Using MD5 "ABCE" becomes 6b011b774af5377cba2ec2b8ecd0b63b +- Using MD5 "ABCD" becomes cb08ca4a7bb5f9683c19133a84872ca7 +- Using MD5 "ABCE" becomes 6b011b774af5377cba2ec2b8ecd0b63b ##### Weaknesses diff --git a/trainingportal/static/lessons/cryptoBreaker/definitions.json b/trainingportal/static/lessons/cryptoBreaker/definitions.json index a61add22..469e80f7 100644 --- a/trainingportal/static/lessons/cryptoBreaker/definitions.json +++ b/trainingportal/static/lessons/cryptoBreaker/definitions.json @@ -29,7 +29,7 @@ }, { "id":"hash", - "name":"One-way crypto", + "name":"One-Way Hash", "description": "crypto_hash.md", "type":"quiz", "mission":"Find the text by cracking the digest of each word.", From e00d51b5859e53146cc2ea741d393d2a809a6bbc Mon Sep 17 00:00:00 2001 From: paul-ion Date: Sat, 28 Dec 2024 08:22:38 -0500 Subject: [PATCH 05/24] Cosmetic text updates --- trainingportal/static/challenges.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/trainingportal/static/challenges.html b/trainingportal/static/challenges.html index be4e09e9..ef06ce14 100644 --- a/trainingportal/static/challenges.html +++ b/trainingportal/static/challenges.html @@ -24,7 +24,6 @@
-

Challenge Description

@@ -55,7 +54,7 @@

-

Play Link and Instructions

+

Challenge

Mission: {{challenge.mission}} From 3d5bc6bcf3ef020b52bfb1a714f9a7de2c943c14 Mon Sep 17 00:00:00 2001 From: paul-ion Date: Sat, 28 Dec 2024 11:25:34 -0500 Subject: [PATCH 06/24] Update welcome message and fix a error --- .../static/lessons/cryptoBreaker/crypto_caesar.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md index 7a0f0a7c..0f2b6e97 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md @@ -1,12 +1,19 @@ -The Caesar cipher is an ancient method for hiding a message known to be used by Julius Caesar. +#### Welcome to the Encryption module. +In this module you will learn about various ways in which information can be encoded and decoded. + +To solve challenges you will need to execute various transformations on a block of given data. Online resources such as `dCode.fr`, `crackstation.net`, `hashes.com` and others offer tools that can help you in your journey. You may also use your programming language of choice and openssl. + +Note: You're allowed to conduct offline brute force attacks, however **trying answer combinations in an automatic fashion using the portal is strictly forbidden**. + +We begin with one of the oldest methods used to hide a message, known to be used by Julius Caesar. #### Algorithm Shift letters by a number of positions. The number of positions is the key. ##### Example ABCD becomes BCDE shifted right by one. -ABCD becomes ZACD shifted left by one. +ABCD becomes ZABC shifted left by one. ##### Weakness It can be easily deciphered by trying all possible shifts and there are as many shifts as letters in the alphabet. From 81edd8eb532670546adf5f8d8f5e8840677b4278 Mon Sep 17 00:00:00 2001 From: paul-ion Date: Sat, 28 Dec 2024 11:25:53 -0500 Subject: [PATCH 07/24] Add hex ASCII codes for ABCD --- trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md b/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md index ae48226e..e38a073d 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md @@ -6,7 +6,8 @@ The ASCII encoding was one of the first methods of computer representation for E Map each letter to a number. ##### Example -ABCD becomes 65 66 67 68 +ABCD becomes 65 66 67 68 in decimal, 41 42 43 44 in hexadecimal (Base16) + ##### Weakness It can be easily deciphered using the well known ASCII character map. From 0fbd64aaf94ecb40e8b0ddca985da4f2fbb7d859 Mon Sep 17 00:00:00 2001 From: paul-ion Date: Sat, 28 Dec 2024 11:26:29 -0500 Subject: [PATCH 08/24] Highlight the cracking sites names --- trainingportal/static/lessons/cryptoBreaker/crypto_hash.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md index 52a9061c..7477604e 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md @@ -28,6 +28,6 @@ Most algorithms leverage the characteristics of the data to arrive at a unique v ##### Weaknesses -Digests can be pre-calculated making them as easy to reverse as an ASCII code. Indeed websites like crackstation.net or hashes.com contain large databases of pre-calculated digests. The best way to prevent the reversing hashed words is to concatenate a random string to the text. This is known as adding a salt. Another mitigation involves hashing the message several times (adding iterations). This increases the ammount of computations necessary to compute the hash. +Digests can be pre-calculated making them as easy to reverse as an ASCII code. Indeed websites like `crackstation.net` or `hashes.com` contain large databases of pre-calculated digests. The best way to prevent the reversing hashed words is to concatenate a random string to the text. This is known as adding a salt. Another mitigation involves hashing the message several times (adding iterations). This increases the ammount of computations necessary to compute the hash. Hashing algorithms are also vulnerable to collision attacks. Such attacks involve altering the input to arrive at the same digest. This is particularly dangerous when using hashing functions to ensure the integrity of executable files. Both MD5 and SHA1 algorithms are vulnerable to collision attacks. \ No newline at end of file From cbd0d4cf5be96885d31bbbd2561ed75f1a4311d7 Mon Sep 17 00:00:00 2001 From: paul-ion Date: Sat, 28 Dec 2024 11:55:38 -0500 Subject: [PATCH 09/24] XOR challenge --- trainingportal/qna.js | 50 +++++++++++++++---- .../lessons/cryptoBreaker/crypto_xor.md | 44 ++++++++++++++++ .../lessons/cryptoBreaker/definitions.json | 8 +++ trainingportal/test/qna.test.js | 4 +- 4 files changed, 93 insertions(+), 13 deletions(-) create mode 100644 trainingportal/static/lessons/cryptoBreaker/crypto_xor.md diff --git a/trainingportal/qna.js b/trainingportal/qna.js index 9fc849cb..de999edc 100644 --- a/trainingportal/qna.js +++ b/trainingportal/qna.js @@ -22,17 +22,24 @@ let getSecretText = () => { } let getRes = (mes, code) => { - let digest = crypto.createHash('sha256').update(mes+masterSalt).digest('hex'); + let digest = crypto.createHash('sha256').update(mes.trim()+masterSalt).digest('hex'); return res = { code:code, digest:digest, } } -let getCode = (challengeId, key) => { - let mes = getSecretText(); - let code = DEFS[challengeId](mes, key); - return getRes(mes, code); +let getCode = (challengeId, message, key) => { + + let mes; + if(message){ + mes = message; + } + else{ + mes = getSecretText(); + } + + return DEFS[challengeId](mes, key); } let checkCode = (mes, digest) => { @@ -64,23 +71,23 @@ let caesarEnc = (mes, key) => { shifted+= String.fromCharCode(newCode); } } - return shifted + return getRes(mes, shifted); } let asciiEnc = (mes) => { let encoding = ""; for(let i=0;i { let code = util.btoa(mes); - return code; + return getRes(mes, code); } let hashEnc = (mes) => { @@ -90,16 +97,37 @@ let hashEnc = (mes) => { let hash = crypto.createHash('md5').update(word).digest('hex'); hashedWords.push(hash); } - return hashedWords.join("\n"); + return getRes(mes, hashedWords.join("\n")); } +let xorEnc = (message) => { + let key = message + let mes = "lorem ipsum dolor sit amet" + key = key.substring(0, mes.length); + + let cipher = ""; + for(let i=0; i < mes.length; i++){ + let mCode = mes.charCodeAt(i); + let kCode = key.charCodeAt(i); + let cCode = mCode ^ kCode; + if(cCode < 16){ + cipher += "0" + cCode.toString(16); + } + else{ + cipher += cCode.toString(16); + } + if(i { describe("base64CaesarEnc", () => { test("false for incorrect code",()=>{ - let res = qna.getCode("caesar"); + let res = qna.getCode("caesar",null,0); let wrong = "1234"; let check = qna.checkCode(wrong, res.digest); expect(check).toBeFalsy(); }); test("true for correct code",()=>{ - let shifted = qna.getCode("caesar",0); + let shifted = qna.getCode("caesar",null,0); //shifted did not change at all because the key is 0 let check = qna.checkCode(shifted.code, res.digest); expect(check).toBeTruthy(); From 66999ed91fdbfdf1e8343eab7a28be1c2cd2894a Mon Sep 17 00:00:00 2001 From: paul-ion Date: Sun, 29 Dec 2024 20:24:26 -0500 Subject: [PATCH 10/24] Vignere Cipher --- trainingportal/qna.js | 59 +++++++++++++++---- .../lessons/cryptoBreaker/crypto_caesar.md | 20 ++++++- .../lessons/cryptoBreaker/crypto_vignere.md | 35 +++++++++++ .../lessons/cryptoBreaker/crypto_xor.md | 2 +- .../lessons/cryptoBreaker/definitions.json | 12 +++- trainingportal/test/qna.test.js | 38 +++++++++--- 6 files changed, 141 insertions(+), 25 deletions(-) create mode 100644 trainingportal/static/lessons/cryptoBreaker/crypto_vignere.md diff --git a/trainingportal/qna.js b/trainingportal/qna.js index de999edc..8d9bb8d3 100644 --- a/trainingportal/qna.js +++ b/trainingportal/qna.js @@ -18,7 +18,7 @@ let getSecretText = () => { secretText += dictionary[index]; if(i { @@ -47,6 +47,20 @@ let checkCode = (mes, digest) => { return vfy === digest; } +let shiftChar = (char, diff) => { + if(char === ' '){ //skip spaces + return ' '; + } + + let newCode = char.charCodeAt(0) + diff + if(newCode > "Z".charCodeAt(0)){ + let diffToZ = newCode - "Z".charCodeAt(0); + newCode = "A".charCodeAt(0) + diffToZ - 1; + } + + return String.fromCharCode(newCode); +} + let caesarEnc = (mes, key) => { let diff; if(util.isNullOrUndefined(key)){ @@ -59,16 +73,37 @@ let caesarEnc = (mes, key) => { let shifted = ""; for(let i=0;i { + let keyArray = []; + let keyLen = 3; + + if(util.isNullOrUndefined(key)){ + for(let i = 0; i "z".charCodeAt(0)){ - let diffToZ = newCode - "z".charCodeAt(0); - newCode = "a".charCodeAt(0) + diffToZ - 1; - } - shifted+= String.fromCharCode(newCode); + if(kIdx === keyLen){ + kIdx = 0; } } return getRes(mes, shifted); @@ -102,7 +137,7 @@ let hashEnc = (mes) => { let xorEnc = (message) => { let key = message - let mes = "lorem ipsum dolor sit amet" + let mes = "LOREM IPSUM DOLOR SIT AMET" key = key.substring(0, mes.length); let cipher = ""; @@ -124,6 +159,7 @@ let xorEnc = (message) => { const DEFS = { "caesar": caesarEnc, + "vignere": vignereEnc, "ascii": asciiEnc, "base64": base64Enc, "hash": hashEnc, @@ -131,6 +167,7 @@ const DEFS = { } module.exports = { + DEFS, getCode, checkCode } diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md index 0f2b6e97..318d6173 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md @@ -12,10 +12,26 @@ We begin with one of the oldest methods used to hide a message, known to be used Shift letters by a number of positions. The number of positions is the key. ##### Example + ABCD becomes BCDE shifted right by one. + ABCD becomes ZABC shifted left by one. -##### Weakness -It can be easily deciphered by trying all possible shifts and there are as many shifts as letters in the alphabet. +##### Weaknesses +The Caesar cipher can be easily deciphered by trying all possible shifts and there are as many shifts as letters in the alphabet. This is also known as `brute forcing` or `cracking` the key. + +Another weakness is that the sequence of characters stays the same. + +For example using a shift of 10: + + `what is the name of the store` + `grkd sc dro xkwo yp dro cdybo` + +We can notice that `the`, one of the most frequent words in the English language, becomes `dro`. Using this knowledge we can reverse the key value of 10. + +Another aspect that can be used is the frequency of letters in a language. For example the letter `e` is the most frequently used in English. Indeed in the chosen text `e` appears 3 times while in the cipher we see `o` appearing 3 times. We can easily derive the key as being the number of positions from `e` to `o`. + +This is called `frequency analysis`. + diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_vignere.md b/trainingportal/static/lessons/cryptoBreaker/crypto_vignere.md new file mode 100644 index 00000000..c250b0c2 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_vignere.md @@ -0,0 +1,35 @@ + +The Vignere cipher is a variation of the Caesar cipher. Vignere uses longer keys, which are harder to guess. + +#### Algorithm +The key contains a sequence of characters which represent shifts. For example `A` would represent `0` shifts being the first letter of the alphabet. `B` would represent `1` shift. + +##### Example +Given the key `ABCD`. + + `AAAA` becomes `ABCD` + `ABCD` becomes `ACEG` + +##### Weakness +The Vignere was considered unbreakable for almost 200 years until the discovery of a method called Kasiski examination. + +This method takes advantage of the fact that for a large block of text with a fixed length key, common words tend to repeat. + +For example using the key `ABC` we have the following substitution. + + `what is the name of the store` + `wict ju tig nboe ph tig suqrf` + +In the case of the Caesar cipher we were able to determine the code for letter `e`, knowing that `e` must be the most common letter in the text. The Vignere cipher can address this problem if the key is sufficiently long. + +In the example we notice the word `tig` appears twice and assuming this word represents `the`, one of the most common English words, we can easily derive the key. + +Cracking the code becomes harder when longer keys are used and especially if multiple keys with different lengths are used. + +For example using `ABCDEFG` as key, it's becoming more difficult to recognize the common words. Applying one more transformation using a different key: `HIJK` increases the difficulty of cracking the cipher further. + + `what is the name of the store` + `wicw mx zhf pdqj uf ujh wyurf` <= `ABCDEFG` + `dqlg tf irm xmaq co eqp fibzo` <= `HIJK` + +Modern cryptographic algorithms use multiple rounds of transformations. Each round uses a different subkey derived from the initial key. \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md b/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md index ad8d0c4b..eb5fd068 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md @@ -39,6 +39,6 @@ If the attacker controls the input, they may easily derive the key by feeding th Even if the attacker doesn't control the input, if they can guess one message and have the cipher for that message, then they will be able to obtain the key and decrypt all subsequent messages. -The algorithm is also succeptible to frequency analysis as similar encrypted blocks will look the same encrypted. +The algorithm is also succeptible to frequency analysis as similar blocks will look the same encrypted. Finally if the key is poorly chosen, as in the example above, the key can be brute forced: meaning the attacker will try all possible key combinations. In the case of a key size of 1 byte, there are 256 combinations. \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/definitions.json b/trainingportal/static/lessons/cryptoBreaker/definitions.json index 80373a3b..de6cb5d7 100644 --- a/trainingportal/static/lessons/cryptoBreaker/definitions.json +++ b/trainingportal/static/lessons/cryptoBreaker/definitions.json @@ -8,7 +8,15 @@ "name":"Caesar Cipher", "description": "crypto_caesar.md", "type":"quiz", - "mission":"Decode the cipher below.", + "mission":"Decode the encrypted Latin text below.", + "codeBlockIds":[] + }, + { + "id":"vignere", + "name":"Vignere Cipher", + "description": "crypto_vignere.md", + "type":"quiz", + "mission":"Decode the cipher below. As before, the plain text is in Latin.", "codeBlockIds":[] }, { @@ -40,7 +48,7 @@ "name":"XOR Encryption", "description": "crypto_xor.md", "type":"quiz", - "mission":"The input is 'lorem ipsum dolor sit amet'. Find the XOR key.", + "mission":"The input is 'LOREM IPSUM DOLOR SIT AMET'. Find the XOR key.", "codeBlockIds":[] } ] diff --git a/trainingportal/test/qna.test.js b/trainingportal/test/qna.test.js index a0cf0752..2f370d69 100644 --- a/trainingportal/test/qna.test.js +++ b/trainingportal/test/qna.test.js @@ -1,22 +1,42 @@ const qna = require("../qna"); const util = require("../util"); +const assert = require("assert"); describe("qna", () => { - describe("base64CaesarEnc", () => { + + describe("crypto", () => { test("false for incorrect code",()=>{ - let res = qna.getCode("caesar",null,0); - let wrong = "1234"; - let check = qna.checkCode(wrong, res.digest); - expect(check).toBeFalsy(); + let text = "PLAIN_TEXT"; + for(let alg in qna.DEFS){ + let res = qna.getCode(alg,text); + let wrong = "1234"; + let check = qna.checkCode(wrong, res.digest); + assert(check === false, `Validation passed for wrong text using ${alg}`); + } + }); test("true for correct code",()=>{ - let shifted = qna.getCode("caesar",null,0); - //shifted did not change at all because the key is 0 - let check = qna.checkCode(shifted.code, res.digest); - expect(check).toBeTruthy(); + let text = "PLAIN_TEXT"; + for(let alg in qna.DEFS){ + let res = qna.getCode(alg,text); + let check = qna.checkCode(text, res.digest); + assert(check === true, `Validation failed for correct text using ${alg}`); + } + }); + + + test("vignere should return plain text for 'AAA'",()=>{ + let text = "PLAIN TEXT"; + let expected = "BYOUA HQKH" + let key = "MNO" + let res = qna.getCode("vignere",text,key); + assert.strictEqual(res.code, expected, "Did not result in the same cipher for key: 'MNO'"); + }); }); + + }); \ No newline at end of file From 990147c05d18e267dffd6407320570c7869597cd Mon Sep 17 00:00:00 2001 From: paul-ion Date: Wed, 1 Jan 2025 10:34:03 -0500 Subject: [PATCH 11/24] Password based key derivation --- trainingportal/qna.js | 53 ++++++++++++---- .../lessons/cryptoBreaker/crypto_caesar.md | 14 ++++- .../lessons/cryptoBreaker/crypto_hash.md | 2 +- .../lessons/cryptoBreaker/crypto_pbk.md | 63 +++++++++++++++++++ .../lessons/cryptoBreaker/definitions.json | 12 +++- trainingportal/test/qna.test.js | 9 +++ trainingportal/util.js | 2 +- 7 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 trainingportal/static/lessons/cryptoBreaker/crypto_pbk.md diff --git a/trainingportal/qna.js b/trainingportal/qna.js index 8d9bb8d3..71b6f946 100644 --- a/trainingportal/qna.js +++ b/trainingportal/qna.js @@ -109,10 +109,17 @@ let vignereEnc = (mes, key) => { return getRes(mes, shifted); } +let getASCIIHexCode = (no) => { + if(no < 16){ + return "0" + no.toString(16); + } + return no.toString(16).toUpperCase(); +} + let asciiEnc = (mes) => { let encoding = ""; for(let i=0;i { let key = message let mes = "LOREM IPSUM DOLOR SIT AMET" key = key.substring(0, mes.length); - + let keyArray = []; + for(let i=0;i { + let kIdx = 0; let cipher = ""; + for(let i=0; i < mes.length; i++){ let mCode = mes.charCodeAt(i); - let kCode = key.charCodeAt(i); + let kCode = key[kIdx]; let cCode = mCode ^ kCode; - if(cCode < 16){ - cipher += "0" + cCode.toString(16); - } - else{ - cipher += cCode.toString(16); - } + cipher += getASCIIHexCode(cCode); + kIdx++; + if(kIdx == key.length) kIdx = 0; if(i { + let passwordString = "LOREM"; + let saltString = "IPSUM"; + + let password = Buffer.from(passwordString); + let salt = Buffer.from(saltString); + + let key = crypto.pbkdf2Sync(password, salt, 1000, 32, "SHA256"); + let cipher = xorOp(mes,key); + return getRes(mes, cipher); +} + const DEFS = { "caesar": caesarEnc, "vignere": vignereEnc, "ascii": asciiEnc, "base64": base64Enc, "hash": hashEnc, - "xor": xorEnc + "xor": xorEnc, + "pbk": pbkEnc } module.exports = { DEFS, getCode, - checkCode + checkCode, + xorOp } diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md index 318d6173..9bb56a34 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md @@ -2,9 +2,19 @@ #### Welcome to the Encryption module. In this module you will learn about various ways in which information can be encoded and decoded. -To solve challenges you will need to execute various transformations on a block of given data. Online resources such as `dCode.fr`, `crackstation.net`, `hashes.com` and others offer tools that can help you in your journey. You may also use your programming language of choice and openssl. +To solve challenges you will need to execute various transformations on a block of given data. -Note: You're allowed to conduct offline brute force attacks, however **trying answer combinations in an automatic fashion using the portal is strictly forbidden**. +Online resources offer tools that can help you in your journey. +Here are a few recommendations: + +- `dCode.fr` : includes a large variety of encoding, hashing and encryption tools +- `criptii.com` : similar to `dCode.fr` +- `crackstation.net` : includes a large dictionary of words and numbers hashed with several different algorithms +- `hashes.net`: similar to `crackstation.net` + +You may also use the `openssl` command line utility or your programming/scripting language of choice. + +`Important Note: You're allowed to conduct offline brute force attacks, however trying answer combinations in an automatic fashion using the portal is strictly forbidden.` We begin with one of the oldest methods used to hide a message, known to be used by Julius Caesar. diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md index 7477604e..191f64e1 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md @@ -28,6 +28,6 @@ Most algorithms leverage the characteristics of the data to arrive at a unique v ##### Weaknesses -Digests can be pre-calculated making them as easy to reverse as an ASCII code. Indeed websites like `crackstation.net` or `hashes.com` contain large databases of pre-calculated digests. The best way to prevent the reversing hashed words is to concatenate a random string to the text. This is known as adding a salt. Another mitigation involves hashing the message several times (adding iterations). This increases the ammount of computations necessary to compute the hash. +Digests can be pre-calculated making them as easy to reverse as an ASCII code. Indeed websites like `crackstation.net` or `hashes.com` contain large databases of pre-calculated digests also known as rainbow tables. The best way to prevent the reversing hashed words is to concatenate a random string to the text. This is known as adding a salt. Another mitigation involves hashing the message several times (adding iterations). This increases the ammount of computations necessary to compute the hash. Hashing algorithms are also vulnerable to collision attacks. Such attacks involve altering the input to arrive at the same digest. This is particularly dangerous when using hashing functions to ensure the integrity of executable files. Both MD5 and SHA1 algorithms are vulnerable to collision attacks. \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_pbk.md b/trainingportal/static/lessons/cryptoBreaker/crypto_pbk.md new file mode 100644 index 00000000..11e7f1fe --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_pbk.md @@ -0,0 +1,63 @@ + +A password based key can be used to make a human chosen secret harder to guess. For example applying a key derivation function to a simple word like: `balloon` generates the 256 bits/32 bytes key: + + 35 1D A6 E0 E2 14 22 72 80 A4 19 3B 2B C4 BC 49 F7 82 AA C2 F3 EC 63 00 51 D9 8C 84 5C A6 33 4A + +This newly derived key can be used to generate a cipher that is much harder to break with cryptanalysis. + +In comparison if we were to use `balloon` as is the key would be: + + 62 61 6C 6C 6F 6F 6E + +Such a short key would increase the likelihood of repeated patterns in the cipher, allowing for the identification of common letters and common words. + +The process of turning a simple passphrase into a more complex key is also known as `key stretching`. + +#### Algorithm + +There are 3 well known classes of password based key derivation algorithms: + +- PBKDF: stands for Password Based Key Derivation Function, also used in the WPA wireless protocol +- BCRYPT: more resistant to cracking because it requires more memory +- SCRYPT: newer algorithm that is also resistant to dedicated cracking circuits + +The PBKDF2 algorithm combines the password with a given salt and then applies a hashing function such as SHA-256 for a given amount of iterations. + +The more iterations, the more compute intensive it is to crack the password. The salt introduces variability, making the password less likely to be found in a precomputed hash table. + +Password based key derivation functions are ideal for password storage as they can make cracking passwords impractical even for dictionary words. + +##### Example + +Algorithm: `PBKDF2` + +- Using `pass` as the password and `salt` as the salt +- Execute a hashing function such as `SHA256` on the password and the salt + + `H1` = HASH ( `pass`, `salt` ) + +- Execute the same hashing function again on the password with the previous hash as a salt + + `H2` = HASH ( `pass`, `H1` ) + +- Repeat by the number of iterations (For our example we will stop at 2 iterations) +- `XOR` the values for all iterations together + + `KEY` = `H1` ^ `H2` + +The wireless protocol WPA2 uses the following key derivation function: + + DK = PBKDF2(SHA1, passphrase, ssid, 4096, 256) //4096 iterations, and 256 bits key length + +Do you see any issues with the provided arguments? + +##### Weaknesses + +A PBK is as strong as the arguments given to the derivation function. If someone uses `password` and `salt` to generate a key, the likelihood a pre-computed hashes existing for all iterations increases. + +If the salt is known to the attacker that also makes the password easier to crack. In the case of WPA2 the ssid is broadcasted and visible to all your neighbours. + +Using a weak hashing algorithm may allow collisions, although the attacker would need to know the final key for the collision vulnerabilities to com into play. The bigger concern is that some algorithms such as MD5 may impose a shorter length key (16 bytes). A shorter key is easier to crack and increases the avenues for cyptanalysis. + + + diff --git a/trainingportal/static/lessons/cryptoBreaker/definitions.json b/trainingportal/static/lessons/cryptoBreaker/definitions.json index de6cb5d7..e59fe7bb 100644 --- a/trainingportal/static/lessons/cryptoBreaker/definitions.json +++ b/trainingportal/static/lessons/cryptoBreaker/definitions.json @@ -24,7 +24,7 @@ "name":"ASCII Encoding", "description": "crypto_ascii.md", "type":"quiz", - "mission":"Decode the text below.", + "mission":"Decode the text below using hexadecimal ASCII encoding.", "codeBlockIds":[] }, { @@ -48,7 +48,15 @@ "name":"XOR Encryption", "description": "crypto_xor.md", "type":"quiz", - "mission":"The input is 'LOREM IPSUM DOLOR SIT AMET'. Find the XOR key.", + "mission":"The plain text is 'LOREM IPSUM DOLOR SIT AMET'. Find the XOR key.", + "codeBlockIds":[] + }, + { + "id":"pbk", + "name":"Password Based Key", + "description": "crypto_pbk.md", + "type":"quiz", + "mission":"Decrypt the cipher below which was XOR encrypted with a key derived with PBKDF2 from the word `LOREM` using the salt `IPSUM` and 1000 iterations.", "codeBlockIds":[] } ] diff --git a/trainingportal/test/qna.test.js b/trainingportal/test/qna.test.js index 2f370d69..4adba91a 100644 --- a/trainingportal/test/qna.test.js +++ b/trainingportal/test/qna.test.js @@ -36,6 +36,15 @@ describe("qna", () => { }); + test("xorOp should return plain text for '0x0'",()=>{ + let text = "PLAIN TEXT"; + let expected = "50 4C 41 49 4E 20 54 45 58 54"; + let keyArray = [0]; + let cipher = qna.xorOp(text,keyArray) + assert.strictEqual(cipher, expected, "Did not result in the same cipher for key: '0x0'"); + + }); + }); diff --git a/trainingportal/util.js b/trainingportal/util.js index 0c661b77..737a11db 100644 --- a/trainingportal/util.js +++ b/trainingportal/util.js @@ -124,7 +124,7 @@ exports.getRandomInt = (min, max) => { if(min >= max) throw Error("getRandomInt min can't be greater than max"); let innerMax = max - min; - let val = Math.floor(Math.random() * innerMax); + let val = Math.round(Math.random() * innerMax); return min + val; } From 091cd23197bb3eb65608df9aaecd60fad72ef6aa Mon Sep 17 00:00:00 2001 From: paul-ion Date: Thu, 2 Jan 2025 10:20:20 -0500 Subject: [PATCH 12/24] Add a reference to HTTP traffic analysis --- trainingportal/static/lessons/cryptoBreaker/crypto_xor.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md b/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md index eb5fd068..3f46e091 100644 --- a/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md @@ -39,6 +39,14 @@ If the attacker controls the input, they may easily derive the key by feeding th Even if the attacker doesn't control the input, if they can guess one message and have the cipher for that message, then they will be able to obtain the key and decrypt all subsequent messages. +For example if XOR was used for encrypting HTTP traffic, the first line of an HTTP request to a website will most likely be: + + GET / HTTP/2 + +Similarly the first line of the HTTP response will be: + + HTTP 200 OK + The algorithm is also succeptible to frequency analysis as similar blocks will look the same encrypted. Finally if the key is poorly chosen, as in the example above, the key can be brute forced: meaning the attacker will try all possible key combinations. In the case of a key size of 1 byte, there are 256 combinations. \ No newline at end of file From f34b496686b909a7fc28070bbb150e8e2e53bb1c Mon Sep 17 00:00:00 2001 From: paul-ion Date: Thu, 2 Jan 2025 12:52:41 -0500 Subject: [PATCH 13/24] Improve challenge code verification and unit tests --- trainingportal/challenges.js | 19 +-- trainingportal/test/challenge.test.js | 236 +++++++++++++------------- 2 files changed, 121 insertions(+), 134 deletions(-) diff --git a/trainingportal/challenges.js b/trainingportal/challenges.js index e0b849be..e13ad425 100644 --- a/trainingportal/challenges.js +++ b/trainingportal/challenges.js @@ -484,21 +484,15 @@ let apiChallengeCode = async (req) => { if(util.isNullOrUndefined(modules[moduleId].skipMasterSalt) || modules[moduleId].skipMasterSalt===false){ ms = masterSalt; } - //either hex or base64 formats should work - //we're looking at the first 10 characters only for situations where the challenge code may get truncated - pcaps, IPS logs - let verificationHashB64; - let verificationHashHex; - if(challengeType === "quiz"){ - verificationHashB64 = crypto.createHash('sha256').update(answer+ms).digest('base64').substr(0,10); - verificationHashHex = crypto.createHash('sha256').update(answer+ms).digest('hex').substr(0,10); - } - else{ - verificationHashB64 = crypto.createHash('sha256').update(challengeId+req.user.codeSalt+ms).digest('base64').substr(0,10); - verificationHashHex = crypto.createHash('sha256').update(challengeId+req.user.codeSalt+ms).digest('hex').substr(0,10); + if(challengeType !== "quiz"){ + answer = challengeId+req.user.codeSalt; } - + //either hex or base64 formats should work + let verificationHashB64 = crypto.createHash('sha256').update(answer+ms).digest('base64'); + let verificationHashHex = crypto.createHash('sha256').update(answer+ms).digest('hex'); + if(challengeCode.indexOf(verificationHashB64)!==0 && challengeCode.indexOf(verificationHashHex)!==0){ if(challengeType === "quiz"){ throw Error("invalidAnswer"); @@ -507,6 +501,7 @@ let apiChallengeCode = async (req) => { throw Error("invalidCode"); } } + //success update challenge curChallengeObj.moduleId = moduleId; return insertChallengeEntry(req.user, curChallengeObj, moduleId); diff --git a/trainingportal/test/challenge.test.js b/trainingportal/test/challenge.test.js index 80f964ec..e642d046 100644 --- a/trainingportal/test/challenge.test.js +++ b/trainingportal/test/challenge.test.js @@ -79,10 +79,10 @@ describe('challengeTests', () => { describe('#isPermittedModule()', () => { test('should return false for secondDegreeBlackBelt', async () => { - assert.notEqual(user, null, "Failed test setup - user null"); + assert.notStrictEqual(user, null, "Failed test setup - user null"); let promise = challenges.isPermittedModule(user,"secondDegreeBlackBelt"); permitted = await promise; - assert.equal(permitted,false,"Shouldn't not be permitted"); + assert.strictEqual(permitted,false,"Shouldn't not be permitted"); return promise; }); }); @@ -119,17 +119,17 @@ describe('challengeTests', () => { test('should issue a badge', async () => { let result = await challenges.verifyModuleCompletion(user, "secondDegreeBlackBelt1"); - assert.equal(result,true,"Should have completed the module"); + assert.strictEqual(result,true,"Should have completed the module"); result = await challenges.verifyModuleCompletion(user, "secondDegreeBlackBelt2"); - assert.equal(result,true,"Should have completed the module"); + assert.strictEqual(result,true,"Should have completed the module"); let promise = db.fetchBadges(user.id); let badges = await promise; - assert.notEqual(null, badges, "badges should NOT be null"); - assert.equal(badges.length, 2, "Incorrect number of badges"); - assert.equal(badges[0].moduleId, "secondDegreeBlackBelt1", "Wrong badge module"); - assert.equal(badges[1].moduleId, "secondDegreeBlackBelt2", "Wrong badge module"); + assert.notStrictEqual(null, badges, "badges should NOT be null"); + assert.strictEqual(badges.length, 2, "Incorrect number of badges"); + assert.strictEqual(badges[0].moduleId, "secondDegreeBlackBelt1", "Wrong badge module"); + assert.strictEqual(badges[1].moduleId, "secondDegreeBlackBelt2", "Wrong badge module"); //cleanup return promise; }); @@ -160,7 +160,7 @@ describe('challengeTests', () => { let promise = challenges.getUserLevelForModule(user, "greenBelt"); let result = await promise; - assert.equal(result,1,"Should be at level 1"); + assert.strictEqual(result,1,"Should be at level 1"); //cleanup return promise; }); @@ -181,18 +181,18 @@ describe('challengeTests', () => { userId:92 }, user); //verify badge code - assert.notEqual(null, badgeCode, "badge code should not be null") + assert.notStrictEqual(null, badgeCode, "badge code should not be null") let uriDecoded = decodeURIComponent(badgeCode); let parts = uriDecoded.split("."); - assert.equal(2,parts.length,"badge code should be split by ."); + assert.strictEqual(2,parts.length,"badge code should be split by ."); //verify the hash matches let infoHash = crypto.createHash('sha256').update(parts[0]+masterSalt).digest('base64'); - assert.equal(infoHash, parts[1]); + assert.strictEqual(infoHash, parts[1]); let decoded = Buffer.from(parts[0],"Base64").toString(); let parsed = JSON.parse(decoded); - assert.notEqual(null, parsed.badgeInfo, "code.info.badgeInfo should not be null") - assert.notEqual(null, parsed.givenName, "code.info.givenName should not be null") - assert.notEqual(null, parsed.familyName, "code.info.firstName should not be null") + assert.notStrictEqual(null, parsed.badgeInfo, "code.info.badgeInfo should not be null") + assert.notStrictEqual(null, parsed.givenName, "code.info.givenName should not be null") + assert.notStrictEqual(null, parsed.familyName, "code.info.firstName should not be null") }); @@ -205,9 +205,9 @@ describe('challengeTests', () => { let parsed = challenges.verifyBadgeCode(badgeCode); - assert.notEqual(null, parsed.badgeInfo, "code.info.badgeInfo should not be null") - assert.notEqual(null, parsed.givenName, "code.info.givenName should not be null") - assert.notEqual(null, parsed.familyName, "code.info.firstName should not be null") + assert.notStrictEqual(null, parsed.badgeInfo, "code.info.badgeInfo should not be null") + assert.notStrictEqual(null, parsed.givenName, "code.info.givenName should not be null") + assert.notStrictEqual(null, parsed.familyName, "code.info.firstName should not be null") }); @@ -215,7 +215,7 @@ describe('challengeTests', () => { let parsed = challenges.verifyBadgeCode("eyJiYWRnZUluZm8iOnsibGluZTEiOiJTZWN1cmUgQ29kaW5nIiwibGluZTIiOiJCbGFjayBCZWx0IiwiYmciOiJibGFjayJ9LCJnaXZlbk5hbWUiOiJGaXJzdExldmVsVXAiLCJmYW1pbHlOYW1lIjoiTGFzdExldmVsVXAiLCJjb21wbGV0aW9uIjoiVGh1IEZlYiAxMSAyMDIxIDIyOjQzOjMxIEdNVC0wNTAwIChFYXN0ZXJuIFN0YW5kYXJkIFRpbWUpIiwiaWRIYXNoIjoiOGQyN2JhMzdjNSJ9.XYZ"); - assert.equal(null, parsed, "Expected null on wrong code") + assert.strictEqual(null, parsed, "Expected null on wrong code") }); }); @@ -232,42 +232,39 @@ describe('challengeTests', () => { }); test('should return invalid request if fields are missing', async () => { - let promise = challenges.apiChallengeCode({"body":{}}); + let error = null try{ - await promise; + await challenges.apiChallengeCode({"body":{}}); } catch(err){ - assert.notEqual(err,null,"Error is null"); - assert.equal(err.message,"invalidRequest","Wrong error code returned"); - promise = new Promise((resolve)=>{resolve("ok");}); + error = err; } - return promise; + assert.notStrictEqual(error,null,"Error is null"); + assert.strictEqual(error.message,"invalidRequest","Wrong error code returned"); }); test('should return invalid code if code is invalid', async () => { - let promise = challenges.apiChallengeCode({"body":{"moduleId":"blackBelt","challengeCode":"