55 *
66 * Copies shared browser-version files (script.js, styles.css, assets/)
77 * from the repo root into desktop-app/resources/, downloads all remote CDN
8- * libraries locally for 100% offline capabilities, and generates a
9- * Neutralinojs-compatible index.html.
8+ * libraries locally for 100% offline capabilities, validates their cryptographic
9+ * integrity using SRI hashes (SHA-384), and generates a Neutralinojs-compatible index.html.
1010 */
1111
1212const fs = require ( "fs" ) ;
1313const path = require ( "path" ) ;
1414const https = require ( "https" ) ;
15+ const crypto = require ( "crypto" ) ;
1516
1617const ROOT_DIR = path . resolve ( __dirname , ".." ) ;
1718const RESOURCES_DIR = path . resolve ( __dirname , "resources" ) ;
@@ -45,43 +46,105 @@ console.log("✓ Copied styles.css → resources/styles.css");
4546copyDirSync ( path . join ( ROOT_DIR , "assets" ) , path . join ( RESOURCES_DIR , "assets" ) ) ;
4647console . log ( "✓ Copied assets/ → resources/assets/" ) ;
4748
48- // Download helper
49- function downloadFile ( url , destPath ) {
49+ /**
50+ * Validates the cryptographic integrity of a file against an expected SHA-384 hash.
51+ */
52+ function verifyIntegrity ( filePath , expectedSha384 ) {
5053 return new Promise ( ( resolve , reject ) => {
51- if ( fs . existsSync ( destPath ) && fs . statSync ( destPath ) . size > 0 ) {
52- resolve ( ) ;
54+ if ( ! expectedSha384 ) {
55+ resolve ( true ) ; // Skip validation if no hash is provided (e.g., relative fonts)
5356 return ;
5457 }
55- console . log ( `Downloading offline dependency: ${ path . basename ( destPath ) } ...` ) ;
56- https . get ( url , ( res ) => {
57- if ( res . statusCode !== 200 ) {
58- reject ( new Error ( `Failed to load ${ url } (${ res . statusCode } )` ) ) ;
59- return ;
58+
59+ const hash = crypto . createHash ( "sha384" ) ;
60+ const stream = fs . createReadStream ( filePath ) ;
61+
62+ stream . on ( "data" , data => hash . update ( data ) ) ;
63+ stream . on ( "end" , ( ) => {
64+ const calculated = "sha384-" + hash . digest ( "base64" ) ;
65+ if ( calculated === expectedSha384 ) {
66+ resolve ( true ) ;
67+ } else {
68+ reject ( new Error ( `Integrity mismatch for ${ path . basename ( filePath ) } :\nExpected: ${ expectedSha384 } \nCalculated: ${ calculated } ` ) ) ;
6069 }
61- const stream = fs . createWriteStream ( destPath ) ;
62- res . pipe ( stream ) ;
63- stream . on ( "finish" , ( ) => {
64- stream . close ( ) ;
65- resolve ( ) ;
66- } ) ;
67- } ) . on ( "error" , reject ) ;
70+ } ) ;
71+ stream . on ( "error" , reject ) ;
72+ } ) ;
73+ }
74+
75+ /**
76+ * Downloads a file from a URL and verifies its integrity.
77+ */
78+ function downloadFile ( url , destPath , expectedSha384 ) {
79+ return new Promise ( ( resolve , reject ) => {
80+ // If file already exists, verify its integrity before skipping
81+ if ( fs . existsSync ( destPath ) && fs . statSync ( destPath ) . size > 0 ) {
82+ verifyIntegrity ( destPath , expectedSha384 )
83+ . then ( ( ) => resolve ( ) )
84+ . catch ( ( ) => {
85+ console . log ( `↻ Cached file ${ path . basename ( destPath ) } failed integrity check. Re-downloading...` ) ;
86+ fs . unlinkSync ( destPath ) ;
87+ downloadAndVerify ( ) ;
88+ } ) ;
89+ return ;
90+ }
91+
92+ downloadAndVerify ( ) ;
93+
94+ function downloadAndVerify ( ) {
95+ console . log ( `Downloading offline dependency: ${ path . basename ( destPath ) } ...` ) ;
96+ https . get ( url , ( res ) => {
97+ if ( res . statusCode !== 200 ) {
98+ reject ( new Error ( `Failed to load ${ url } (${ res . statusCode } )` ) ) ;
99+ return ;
100+ }
101+ const stream = fs . createWriteStream ( destPath ) ;
102+ res . pipe ( stream ) ;
103+ stream . on ( "finish" , ( ) => {
104+ stream . close ( ) ;
105+
106+ // Verify integrity of downloaded file
107+ verifyIntegrity ( destPath , expectedSha384 )
108+ . then ( ( ) => resolve ( ) )
109+ . catch ( err => {
110+ // Delete corrupted file
111+ if ( fs . existsSync ( destPath ) ) {
112+ fs . unlinkSync ( destPath ) ;
113+ }
114+ reject ( err ) ;
115+ } ) ;
116+ } ) ;
117+ } ) . on ( "error" , reject ) ;
118+ }
68119 } ) ;
69120}
70121
71122async function prepareOfflineDependencies ( ) {
72- console . log ( "\nStarting Offline Assets Preparation..." ) ;
123+ console . log ( "\nStarting Secure Offline Assets Preparation..." ) ;
73124 let html = fs . readFileSync ( path . join ( ROOT_DIR , "index.html" ) , "utf-8" ) ;
74125
75- // Find all CDN script and link tags
76- const cdnRegex = / ( h r e f | s r c ) = " ( h t t p s : \/ \/ (?: c d n j s \. c l o u d f l a r e \. c o m | c d n \. j s d e l i v r \. n e t ) \/ [ ^ " ] + ) " / g;
126+ // Find all CDN script and link tags that match standard script/stylesheet declarations
127+ const tagRegex = / < ( l i n k | s c r i p t ) [ ^ > ] + (?: h r e f | s r c ) = " h t t p s : \/ \/ (?: c d n j s \. c l o u d f l a r e \. c o m | c d n \. j s d e l i v r \. n e t ) \/ [ ^ " ] + " [ ^ > ] * > / g;
77128 let match ;
78129 const downloads = [ ] ;
79130 const replacements = [ ] ;
80131
81- while ( ( match = cdnRegex . exec ( html ) ) !== null ) {
82- const attr = match [ 1 ] ;
83- const url = match [ 2 ] ;
132+ while ( ( match = tagRegex . exec ( html ) ) !== null ) {
133+ const fullTag = match [ 0 ] ;
84134
135+ // Extract url
136+ const urlMatch = / (?: h r e f | s r c ) = " ( [ ^ " ] + ) " / . exec ( fullTag ) ;
137+ if ( ! urlMatch ) continue ;
138+ const url = urlMatch [ 1 ] ;
139+
140+ // Extract integrity hash
141+ const integrityMatch = / i n t e g r i t y = " ( [ ^ " ] + ) " / . exec ( fullTag ) ;
142+ const expectedSha384 = integrityMatch ? integrityMatch [ 1 ] : null ;
143+
144+ if ( ! expectedSha384 ) {
145+ console . warn ( `⚠ Warning: CDN dependency is missing an integrity hash: ${ url } ` ) ;
146+ }
147+
85148 // Determine local filename - sanitize package version tags or query strings
86149 const urlPath = new URL ( url ) . pathname ;
87150 let filename = path . basename ( urlPath ) ;
@@ -90,27 +153,29 @@ async function prepareOfflineDependencies() {
90153 }
91154
92155 const localDest = path . join ( LIBS_DIR , filename ) ;
93- downloads . push ( downloadFile ( url , localDest ) ) ;
156+ downloads . push ( downloadFile ( url , localDest , expectedSha384 ) ) ;
94157
95158 // Queue replacement in HTML to point to local libs folder
159+ const attr = fullTag . includes ( "href=" ) ? "href" : "src" ;
96160 replacements . push ( {
97161 original : `${ attr } ="${ url } "` ,
98162 replaced : `${ attr } ="/libs/${ filename } "`
99163 } ) ;
100164 }
101165
102- // Also download the relative fonts loaded by bootstrap-icons
166+ // Also download the relative fonts loaded by bootstrap-icons (these are loaded by the stylesheet and do not have SRI tags)
103167 const fontDir = path . join ( LIBS_DIR , "fonts" ) ;
104168 fs . mkdirSync ( fontDir , { recursive : true } ) ;
105- downloads . push ( downloadFile ( "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2" , path . join ( fontDir , "bootstrap-icons.woff2" ) ) ) ;
106- downloads . push ( downloadFile ( "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff" , path . join ( fontDir , "bootstrap-icons.woff" ) ) ) ;
169+ downloads . push ( downloadFile ( "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2" , path . join ( fontDir , "bootstrap-icons.woff2" ) , null ) ) ;
170+ downloads . push ( downloadFile ( "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff" , path . join ( fontDir , "bootstrap-icons.woff" ) , null ) ) ;
107171
108- // Wait for all downloads to finish
172+ // Wait for all downloads and cryptographic validations to finish
109173 try {
110174 await Promise . all ( downloads ) ;
111- console . log ( "✓ All offline libraries successfully prepared ." ) ;
175+ console . log ( "✓ All offline libraries successfully downloaded and cryptographically validated ." ) ;
112176 } catch ( err ) {
113- console . warn ( "⚠ Failed to bundle some dependencies offline. Fallback to CDNs will occur." , err . message ) ;
177+ console . error ( "✗ Critical Security Error: Dependency integrity check failed!" , err . message ) ;
178+ process . exit ( 1 ) ; // Abort execution if a download fails validation
114179 }
115180
116181 // Apply replacements in HTML
@@ -142,4 +207,7 @@ async function prepareOfflineDependencies() {
142207 console . log ( "\nDone! Run `npm run dev` to start the desktop app." ) ;
143208}
144209
145- prepareOfflineDependencies ( ) . catch ( console . error ) ;
210+ prepareOfflineDependencies ( ) . catch ( err => {
211+ console . error ( "✗ Fatal Prepare Error:" , err ) ;
212+ process . exit ( 1 ) ;
213+ } ) ;
0 commit comments