NML Says

Data Security 10

Model Solution to Exercises from the Previous Lesson

This model solution is available from https://codeberg.org/arosano/restapiencr_done.git

Example 1. routes/users.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const express = require('express');
const router = express.Router();

const con = require('../controllers/controllers');

/* GET user register ie send form */
router.get('/register', function(req, res, next) {
    res.render('register', {
        title: 'Please Register',
        subtitle: 'Follow the embedded cues'
    });
});

router.post('/register', con.handleRegistration, function(req, res, next) {
    res.status(201).json({message: 'Registration succesful'});
});

router.get('/login', function(req, res, next) {
    res.render('login', {
        title: 'Please Login'
    });
});

router.post('/login', con.handleLogin, function(req, res, next) {
    res.status(200).json("Bearer " + res.locals.token);
}); 

router.patch('/toggleAdmin/:email', con.isAuth, con.isAdmin, con.toggleAdmin, function(req, res, next) {
    res.status(201).json('Update successful');
}); 

router.get('/user/:email', con.isAuth, con.isAdminOrSelf, function(req, res, next) {
    console.log(res.locals.user);
    res.json({user: res.locals.user});
});

module.exports = router;
Example 2. controllers/controllers.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
/* controllers.js */
require ('dotenv').config();

const bcrypt = require('bcryptjs');
const crypto = require('./markSkeltonEncr.js');
const jwt = require('jsonwebtoken');
const model = require('../models/dbhandlers');

const SU = 0;           // admin

module.exports = {

    handleLogin: async function(req, res, next) {
        let errmsg = 'Error in credentials\n';
        let errmsgt = 'Error in token signing\n';
        try {
            await model.getUser(req, res, next);
            let rc = await bcrypt.compare(req.body.password, res.locals.user.password);
            if (!rc)
                throw new Error(errmsg);

            const payload = { email: res.locals.user.email, profile: res.locals.user.profile };
            const lifetime = { expiresIn: '1h' };
            let token = await jwt.sign(payload, process.env.SECRET, lifetime);
            if (!token)
                throw new Error(errmsgt);
            res.locals.token = token;

            next();
        } catch (err) {
            return res.status(500).json({message: err.message});
        }
    },
    handleRegistration: async function(req, res, next) {
        try {
            let hash = await bcrypt.hash(req.body.password, parseInt(process.env.ROUNDS));
            res.locals.hash = hash;
            req.body.bio = await crypto.encrypt(req.body.bio);
            await model.insertUser(req, res, next);
            next();
        } catch (err) {
            return res.status(500).json({message: err.message});
        }
    },

    isAuth: async function(req, res, next) {
        try {
            let errmsg = 'You must be logged in';
            let token = req.headers.authorization && req.headers.authorization.split(' ')[1];
            if (!token)
                throw new Error(errmsg);
            errmsg = 'Failed to authenticate token';
            let rc = await jwt.verify(token, process.env.SECRET);
            if (!rc)
                throw new Error(errmsg);

            res.locals.authorized = true;
            res.locals.email = rc.email;
            res.locals.profile = rc.profile;
            next();
        } catch(err) {
            res.status(500).json({message: err.message});
        }
    },

    isAdmin: function(req, res, next) {
        try {
            if (res.locals.authorized && res.locals.profile == SU) 
                next();
            else
                throw new Error('You must be a logged in admin');
        } catch(err) {
            res.status(500).json({message: err.message});
        }
    },

    isAdminOrSelf: async function(req, res, next) {
        try {
            if (!res.locals.authorized)
                throw new Error('You must be logged in');
            if (res.locals.email == req.params.email
                || res.locals.profile == SU) { 
                req.body.email = req.params.email;
                await model.getUser(req, res, next);
                res.locals.user.bio = await crypto.decrypt(res.locals.user.bio);    
                next();
            } else
                throw new Error('You must be admin or self');
        } catch(err) {
            res.status(500).json({message: err.message});
        }
    },

    toggleAdmin: async function(req, res, next) {
        try {
            req.body.email = req.params.email;
            await model.getUser(req, res, next);
            if (!res.locals.user)
                throw new Error('User not found');
            if (res.locals.user.profile == SU) {
                let count = await model.countAdmins();
                if (count <= 1)
                    throw new Error('Cannot remove last admin');
                await model.toggleAdmin(req, res, next);
            } 
            next();
        } catch(err) {
            res.status(500).json({message: err.message});
        }
    }
}
Example 3. controllers/markSkeltonEncr.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* credit to https://mskelton.dev/bytes/encrypting-data-with-node-js */

const crypto = require ('node:crypto');

const algorithm = "aes-256-cbc";
const secretKey = process.env.SECRET;

if (!secretKey) {
    throw new Error("SECRET_KEY environment variable is required");
}

