hof 22.11.1 → 22.12.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 2025-11-20, Version 22.12.1 (Stable), @dk4g @jamiecarterHO
2
+
3
+ ### Infrastructure
4
+ - Updated CI/CD pipeline to test against Node.js 20.x, 22.x, and 24.x
5
+ - Updated Redis testing versions to 7 and 8
6
+ - Added `NODE_VERSION` environment variable for consistent Node.js version across jobs
7
+ - Updated release process to use Node.js 24 for tagging and publishing operations
8
+
9
+ ### Security
10
+ - Replaced deprecated `crypto.createCipher`/`crypto.createDecipher` with `crypto.createCipheriv`/`crypto.createDecipheriv`
11
+ - Added proper initialisation vector (IV) handling for enhanced security
12
+ - Enforced 32-byte session secret requirement for AES-256 encryption compatibility
13
+ - Removed insecure default session secret ('changethis') - now requires explicit configuration
14
+
15
+ ### Migration Notes
16
+ - **Session Reset Required**: Due to enhanced encryption security, existing user sessions will be invalidated and users will need to re-authenticate after this update
17
+ - **Session Secret**: You must now set a unique `SESSION_SECRET` environment variable of exactly 32 bytes for encryption compatibility.
18
+ For testing purposes, you can use the following command to generate a random value. For production environments, consult a security expert or refer to official cryptographic guidelines to generate a secure secret
19
+ `node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"`
20
+
1
21
  ## 2025-11-15, Version 22.11.0 (Stable), @Rhodine-orleans-lindsay
2
22
 
3
23
  ### Changed
@@ -51,7 +51,7 @@ const defaults = {
51
51
  },
52
52
  session: {
53
53
  ttl: process.env.SESSION_TTL || 1800,
54
- secret: process.env.SESSION_SECRET || 'changethis',
54
+ secret: process.env.SESSION_SECRET,
55
55
  name: process.env.SESSION_NAME || 'hod.sid',
56
56
  sanitiseInputs: false
57
57
  },
