Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: hash uinFin in domain layer #596

Merged
merged 7 commits into from
Nov 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 70 additions & 93 deletions src/app/controllers/myinfo.server.controller.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict'

const bcrypt = require('bcrypt')
const crypto = require('crypto')
const CircuitBreaker = require('opossum')
const to = require('await-to-js').default
const Promise = require('bluebird')
Expand All @@ -10,7 +9,6 @@ const mongoose = require('mongoose')
const moment = require('moment')
const { StatusCodes } = require('http-status-codes')

const { sessionSecret } = require('../../config/config')
const { createReqMeta } = require('../utils/request')
const logger = require('../../config/logger').createLoggerWithLabel(module)
const getMyInfoHashModel = require('../models/myinfo_hash.server.model').default
Expand Down Expand Up @@ -76,15 +74,10 @@ exports.addMyInfo = (myInfoService) => async (req, res, next) => {
// Set current form fields to the new prefilledFields.
req.form.form_fields = prefilledFields

const hashedUinFin = crypto
.createHmac('sha256', sessionSecret)
.update(uinFin)
.digest('hex')

Promise.props(readOnlyHashPromises)
.then((readOnlyHashes) => {
return MyInfoHash.updateHashes(
hashedUinFin,
uinFin,
formId,
readOnlyHashes,
myInfoService.spCookieMaxAge,
Expand Down Expand Up @@ -142,109 +135,93 @@ function _preHashCheckConversion(field) {
* @param {Object} res.locals.uinFin - UIN/FIN of form submitter
* @param {Object} next - Express next middleware function
*/
exports.verifyMyInfoVals = function (req, res, next) {
exports.verifyMyInfoVals = async function (req, res, next) {
const { authType } = req.form
const actualMyInfoFields = req.form.form_fields.filter(
(field) => field.myInfo && field.myInfo.attr,
)
if (authType === 'SP' && actualMyInfoFields.length > 0) {
const uinFin = res.locals.uinFin
const formObjId = req.form._id

let hashedUinFin = crypto
.createHmac('sha256', sessionSecret)
.update(uinFin)
.digest('hex')
MyInfoHash.findOne(
{ uinFin: hashedUinFin, form: formObjId },
(err, hashedObj) => {
if (err) {
logger.error({
message: 'Error retrieving MyInfo hash from database',
meta: {
action: 'verifyMyInfoVals',
...createReqMeta(req),
},
error: err,
})
return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({
message: 'MyInfo verification unavailable, please try again later.',
spcpSubmissionFailure: true,
})
let hashedFields
try {
hashedFields = await MyInfoHash.findHashes(uinFin, formObjId)
} catch (error) {
logger.error({
message: 'Error retrieving MyInfo hash from database',
meta: {
action: 'verifyMyInfoVals',
...createReqMeta(req),
},
error,
})
return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({
message: 'MyInfo verification unavailable, please try again later.',
spcpSubmissionFailure: true,
})
}
if (!hashedFields) {
logger.error({
message: `Unable to find MyInfo hashes for ${formObjId}`,
meta: {
action: 'verifyMyInfoVals',
...createReqMeta(req),
formId: formObjId,
},
})
return res.status(StatusCodes.GONE).json({
message: 'MyInfo verification expired, please refresh and try again.',
spcpSubmissionFailure: true,
})
}
// Fields from client submission
let clientFormFields = req.body.parsedResponses // responses were transformed in submissions.server.controller.js
let clientMyInfoFields = clientFormFields
.filter((field) => field.isVisible && field.myInfo && field.myInfo.attr)
.map(_preHashCheckConversion)

// compare hashed values to submission values
const bcryptCompares = clientMyInfoFields.map((clientField) => {
const expected = hashedFields[clientField.attr]
return expected
? bcrypt.compare(clientField.val, expected)
: Promise.resolve(true)
})
Promise.all(bcryptCompares)
.then((compare) => {
return {
compare, // Array<Boolean> indicating hash pass/fail
fail: compare.some((v) => !v), // Whether any hashes failed
}
})
.then((hashResults) => {
if (hashResults.fail) {
// Array of MyInfo attributes that failed verification
let hashFailedAttrs = _.zip(clientMyInfoFields, hashResults.compare)
.filter(([_, compare]) => compare === false)
.map(([clientField, _]) => clientField.attr)
mantariksh marked this conversation as resolved.
Show resolved Hide resolved

if (!hashedObj) {
logger.error({
message: `Unable to find MyInfo hashes for ${formObjId}`,
message: `Hash did not match for form ${formObjId}`,
meta: {
action: 'verifyMyInfoVals',
...createReqMeta(req),
formId: formObjId,
failedFields: hashFailedAttrs,
},
})
return res.status(StatusCodes.GONE).json({
message:
'MyInfo verification expired, please refresh and try again.',
return res.status(StatusCodes.UNAUTHORIZED).json({
message: 'MyInfo verification failed.',
spcpSubmissionFailure: true,
})
}
// Fields from client submission
let clientFormFields = req.body.parsedResponses // responses were transformed in submissions.server.controller.js
let clientMyInfoFields = clientFormFields
.filter(
(field) => field.isVisible && field.myInfo && field.myInfo.attr,
} else {
let verifiedKeys = _.intersection(
_.uniq(clientMyInfoFields.map((field) => field.attr)),
_.keys(hashedFields),
)
.map(_preHashCheckConversion)

// Fields from saved hash
let hashedFields = hashedObj.fields
// compare hashed values to submission values
const bcryptCompares = clientMyInfoFields.map((clientField) => {
const expected = hashedFields[clientField.attr]
return expected
? bcrypt.compare(clientField.val, expected)
: Promise.resolve(true)
})
Promise.all(bcryptCompares)
.then((compare) => {
return {
compare, // Array<Boolean> indicating hash pass/fail
fail: compare.some((v) => !v), // Whether any hashes failed
}
})
.then((hashResults) => {
if (hashResults.fail) {
// Array of MyInfo attributes that failed verification
let hashFailedAttrs = _.zip(
clientMyInfoFields,
hashResults.compare,
)
.filter(([_, compare]) => compare === false)
.map(([clientField, _]) => clientField.attr)

logger.error({
message: `Hash did not match for form ${formObjId}`,
meta: {
action: 'verifyMyInfoVals',
...createReqMeta(req),
failedFields: hashFailedAttrs,
},
})
return res.status(StatusCodes.UNAUTHORIZED).json({
message: 'MyInfo verification failed.',
spcpSubmissionFailure: true,
})
} else {
let verifiedKeys = _.intersection(
_.uniq(clientMyInfoFields.map((field) => field.attr)),
_.keys(hashedFields),
)
req.hashedFields = _.pick(hashedFields, verifiedKeys)
return next()
}
})
},
)
req.hashedFields = _.pick(hashedFields, verifiedKeys)
return next()
}
})
} else {
return next()
}
Expand Down
24 changes: 23 additions & 1 deletion src/app/models/myinfo_hash.server.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import crypto from 'crypto'
import { Mongoose, Schema } from 'mongoose'

import { sessionSecret } from '../../config/config'
import { IHashes, IMyInfoHashModel, IMyInfoHashSchema } from '../../types'

import { FORM_SCHEMA_ID } from './form.server.model'
Expand Down Expand Up @@ -45,11 +47,15 @@ MyInfoHashSchema.index({ expireAt: 1 }, { expireAfterSeconds: 0 })

MyInfoHashSchema.statics.updateHashes = async function (
this: IMyInfoHashModel,
hashedUinFin: string,
uinFin: string,
formId: string,
readOnlyHashes: IHashes,
spCookieMaxAge: number,
): Promise<IMyInfoHashSchema | null> {
const hashedUinFin = crypto
.createHmac('sha256', sessionSecret)
.update(uinFin)
.digest('hex')
return this.findOneAndUpdate(
{
uinFin: hashedUinFin,
Expand All @@ -65,6 +71,22 @@ MyInfoHashSchema.statics.updateHashes = async function (
)
}

MyInfoHashSchema.statics.findHashes = async function (
this: IMyInfoHashModel,
uinFin: string,
formId: string,
): Promise<IHashes | null> {
const hashedUinFin = crypto
.createHmac('sha256', sessionSecret)
.update(uinFin)
.digest('hex')
const hashInfo = await this.findOne({
uinFin: hashedUinFin,
form: formId,
})
return hashInfo ? hashInfo.fields : null
}

const compileMyInfoHashModel = (db: Mongoose) =>
db.model<IMyInfoHashSchema, IMyInfoHashModel>(
MYINFO_HASH_SCHEMA_ID,
Expand Down
3 changes: 2 additions & 1 deletion src/types/myinfo_hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ export interface IMyInfoHashSchema extends IMyInfoHash, Document {}

export interface IMyInfoHashModel extends Model<IMyInfoHashSchema> {
updateHashes: (
hashedUinFin: string,
uinFin: string,
formId: string,
readOnlyHashes: IHashes,
spCookieMaxAge: number,
) => Promise<IMyInfoHashSchema | null>
findHashes: (uinFin: string, formId: string) => Promise<IHashes | null>
}
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ const ALL_MYINFO_HASHES = {
'$2b$10$eLakOm88NEuUawlGHsiPjeaImpYT5ZBH1JyZUseIRL9kCtDRIKdCe',
}

const SESSION_SECRET = 'secret'
const SESSION_SECRET = process.env.SESSION_SECRET

const User = dbHandler.makeModel('user.server.model', 'User')
const Agency = dbHandler.makeModel('agency.server.model', 'Agency')
Expand All @@ -506,9 +506,6 @@ const Controller = spec(
'dist/backend/app/controllers/myinfo.server.controller',
{
mongoose: Object.assign(mongoose, { '@noCallThru': true }),
'../../config/config': {
sessionSecret: SESSION_SECRET,
},
},
)

Expand Down
Loading