const key = crypto
    .createHash("sha512")
    .update(secretKey)
    .digest("hex")
    .substring(0, 32);

const IV = crypto.randomBytes(16);

module.exports = {
    encrypt: async function(plaintext) {
        const cipher = await crypto.createCipheriv(algorithm, Buffer.from(key), IV);
        let ciphertext = await cipher.update(plaintext, "utf-8", "hex");
        ciphertext += cipher.final("hex");
    
        // Package the IV and ciphertext plaintext together so it can be stored in a single
        // column in the plaintextbase.
        return IV.toString("hex") + ciphertext;
    },
    
    decrypt: async function(ciphertext) {
        // Unpackage the combined iv + ciphertext. Since we are using a fixed
        // size IV, we can hard code the slice length.
        const inputIV = ciphertext.slice(0, 32);
        const encrypted = ciphertext.slice(32);
        const decipher = await crypto.createDecipheriv(
            algorithm,
            Buffer.from(key),
            Buffer.from(inputIV, "hex"),
        );
    
        let plaintext = await decipher.update(encrypted, "hex", "utf-8");
        plaintext += decipher.final("utf-8");
        // console.log(`decr: ${plaintext}`);
        return plaintext;
    }
};
Example 4. models/dbhandlers.js
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
const path = require('path');
const sqlite3 = require("better-sqlite3");
const SU = 0;

// Start db connection
const connect = async function () {
    try {
        const db = await new sqlite3(path.resolve('db/sampleAPI.db'), {fileMustExist: true});
        return db;
    } catch (err) {
            console.error(err);
    }
};

module.exports = {
    countAdmins: async function() {
        try {
            let db = await connect();
            let sql = 'select count(*) as c from user where profile = ?';
            let query = db.prepare(sql);
            let row = await query.all(SU);
            return row[0].c;
        } catch (err) {
            return 0;
        }
    },

    getAllCities: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'select * from city';
            let query = db.prepare(sql);
            let rows = await query.all();
            return rows;
        } catch (err) {
            res.status(400).json(err.message);
        }
    },

    getAllContinents: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'select * from continent';
            let query = db.prepare(sql);
            let rows = await query.all();
            return rows;
        } catch (err) {
            res.status(400).json(err.message);
        }
    },

    getAllCountries: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'select * from country';
            let query = db.prepare(sql);
            let rows = await query.all();
            return rows;
        } catch (err) {
            res.status(400).json(err.message);
        }
    },

    getAllLanguages: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'select * from countrylanguage';
            let query = db.prepare(sql);
            let rows = await query.all();
            return rows;
        } catch (err) {
            res.status(400).json(err.message);
        }
    },
    getCitiesCtry: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'select * from city where countrycode = ?';
            let query = db.prepare(sql);
            let rows = await query.all(req.params.ctry);
            return rows;
        } catch (err) {
            res.status(400).json(err.message);
        }
    },

    getUser: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'select * from user where email = ?';
            let query = db.prepare(sql);
            let row = await query.get(req.body.email);
            res.locals.user = row;
        } catch (err) {
            res.status(400).json(err.message);
        }
    },

    insertUser: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'insert into user (email, password, bio) values(?, ?, ?)';
            let query = db.prepare(sql);
            let row = await query.run(req.body.email, res.locals.hash, req.body.bio);
            res.locals.user = row;
        } catch (err) {
            res.status(400).json(err.message);
        }
    },

    insertCity: async function (req, res, next) {
        try {
            let db = await connect();
            let sql = 'insert into city values(?, ?, ?, ?)';
            let query = db.prepare(sql);
            let row = await query.run(req.body.name, req.body.countrycode, req.body.district, req.body.population);
        } catch (err) {
            res.status(400).json(err.message);
        }
    },

    toggleAdmin: async function(req, res, next) {
        try {
            let db = await connect();
            await this.getUser(req, res, next);
            let pro = res.locals.user.profile;
            if (pro == SU) 
                pro = 99;
            else
                pro = SU;
            let sql = 'update user set profile = ? where email = ?';
            let query = db.prepare(sql);
            await query.run(pro, res.locals.user.email);
        } catch (err) {
            res.status(400).json(err.message);
        }
    }

}
Example 5. curlit1.sh
1
2
3
4
5
6
7
8
#!/usr/bin/env sh

TOKEN=`curl -s -d email=$1 -d password=$2 http://localhost:3000/users/login`
TOKENC=$(echo "$TOKEN" | sed 's/"//g')


curl -H "Authorization: ${TOKENC}" \
     -s http://localhost:3000/users/user/$3    # expect error if not self or admin

Today

Q & A

  • What was good?
  • What was not so good?
  • Did you miss anything?

About Exams

  • Procedure
  • Preparation
  • Recommendations

The End