@@ -0,0 +1,102 @@
1
+
2
+ <!DOCTYPE html>
3
+ <!--[if lt IE 9]><html class="lte-ie8" lang="{{htmlLang}}"><![endif]-->
4
+ <!--[if gt IE 8]><!--><html lang="{{htmlLang}}" class="govuk-template"><!--<![endif]-->
5
+ <head>
6
+ <meta charset="utf-8" />
7
+ <title>{{$pageTitle}}{{/pageTitle}}</title>
8
+ {{$head}}{{/head}}
9
+
10
+ <link rel="shortcut icon" sizes="16x16 32x32 48x48" href="{{govukAssetPath}}images/favicon.ico" type="image/x-icon">
11
+ <link rel="mask-icon" href="{{govukAssetPath}}images/govuk-mask-icon.svg" color="#0b0c0c">
12
+ <link rel="apple-touch-icon" sizes="180x180" href="{{govukAssetPath}}images/govuk-apple-touch-icon-180x180.png">
13
+ <link rel="apple-touch-icon" sizes="167x167" href="{{govukAssetPath}}images/govuk-apple-touch-icon-167x167.png">
14
+ <link rel="apple-touch-icon" sizes="152x152" href="{{govukAssetPath}}images/govuk-apple-touch-icon-152x152.png">
15
+ <link rel="apple-touch-icon" href="{{govukAssetPath}}images/govuk-apple-touch-icon.png">
16
+
17
+
18
+ <meta name="theme-color" content="#0b0c0c" />
19
+
20
+ <meta name="viewport" content="width=device-width, initial-scale=1">
21
+
22
+
23
+ <meta property="og:image" content="{{govukAssetPath}}images/opengraph-image.png">
24
+ </head>
25
+
26
+ <body class="{{$bodyClasses}}{{/bodyClasses}} govuk-template__body js-enabled" >
27
+ <script {{#nonce}}nonce="{{nonce}}"{{/nonce}}>document.body.className = ((document.body.className) ? document.body.className + ' js-enabled' : 'js-enabled');</script>
28
+
29
+
30
+
31
+ <div id="global-cookie-message" class="gem-c-cookie-banner govuk-clearfix" data-module="cookie-banner" role="region" aria-label="cookie banner" data-nosnippet="">
32
+ {{$cookieMessage}}{{/cookieMessage}}
33
+ </div>
34
+
35
+ {{$bodyStart}}{{/bodyStart}}
36
+
37
+ <header role="banner" id="govuk-header" class="{{$headerClass}}{{/headerClass}}">
38
+ <div class="govuk-header__container govuk-width-container">
39
+
40
+ <div class="govuk-header__logo">
41
+ <a href="{{$homepageUrl}}https://www.gov.uk{{/homepageUrl}}" title="{{$logoLinkTitle}}Go to the GOV.UK homepage{{/logoLinkTitle}}" id="logo" class="govuk-header__link govuk-header__link--homepage" target="_blank" data-module="track-click" data-track-category="homeLinkClicked" data-track-action="homeHeader">
42
+ <span class="govuk-header__logotype">
43
+ <!--[if gt IE 8]><!-->
44
+ <svg aria-hidden="true" focusable="false" class="govuk-header__logotype-crown" xmlns="http://www.w3.org/2000/svg\" viewBox="0 0 32 30" height="30" width="32">
45
+ <path fill="currentColor" fill-rule="evenodd" d="M22.6 10.4c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4m-5.9 6.7c-.9.4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4m10.8-3.7c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s0 2-1 2.4m3.3 4.8c-1 .4-2-.1-2.4-1-.4-.9.1-2 1-2.4.9-.4 2 .1 2.4 1s-.1 2-1 2.4M17 4.7l2.3 1.2V2.5l-2.3.7-.2-.2.9-3h-3.4l.9 3-.2.2c-.1.1-2.3-.7-2.3-.7v3.4L15 4.7c.1.1.1.2.2.2l-1.3 4c-.1.2-.1.4-.1.6 0 1.1.8 2 1.9 2.2h.7c1-.2 1.9-1.1 1.9-2.1 0-.2 0-.4-.1-.6l-1.3-4c-.1-.2 0-.2.1-.3m-7.6 5.7c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s0 2 1 2.4m-5 3c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s.1 2 1 2.4m-3.2 4.8c.9.4 2-.1 2.4-1 .4-.9-.1-2-1-2.4-.9-.4-2 .1-2.4 1s0 2 1 2.4m14.8 11c4.4 0 8.6.3 12.3.8 1.1-4.5 2.4-7 3.7-8.8l-2.5-.9c.2 1.3.3 1.9 0 2.7-.4-.4-.8-1.1-1.1-2.3l-1.2 4c.7-.5 1.3-.8 2-.9-1.1 2.5-2.6 3.1-3.5 3-1.1-.2-1.7-1.2-1.5-2.1.3-1.2 1.5-1.5 2.1-.1 1.1-2.3-.8-3-2-2.3 1.9-1.9 2.1-3.5.6-5.6-2.1 1.6-2.1 3.2-1.2 5.5-1.2-1.4-3.2-.6-2.5 1.6.9-1.4 2.1-.5 1.9.8-.2 1.1-1.7 2.1-3.5 1.9-2.7-.2-2.9-2.1-2.9-3.6.7-.1 1.9.5 2.9 1.9l.4-4.3c-1.1 1.1-2.1 1.4-3.2 1.4.4-1.2 2.1-3 2.1-3h-5.4s1.7 1.9 2.1 3c-1.1 0-2.1-.2-3.2-1.4l.4 4.3c1-1.4 2.2-2 2.9-1.9-.1 1.5-.2 3.4-2.9 3.6-1.9.2-3.4-.8-3.5-1.9-.2-1.3 1-2.2 1.9-.8.7-2.3-1.2-3-2.5-1.6.9-2.2.9-3.9-1.2-5.5-1.5 2-1.3 3.7.6 5.6-1.2-.7-3.1 0-2 2.3.6-1.4 1.8-1.1 2.1.1.2.9-.3 1.9-1.5 2.1-.9.2-2.4-.5-3.5-3 .6 0 1.2.3 2 .9l-1.2-4c-.3 1.1-.7 1.9-1.1 2.3-.3-.8-.2-1.4 0-2.7l-2.9.9C1.3 23 2.6 25.5 3.7 30c3.7-.5 7.9-.8 12.3-.8"></path>
46
+ </svg>
47
+ <!--<![endif]-->
48
+ <!--[if IE 8]>
49
+ <img src="{{govukAssetPath}}images/govuk-logotype-tudor-crown.png" class="govuk-header__logotype-crown-fallback-image" width="32" height="30" alt="">
50
+ <![endif]-->
51
+ </span>
52
+ <span class="govuk-header__logotype-text">
53
+ {{$globalHeaderText}}GOV.UK{{/globalHeaderText}}
54
+ </span>
55
+ </a>
56
+ </div>
57
+ {{$insideHeader}}{{/insideHeader}}
58
+
59
+ {{$propositionHeader}}{{/propositionHeader}}
60
+ </div>
61
+ </header>
62
+
63
+
64
+ {{$afterHeader}}{{/afterHeader}}
65
+
66
+
67
+ {{$main}}{{/main}}
68
+
69
+ <footer class="govuk-footer" id="footer" role="contentinfo">
70
+
71
+ <div class="govuk-width-container">
72
+ {{$footerTop}}{{/footerTop}}
73
+
74
+ <div class="govuk-footer__meta">
75
+ <div class="govuk-footer__meta-item govuk-footer__meta-item--grow">
76
+ <h2 class="govuk-visually-hidden">Support links</h2>
77
+ {{$footerSupportLinks}}{{/footerSupportLinks}}
78
+
79
+ <svg aria-hidden="true" focusable="false" class="govuk-footer__licence-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 483.2 195.7" height="17" width="41">
80
+ <path fill="currentColor" d="M421.5 142.8V.1l-50.7 32.3v161.1h112.4v-50.7zm-122.3-9.6A47.12 47.12 0 0 1 221 97.8c0-26 21.1-47.1 47.1-47.1 16.7 0 31.4 8.7 39.7 21.8l42.7-27.2A97.63 97.63 0 0 0 268.1 0c-36.5 0-68.3 20.1-85.1 49.7A98 98 0 0 0 97.8 0C43.9 0 0 43.9 0 97.8s43.9 97.8 97.8 97.8c36.5 0 68.3-20.1 85.1-49.7a97.76 97.76 0 0 0 149.6 25.4l19.4 22.2h3v-87.8h-80l24.3 27.5zM97.8 145c-26 0-47.1-21.1-47.1-47.1s21.1-47.1 47.1-47.1 47.2 21 47.2 47S123.8 145 97.8 145"></path>
81
+ </svg>
82
+
83
+ <span class="govuk-footer__licence-description">{{$licenceMessage}}All content is available under the <a href="https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/" id="open-government-licence" class="govuk-footer__link" target="_blank" rel="license">Open Government Licence v3.0</a>, except where otherwise stated{{/licenceMessage}}</span>
84
+ </div>
85
+
86
+ <div class="govuk-footer__meta-item">
87
+ <a class="govuk-footer__link govuk-footer__copyright-logo" id="copyright-logo" target="_blank" href="https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/">{{$crownCopyrightMessage}}© Crown copyright{{/crownCopyrightMessage}}</a>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </footer>
92
+
93
+ <div id="global-app-error" class="app-error hidden"></div>
94
+
95
+
96
+ {{$bodyEnd}}{{/bodyEnd}}
97
+
98
+
99
+ <script {{#nonce}}nonce="{{nonce}}"{{/nonce}}>if (typeof window.GOVUK === 'undefined') document.body.className = document.body.className.replace('js-enabled', '');</script>
100
+
101
+ </body>
102
+ </html>
package/lib/encryption.js CHANGED
@@ -1,23 +1,49 @@
1
- /* eslint-disable */
2
1
  'use strict';
3
2
 
4
- const crypto = require('crypto');
3
+ const crypto = require('node:crypto');
5
4
  const algorithm = 'aes-256-cbc';
5
+ const ivLength = 16;
6
6
 
7
- module.exports = password => ({
8
-
9
- encrypt: text => {
10
- const cipher = crypto.createCipher(algorithm, password);
11
- let crypted = cipher.update(text, 'utf8', 'hex');
12
- crypted += cipher.final('hex');
13
- return crypted;
14
- },
15
-
16
- decrypt: text => {
17
- const decipher = crypto.createDecipher(algorithm, password);
18
- let dec = decipher.update(text, 'hex', 'utf8');
19
- dec += decipher.final('utf8');
20
- return dec;
7
+ /**
8
+ * Creates an encryption utility with AES-256-CBC algorithm.
9
+ * Provides encrypt and decrypt methods that use a random IV for each encryption operation.
10
+ *
11
+ * @module encryption
12
+ * @param {string|Buffer} secret - Must be exactly 32 bytes
13
+ * @returns {Object} Encryption utility object
14
+ * @throws {Error} If secret is not exactly 32 bytes
15
+ */
16
+ module.exports = secret => {
17
+ const encryptionKey = Buffer.from(secret, 'utf8');
18
+ if (encryptionKey.byteLength !== 32) {
19
+ throw new Error(`Encryption secret must be exactly 32 bytes. Provided: ${encryptionKey.byteLength} bytes.`);
21
20
  }
22
21
 
23
- });
22
+ return {
23
+ encrypt: text => {
24
+ try {
25
+ const iv = crypto.randomBytes(ivLength);
26
+ const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv);
27
+ let encrypted = cipher.update(text, 'utf8');
28
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
29
+ return iv.toString('hex') + ':' + encrypted.toString('hex');
30
+ } catch (error) {
31
+ throw new Error(`Encryption failed: ${error.message}`);
32
+ }
33
+ },
34
+
35
+ decrypt: text => {
36
+ try {
37
+ const textParts = text.split(':');
38
+ const iv = Buffer.from(textParts.shift(), 'hex');
39
+ const encryptedText = Buffer.from(textParts.join(':'), 'hex');
40
+ const decipher = crypto.createDecipheriv(algorithm, encryptionKey, iv);
41
+ let decrypted = decipher.update(encryptedText);
42
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
43
+ return decrypted.toString('utf8');
44
+ } catch (error) {
45
+ throw new Error(`Decryption failed: ${error.message}`);
46
+ }
47
+ }
48
+ };
49
+ };
package/lib/sessions.js CHANGED
@@ -10,8 +10,11 @@ const secureHttps = config => config.protocol === 'https' || config.env === 'pro
10
10
  module.exports = (app, config) => {
11
11
  const logger = config.logger || console;
12
12
 
13
- if (config.env === 'production' && config.session.secret === 'changethis') {
14
- throw new Error('Session secret must be set to a unique random string for production environments');
13
+ const secretBuffer = Buffer.from(config.session.secret, 'utf8');
14
+ if (secretBuffer.byteLength !== 32) {
15
+ throw new Error(
16
+ `Session secret must be exactly 32 bytes. Current: ${secretBuffer.byteLength} bytes.`
17
+ );
15
18
  }
16
19
 
17
20
  app.use(cookieParser(config.session.secret, {
package/lib/settings.js CHANGED
@@ -42,7 +42,7 @@ module.exports = async (app, config) => {
42
42
  viewsArray.slice().reverse().forEach(view => {
43
43
  const customViewPath = path.resolve(config.root, view);
44
44
  try {
45
- fs.accessSync(customViewPath, fs.F_OK);
45
+ fs.accessSync(customViewPath, fs.constants.F_OK);
46
46
  } catch (err) {
47
47
  throw new Error(`Cannot find views at ${customViewPath}`);
48
48
  }
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "hof",
3
3
  "description": "A bootstrap for HOF projects",
4
- "version": "22.11.1",
4
+ "version": "22.12.0-beta.0",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
7
7
  "author": "HomeOffice",
8
8
  "engines": {
9
- "node": ">=10.22.1",
10
- "npm": ">=6.14.0"
9
+ "node": ">=14.0.0"
11
10
  },
12
11
  "bin": {
13
12
  "hof-build": "./bin/hof-build",