diff --git a/trainingportal/challenges.js b/trainingportal/challenges.js index 31c5288..2e17588 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'))); @@ -104,21 +105,30 @@ let init = async () => { masterSalt=process.env.CHALLENGE_MASTER_SALT; } - let dbModuleVersion = await db.getModuleVersion(); - if(dbModuleVersion < moduleVer){ - util.log("New training modules version, updating module completion for all users.") - recreateBadgesOnModulesUpdate(); - db.updateModuleVersion(moduleVer); - } + try { + let dbModuleVersion = await db.getModuleVersion(); + if(dbModuleVersion < moduleVer){ + util.log("New training modules version, updating module completion for all users.") + recreateBadgesOnModulesUpdate(); + db.updateModuleVersion(moduleVer); + if(dbModuleVersion < moduleVer){ + util.log("New training modules version, updating module completion for all users.") + recreateBadgesOnModulesUpdate(); + db.updateModuleVersion(moduleVer); + } + } + } catch (error) { + console.log(`Error handling module version ${error.message}`); + } + } 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 +154,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 +180,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 +199,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 +218,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 +239,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 +254,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 +277,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 +305,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 +332,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 +359,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 +382,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 +430,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"); } @@ -466,13 +494,24 @@ apiChallengeCode = async (req) => { if(util.isNullOrUndefined(modules[moduleId].skipMasterSalt) || modules[moduleId].skipMasterSalt===false){ ms = masterSalt; } + + if(challengeType !== "quiz"){ + answer = challengeId+req.user.codeSalt; + } + //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 = 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){ - throw Error("invalidCode"); + if(challengeType === "quiz"){ + throw Error("invalidAnswer"); + } + else{ + throw Error("invalidCode"); + } } + //success update challenge curChallengeObj.moduleId = moduleId; return insertChallengeEntry(req.user, curChallengeObj, moduleId); @@ -482,7 +521,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 +548,12 @@ insertChallengeEntry = async (user,curChallengeObj, moduleId) => { - module.exports = { apiChallengeCode, badgrCall, getBadgeCode, getChallengeNames, getChallengeDefinitions, - getChallengeDefinitionsForUser, getDescription, getModules, getPermittedChallengesForUser, diff --git a/trainingportal/db.js b/trainingportal/db.js index c392c31..e7e6e9b 100644 --- a/trainingportal/db.js +++ b/trainingportal/db.js @@ -161,7 +161,9 @@ getPromise = (dbFunc, params) => { return promise; }; -init = async () => { + + +let init = async () => { var con = getConn(); var sql = ""; var dbSetup = ""; @@ -251,6 +253,9 @@ init = async () => { } }; +let initSync = async() => { + await init() +} //Creates a user in the database insertUser = function(user,errCb,doneCb){ @@ -655,6 +660,7 @@ module.exports = { fetchUsers, fetchUsersWithId, init, + initSync, insertBadge, insertChallengeEntry, insertUser, diff --git a/trainingportal/package-lock.json b/trainingportal/package-lock.json index eb7bb20..8f4f925 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 cc647be..78f2e03 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 0000000..da8f55e --- /dev/null +++ b/trainingportal/qna.js @@ -0,0 +1,244 @@ +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","adipiscing","elit","maecenas","mollis","nec","libero","non","sed","dictum","vel","ligula","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","vel","finibus","pretium","quis","ante","etiam","neque","id","bibendum","nisi","enim","gravida","diam","sed","urna","velit","id","ligula","quisque","neque","in","vestibulum","morbi","sit","amet","lectus","Sed","suscipit","velit","ac","magna","aliquam","eu","nisl","dictum","ac","ante","ut","nibh","bibendum","faucibus","egestas","vehicula","neque","aenean","lorem","velit","maximus","nec","placerat","sed","vel","ligula","vivamus","vitae","nisi","in","sit","amet","nulla","non","libero","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.trim()+masterSalt).digest('hex'); + return res = { + code:code, + digest:digest, + } +} + +let getCode = (challengeId, message, key) => { + + let mes; + if(message){ + mes = message; + } + else{ + mes = getSecretText(); + } + + return DEFS[challengeId](mes, key); +} + +let checkCode = (mes, digest) => { + let vfy = crypto.createHash('sha256').update(mes+masterSalt).digest('hex'); + 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)){ + diff = util.getRandomInt(10,20); + } + else{ + diff = key; + } + + let shifted = ""; + for(let i=0;i { + let keyArray = []; + let keyLen = 3; + let mes = "LOREM " + m; + + if(util.isNullOrUndefined(key)){ + for(let i = 0; i { + if(no < 16){ + return "0" + no.toString(16).toUpperCase(); + } + return no.toString(16).toUpperCase(); +} + +let asciiEnc = (mes) => { + let encoding = ""; + for(let i=0;i { + let code = util.btoa(mes); + return getRes(mes, code); +} + +let hashEnc = (mes) => { + let words = mes.split(" "); + let hashedWords = []; + let hashes = []; + for(let word of words){ + let hash = crypto.createHash('md5').update(word).digest('hex'); + if(hashedWords.indexOf(word) === -1){ + hashedWords.push(word); + hashes.push(hash); + } + } + return getRes(hashedWords.join(" "), hashes.join("\n")); +} + +let xorEnc = (message) => { + 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[kIdx]; + let cCode = mCode ^ kCode; + 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); +} + + +let analysisEnc = (mes) => { + let goldenKeyMaterial = mes; + let goldenKeyWords = goldenKeyMaterial.split(" "); + let goldenKeySalt = goldenKeyWords[0]; + let goldenKeyShift = goldenKeyWords[1]; + let goldenKeySaltHash = crypto.createHash('md5').update(goldenKeySalt).digest("hex"); + let goldenKeyShiftHash = crypto.createHash('md5').update(goldenKeyShift).digest("hex"); + let goldenKeyScramble = vigenereEnc(goldenKeyMaterial,goldenKeyShift).code; + let pass = Buffer.from(goldenKeyMaterial); + let salt = Buffer.from(goldenKeySalt); + let keyBytes = 32; + let keyAlg = "SHA256"; + let keyIter = 1000; + let goldenKey = crypto.pbkdf2Sync(pass, salt, keyIter, keyBytes, keyAlg).toString("hex"); + let keyInfo = { + "keyMaterialShifted": goldenKeyScramble, + "goldenKeyShiftHash": goldenKeyShiftHash, + "goldenKeySaltHash": goldenKeySaltHash, + "hashingFunction":"SHA256", + "iter":keyIter + } + let keyInfoB64 = util.btoa(JSON.stringify(keyInfo)); + let postData = `kmb64=${keyInfoB64}`; + let post = `POST / HTTP/1.1\n`; + post+=`Host: finance.biznis\n`; + post+=`Content-length: ${postData.length}\n\n`; + post+= postData; + + let mesKey = crypto.randomBytes(16); + let cipher = xorOp(post,mesKey); + return getRes(goldenKey, cipher); +} + +const DEFS = { + "crypto_caesar": caesarEnc, + "crypto_vigenere": vigenereEnc, + "crypto_ascii": asciiEnc, + "crypto_base64": base64Enc, + "crypto_hash": hashEnc, + "crypto_xor": xorEnc, + "crypto_pbk": pbkEnc, + "crypto_analysis": analysisEnc +} + +module.exports = { + DEFS, + getCode, + checkCode, + xorOp +} + diff --git a/trainingportal/server.js b/trainingportal/server.js index 40ce83f..9632b15 100644 --- a/trainingportal/server.js +++ b/trainingportal/server.js @@ -26,7 +26,7 @@ const uid = require('uid-safe'); const validator = require('validator'); const db = require(path.join(__dirname, 'db')); -db.init(); +db.initSync(); const auth = require(path.join(__dirname, 'auth')); const util = require(path.join(__dirname, 'util')); @@ -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 fbc7a9a..6a4930f 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}} @@ -71,15 +70,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/lessons/cryptoBreaker/crypto_analysis.md b/trainingportal/static/lessons/cryptoBreaker/crypto_analysis.md new file mode 100644 index 0000000..ac84c56 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_analysis.md @@ -0,0 +1,62 @@ +Welcome to the final lesson of this module. + +Cryptanalysis is the process of decrypting a cipher without knowing the key. We've briefly explored some forms of cryptanalysis so far, but now we get to put them all to work. + +Cryptanalysis has evolved over time alongside new ciphers being invented. In World War II, the British government brought together the brightest mathematicians in England to break the Enigma cipher. Alan Turing, one of the most influential minds in the development of computer science, was instrumental in cracking the cipher. He leveraged previous research by Polish cryptanalyst Jerzy Różycki to create a statistical analysis cracking technique called *Banburismus*. + +Some of the crypanalysis concepts that we have explored so far are: + +- Cracking/Brute-Force: Trying all key combinations +- Frequency Analysis: Identifying patterns in the cipher. For example identifying the word `the` or the letter `e` for English texts +- Known Plain-Text: Leveraging a public piece of information about the cipher. + +Another concept not yet covered is the `indicator`. The indicator is a special message exchanged by two parties in order to establish what key to use. Example of an indicator could be 42, which specifies that the two parties should use key number 42 from a list of keys known by both parties. To decrypt the cipher one would have to obtain the list of keys and locate number 42 in the list. In fact, the Enigma cracking efforts were greatly helped by a British commando unit who stole the keys used for the month of February. + +Another concept is the `depth`. Depth represents how many times the same key is used. The higher the depth the more data using the same key which increases the possibility of identifying patterns in the data. To reduce depth, keys need to be rotated frequently. + + + +#### About the Challenge + +In this challenge you will have to leverage all the basic data transformation methods learned so far to decrypt "the golden key". There are multiple keys and algorithms being used. You will have to determine the keys and the algorithms. + +You are given an intercepted cipher text for a client/server application. The intercepted message is an `indicator` which contains information about the golden key. It is being sent periodically to transmit a new the golden key which is then used to digitally sign transactions. The developers of the application have decided to implement a lightweigh message encryption algorithm because the application is used in financial transactions and has to have minimum latency. + +**NOTE: Writing your own encryption algorithm or using known weak ciphers to improve performance is a known fallacy. Cryptographic algorithms such as AES 256, at this point in time, have a very strong mathematic foundation and have evolved over multiple iterations to optimize performance and resilience to attacks.** + +You know that the application uses HTTP for communication. Having this insight you must determine the key and extract a randomly generated golden key from the message. + +The golden key is wrapped in several layers of encoding so you will need to recognize all types of transformations leading to the final value. + +#### Challenge Tips + +- Go back and read some of the previous lessons. They contain information that will help with this challenge. +- HTTP is a well known communication protocol, there are many common words. Keep trying until you reconstruct most of the key. +- If you recover part of the encryption key, pad the missing bytes with 0x0. This way when the key repeats you can uncover more of the message. +- In one of the previous lessons you've decrypted a key using the plain text and the cipher. That should point you to what algorithm is being used. +- Once you uncover more of the message, or you are able to infer the text, add the correct bytes to the key. Then copy the resulting longer key to a file and identify the repeating bytes. + +Example: + + //You uncovered the following key bytes: `1 2 3 4`. Now the message looks like this + "PLAIJ#UB]S" + //Add a 0 to the key: `1 2 3 4 0`. Now the message looks like this + "PLAIK TEXQ" + //Now you can probably guess the message but let's assume for the sake of the example that you only know 'TEXT' which gives you the last byte in the sequence `5`. Write all the bytes together + `1 2 3 4 0 1 2 3 4 5` + //Now identify the repeating bytes + `1 2 3 4 0` + `1 2 3 4 5` + //Replace `0` with `5` and apply the new key below to the cipher. + `1 2 3 4 5 1 2 3 4 5` + //Now you are able to decrypt the message + "PLAIN TEXT" + //Note that you don't need to repeat the byte sequence. You can simply use `1 2 3 4 5` as the key. + +In our example we used a 5 byte key, however key sizes are usually multiples of 2: 16 bytes, 32 bytes, 64 bytes. Start with 16 and go to higher lengths if needed. + + +#### References + +- [Wikipedia: Cryptanalysis](https://en.wikipedia.org/wiki/Cryptanalysis) +- [Wikipedia: Cryptanalysis of the Enigma](https://en.wikipedia.org/wiki/Cryptanalysis_of_the_Enigma) \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md b/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md new file mode 100644 index 0000000..cab1f7e --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_ascii.md @@ -0,0 +1,18 @@ + +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 in decimal, 41 42 43 44 in hexadecimal (Base16) + + +##### Weakness +It can be easily deciphered using the well known ASCII character map. + + +#### References + +[Wikipedia: ASCII](https://en.wikipedia.org/wiki/ASCII) \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_base64.md b/trainingportal/static/lessons/cryptoBreaker/crypto_base64.md new file mode 100644 index 0000000..e5d193b --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_base64.md @@ -0,0 +1,29 @@ + +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. + +#### References + +[Wikipedia: Base64](https://en.wikipedia.org/wiki/Base64) diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md new file mode 100644 index 0000000..5efaada --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_caesar.md @@ -0,0 +1,48 @@ + +#### 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. + +There are several online resources 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 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. + +#### 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 ZABC shifted left by one. + +##### 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`. + +#### References + +[Wikipedia: Caesar Cipher](https://en.wikipedia.org/wiki/Caesar_cipher) \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md new file mode 100644 index 0000000..56521f0 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_hash.md @@ -0,0 +1,37 @@ + +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 also known as rainbow tables. The best way to prevent 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 amount of computations necessary to calculate 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. + +#### References + +[Wikipedia: Cryptographic Hash Function](https://en.wikipedia.org/wiki/Cryptographic_hash_function) \ 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 0000000..7fd35f5 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_pbk.md @@ -0,0 +1,68 @@ + +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. + + +#### References + +[Wikipedia: PBKDF2](https://en.wikipedia.org/wiki/PBKDF2) + + + diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_vignere.md b/trainingportal/static/lessons/cryptoBreaker/crypto_vignere.md new file mode 100644 index 0000000..e6a5429 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_vignere.md @@ -0,0 +1,39 @@ + +The Vigenère cipher is a variation of the Caesar cipher. Vigenère 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 Vigenère 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 Vigenère 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. + +#### References + +[Wikipedia: Vigenère Cipher](https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher) \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md b/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md new file mode 100644 index 0000000..d134888 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/crypto_xor.md @@ -0,0 +1,56 @@ +XOR encryption is a very fast encryption method that leverages the `eXclusive OR` boolean operation. + +XOR outputs true whenever the inputs differ: + + A B C + 0 ^ 0 = 0 + 0 ^ 1 = 1 + 1 ^ 0 = 1 + 1 ^ 1 = 0 + + +XOR has the property that the result can be reversed to obtain the value of the inputs. This is useful for encryption. + + A ^ B = C + B ^ C = A + A ^ C = B + +#### Algorithm + +- A key is chosen. This key can be a text or it can be binary +- Perform a bitwise xor operation between each block of input data end each block of key data. +- To decrypt perform the same operation in reverse, applying the key to the cipher. + +##### Example + +ABCD is 65 66 67 68 in ASCII code. + +The binary (Base2) character sequence is 0100 0001, 0100 0010, 0100 0011, 0100 0100 + +Let's choose E as the key. E is `0100 0101`. + + 0100 0001 ^ 0100 0101 = 0000 0100 (0x04) + 0100 0010 ^ 0100 0101 = 0000 0111 (0x07) + 0100 0011 ^ 0100 0101 = 0000 0110 (0x06) + 0100 0100 ^ 0100 0101 = 0000 0001 (0x01) + +##### Weaknesses +If the attacker controls the input, they may easily derive the key by feeding the cryptographic function an array of 0s. + +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 1.1 traffic, the first line of an HTTP request to a website will most likely be: + + GET / HTTP/1.1 + +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. + +#### References + +[Wikipedia: XOR cipher](https://en.wikipedia.org/wiki/XOR_cipher) \ No newline at end of file diff --git a/trainingportal/static/lessons/cryptoBreaker/definitions.json b/trainingportal/static/lessons/cryptoBreaker/definitions.json new file mode 100644 index 0000000..eb04270 --- /dev/null +++ b/trainingportal/static/lessons/cryptoBreaker/definitions.json @@ -0,0 +1,72 @@ +[ + { + "level":0, + "name":"Crypto Breaker", + "challenges":[ + { + "id":"crypto_caesar", + "name":"Caesar Cipher", + "description": "crypto_caesar.md", + "type":"quiz", + "mission":"Decrypt the encrypted Latin text below.", + "codeBlockIds":[] + }, + { + "id":"crypto_vigenere", + "name":"Vigenère Cipher", + "description": "crypto_vigenere.md", + "type":"quiz", + "mission":"Decrypt the cipher below knowing that the first word is 'LOREM'.", + "codeBlockIds":[] + }, + { + "id":"crypto_ascii", + "name":"ASCII Encoding", + "description": "crypto_ascii.md", + "type":"quiz", + "mission":"Decode the text below using hexadecimal ASCII encoding.", + "codeBlockIds":[] + }, + { + "id":"crypto_base64", + "name":"Base64 Encoding", + "description": "crypto_base64.md", + "type":"quiz", + "mission":"Decode the text below.", + "codeBlockIds":[] + }, + { + "id":"crypto_hash", + "name":"One-Way Hash", + "description": "crypto_hash.md", + "type":"quiz", + "mission":"Find the text by cracking the digest of each word. Make sure the words are entered in the same order, separated by spaces.", + "codeBlockIds":[] + }, + { + "id":"crypto_xor", + "name":"XOR Encryption", + "description": "crypto_xor.md", + "type":"quiz", + "mission":"The plain text is 'LOREM IPSUM DOLOR SIT AMET'. Find the characters of the XOR key.", + "codeBlockIds":[] + }, + { + "id":"crypto_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 of SHA256 hashes.", + "codeBlockIds":[] + }, + { + "id":"crypto_analysis", + "name":"Cryptanalysis", + "description": "crypto_analysis.md", + "type":"quiz", + "mission":"Obtain the golden key.", + "codeBlockIds":[] + } + ] + } +] \ No newline at end of file diff --git a/trainingportal/static/lessons/modules.json b/trainingportal/static/lessons/modules.json index ec1ed09..688c94e 100644 --- a/trainingportal/static/lessons/modules.json +++ b/trainingportal/static/lessons/modules.json @@ -14,6 +14,19 @@ }, "requiredModules":[] }, + "cryptoBreaker":{ + "name":"Crypto Breaker - Part 1", + "summary":"Introduction to Cryptography", + "description":"Learn about basic data transformation methods, a bit on the history of ciphers, common cryptographic weaknesses and how to exploit them.", + "description2": "Includes 8 lessons. Estimated duration 2 hours.", + "badgeInfo":{ + "line1":"Secure Coding Dojo", + "line2":"Crypto Breaker I", + "line3":"", + "bg":"navy" + }, + "requiredModules":[] + }, "greenBelt":{ "name":"Green Belt", "summary":"Common software security flaws - part 1", diff --git a/trainingportal/static/main-app.js b/trainingportal/static/main-app.js index c0708c7..933d33b 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 439c015..830a830 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 5e09769..79596f0 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 b027124..477ceea 100644 --- a/trainingportal/test/challenge.test.js +++ b/trainingportal/test/challenge.test.js @@ -79,14 +79,24 @@ 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; }); }); + 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', () => { @@ -109,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; }); @@ -150,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; }); @@ -171,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") }); @@ -195,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") }); @@ -205,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") }); }); @@ -222,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":"