11 | This template's origins arise from my work on My Budget (https://github.com/reZach/my-budget) beginning in 2019. I was building a free Electron application to manage your budget, and was doing the best I could to use and learn Electron. After I spent more time working on the project, I realized the practices I were using were not secure, and decided I needed to build an Electron template that could be used for the new (v2) budgeting application.
12 |
13 |
14 | As I began to work more and more on this template, my focus changed from building a budgeting app to making a secure electron template. Many people have offered their expertise and knowledge in making this template to the one it is today. To these people I say, thank you. I hope you make use of this template by building a wonderfully secure application!
15 |
16 |
17 | );
18 | }
19 | }
20 |
21 | export default About;
22 |
--------------------------------------------------------------------------------
/webpack.development.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require("html-webpack-plugin");
2 | const CspHtmlWebpackPlugin = require("csp-html-webpack-plugin");
3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
4 | const { merge } = require("webpack-merge");
5 | const base = require("./webpack.config");
6 | const path = require("path");
7 |
8 | module.exports = merge(base, {
9 | mode: "development",
10 | devtool: "source-map", // Show the source map so we can debug when developing locally
11 | devServer: {
12 | host: "localhost",
13 | port: "40992",
14 | hot: true, // Hot-reload this server if changes are detected
15 | compress: true, // Compress (gzip) files that are served
16 | static: {
17 | directory: path.resolve(__dirname, "app/dist"), // Where we serve the local dev server's files from
18 | watch: true, // Watch the directory for changes
19 | staticOptions: {
20 | ignored: /node_modules/ // Ignore this path, probably not needed since we define directory above
21 | }
22 | }
23 | },
24 | plugins: [
25 | new MiniCssExtractPlugin(),
26 | new HtmlWebpackPlugin({
27 | template: path.resolve(__dirname, "app/src/index.html"),
28 | filename: "index.html"
29 | }),
30 | new CspHtmlWebpackPlugin({
31 | "base-uri": ["'self'"],
32 | "object-src": ["'none'"],
33 | "script-src": ["'self'"],
34 | "style-src": ["'self'"],
35 | "frame-src": ["'none'"],
36 | "worker-src": ["'none'"]
37 | })
38 | ]
39 | })
--------------------------------------------------------------------------------
/app/src/pages/welcome/welcome.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ROUTES from "Constants/routes";
3 | import { Link } from "react-router-dom";
4 |
5 | class Welcome extends React.Component {
6 | render() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | Thank you for trying out the secure-electron-template!
15 |
16 |
17 | Please navigate to view the features of this template.
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
Samples
26 |
27 | Using the Electron store.
28 | Changing locales.
29 | Undo/redoing actions.
30 | Custom context menu.
31 | Sample image loaded.
32 |
33 |
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | export default Welcome;
41 |
--------------------------------------------------------------------------------
/webpack.production.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require("html-webpack-plugin");
2 | const CspHtmlWebpackPlugin = require("csp-html-webpack-plugin");
3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
4 | const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
5 | const { merge } = require("webpack-merge");
6 | const base = require("./webpack.config");
7 | const path = require("path");
8 |
9 | module.exports = merge(base, {
10 | mode: "production",
11 | devtool: false,
12 | plugins: [
13 | new MiniCssExtractPlugin(),
14 | new HtmlWebpackPlugin({
15 | template: path.resolve(__dirname, "app/src/index.html"),
16 | filename: "index.html",
17 | base: "app://rse"
18 | }),
19 | // You can paste your CSP in this website https://csp-evaluator.withgoogle.com/
20 | // for it to give you suggestions on how strong your CSP is
21 | new CspHtmlWebpackPlugin(
22 | {
23 | "base-uri": ["'self'"],
24 | "object-src": ["'none'"],
25 | "script-src": ["'self'"],
26 | "style-src": ["'self'"],
27 | "frame-src": ["'none'"],
28 | "worker-src": ["'none'"]
29 | },
30 | {
31 | hashEnabled: {
32 | "style-src": false
33 | }
34 | }
35 | )
36 | ],
37 | optimization: {
38 | minimize: true,
39 | minimizer: [
40 | "...", // This adds default minimizers to webpack. For JS, Terser is used. // https://webpack.js.org/configuration/optimization/#optimizationminimizer
41 | new CssMinimizerPlugin()
42 | ]
43 | }
44 | });
45 |
--------------------------------------------------------------------------------
/app/localization/i18n.config.js:
--------------------------------------------------------------------------------
1 | const i18n = require("i18next");
2 | const reactI18Next = require("react-i18next");
3 | const i18nBackend = require("i18next-electron-fs-backend").default;
4 | const whitelist = require("./whitelist");
5 |
6 | // On Mac, the folder for resources isn't
7 | // in the same directory as Linux/Windows;
8 | // https://www.electron.build/configuration/contents#extrafiles
9 | const isMac = window.api.i18nextElectronBackend.clientOptions.platform === "darwin";
10 | const isDev = window.api.i18nextElectronBackend.clientOptions.environment === "development";
11 | const prependPath = isMac && !isDev ? window.api.i18nextElectronBackend.clientOptions.resourcesPath : ".";
12 |
13 | i18n
14 | .use(i18nBackend)
15 | .use(reactI18Next.initReactI18next)
16 | .init({
17 | backend: {
18 | loadPath: prependPath + "/app/localization/locales/{{lng}}/{{ns}}.json",
19 | addPath: prependPath + "/app/localization/locales/{{lng}}/{{ns}}.missing.json",
20 | contextBridgeApiKey: "api" // needs to match first parameter of contextBridge.exposeInMainWorld in preload file; defaults to "api"
21 | },
22 | debug: false,
23 | namespace: "translation",
24 | saveMissing: true,
25 | saveMissingTo: "current",
26 | lng: "en",
27 | fallbackLng: false, // set to false when generating translation files locally
28 | supportedLngs: whitelist.langs
29 | });
30 |
31 | window.api.i18nextElectronBackend.onLanguageChange((args) => {
32 | i18n.changeLanguage(args.lng, (error, _t) => {
33 | if (error) {
34 | console.error(error);
35 | }
36 | });
37 | });
38 |
39 | module.exports = i18n;
40 |
--------------------------------------------------------------------------------
/test/spec.js:
--------------------------------------------------------------------------------
1 | const Application = require("spectron").Application;
2 | const assert = require("assert");
3 | const electronPath = require("electron");
4 | const path = require("path");
5 |
6 | // Sample code taken from:
7 | // https://github.com/electron-userland/spectron
8 | describe("Application launch", function () {
9 | this.timeout(10000);
10 |
11 | beforeEach(function () {
12 | this.app = new Application({
13 | // Your electron path can be any binary
14 | // i.e for OSX an example path could be '/Applications/MyApp.app/Contents/MacOS/MyApp'
15 | // But for the sake of the example we fetch it from our node_modules.
16 | path: electronPath,
17 |
18 | // Assuming you have the following directory structure
19 |
20 | // |__ my project
21 | // |__ ...
22 | // |__ main.js
23 | // |__ package.json
24 | // |__ index.html
25 | // |__ ...
26 | // |__ test
27 | // |__ spec.js <- You are here! ~ Well you should be.
28 |
29 | // The following line tells spectron to look and use the main.js file
30 | // and the package.json located 1 level above.
31 | args: [path.join(__dirname, '..')]
32 | })
33 | return this.app.start()
34 | })
35 |
36 | afterEach(function () {
37 | if (this.app && this.app.isRunning()) {
38 | return this.app.stop()
39 | }
40 | });
41 |
42 | it("shows an initial window", function () {
43 | return this.app.client.getWindowCount().then(function (count) {
44 | assert.strictEqual(count, 1);
45 | // Please note that getWindowCount() will return 2 if `dev tools` are opened.
46 | // assert.equal(count, 2)
47 | });
48 | });
49 | });
--------------------------------------------------------------------------------
/app/src/core/routes.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Routes, Route } from "react-router";
3 | import ROUTES from "Constants/routes";
4 | import loadable from "@loadable/component";
5 |
6 | // Load bundles asynchronously so that the initial render happens faster
7 | const Welcome = loadable(() =>
8 | import(/* webpackChunkName: "WelcomeChunk" */ "Pages/welcome/welcome")
9 | );
10 | const About = loadable(() =>
11 | import(/* webpackChunkName: "AboutChunk" */ "Pages/about/about")
12 | );
13 | const Motd = loadable(() =>
14 | import(/* webpackChunkName: "MotdChunk" */ "Pages/motd/motd")
15 | );
16 | const Localization = loadable(() =>
17 | import(
18 | /* webpackChunkName: "LocalizationChunk" */ "Pages/localization/localization"
19 | )
20 | );
21 | const UndoRedo = loadable(() =>
22 | import(/* webpackChunkName: "UndoRedoChunk" */ "Pages/undoredo/undoredo")
23 | );
24 | const ContextMenu = loadable(() =>
25 | import(/* webpackChunkName: "ContextMenuChunk" */ "Pages/contextmenu/contextmenu")
26 | );
27 | const Image = loadable(() =>
28 | import(/* webpackChunkName: "ContextMenuChunk" */ "Pages/image/image")
29 | );
30 |
31 | class AppRoutes extends React.Component {
32 | render() {
33 | return (
34 |
35 | }>
36 | }>
37 | }>
38 | }>
39 | }>
40 | }>
41 | }>
42 |
43 | );
44 | }
45 | }
46 |
47 | export default AppRoutes;
48 |
--------------------------------------------------------------------------------
/docs/app.md:
--------------------------------------------------------------------------------
1 | # App
2 | The main location where all of your application code lives. Inside this folder are 4 sub-folders:
3 | ```
4 | dist/
5 | electron/
6 | localization/
7 | src/
8 | ```
9 |
10 | #### dist
11 | This folder holds bundled files from webpack. You shouldn't be modifying anything in this folder, the files contained within will be regenerated upon build (dev or production).
12 |
13 | #### electron
14 | Electron-specific files. These would be the main file that creates the window (`main.js`), the menu bar (`menu.js`) or the preload script (`preload.js`).
15 |
16 | > In the main.js file you would configure the app window and any app specific event handlers or setup IPC (inter-process-communication). The menu bar is self-explanatory so I will skip saying anything about that file. The preload.js file is where you expose, [_safely_](https://blog.doyensec.com/2019/04/03/subverting-electron-apps-via-insecure-preload.html), node symbols so that your renderer process can use them.
17 |
18 | #### localization
19 | A folder that will holds localized files of translations for your app. This setup is assuming you would be using translations offline and not call out to a webservice to translate your app. If you'd like to use another method of translating, i18next names each provider a "backend;" you can browse the list of them [here](https://www.i18next.com/overview/plugins-and-utils#backends).
20 |
21 | > Note, there are two config files in this template. **i18n.config.js** is for the renderer process (ie. front-end) and **i18n.mainconfig.js** is used for the menu items. It is important that these two config files _match_ (logically speaking, not word-for-word) so that the translation files work between menu items and the renderer process.
22 |
23 | #### src
24 | Application-specific files, these are your js/css files and everything else you are used to. A more detailed look of this directory is [here](https://github.com/reZach/secure-electron-template/blob/master/docs/src.md).
--------------------------------------------------------------------------------
/app/src/pages/contextmenu/contextmenu.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SubItem from "Components/subitem/subitem";
3 |
4 | class ContextMenu extends React.Component {
5 | constructor(props) {
6 | super(props);
7 |
8 | this.state = {
9 | message: "",
10 | };
11 | }
12 |
13 | componentWillUnmount() {
14 | // Clear any existing bindings;
15 | // important on mac-os if the app is suspended
16 | // and resumed. Existing subscriptions must be cleared
17 | window.api.contextMenu.clearRendererBindings();
18 | }
19 |
20 | componentDidMount() {
21 | // Set up binding in code whenever the context menu item
22 | // of id "alert" is selected
23 | window.api.contextMenu.onReceive("loudAlert", function (args) {
24 | alert(
25 | `This alert was brought to you by secure-electron-context-menu by ${args.attributes.name}`
26 | );
27 |
28 | // Note - we have access to the "params" object as defined here: https://www.electronjs.org/docs/api/web-contents#event-context-menu
29 | // args.params
30 | });
31 | }
32 |
33 | render() {
34 | return (
35 |
36 |
37 |
38 |
42 | Context menu
43 |
44 |
Right-click the header above!
45 |
46 |
47 |
48 |
49 | {/* Demonstrating how to use the context menu with multiple items */}
50 |
51 |
52 |
53 |
54 |
55 | );
56 | }
57 | }
58 |
59 | export default ContextMenu;
60 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # FAQ
2 |
3 | ## How do I use the Material-UI framework in this template?
4 | Please see [this issue](https://github.com/reZach/secure-electron-template/issues/14).
5 |
6 | ## What files can I remove once I start working on my own project?
7 | If you clone secure-electron-template and want to build your own app, you'll be able to get rid of some of the repository's default files.
8 |
9 | Specifically, you can drop:
10 | * The docs folder
11 | * [At the root level, mostly config/github files]
12 | * .editorconfig
13 | * .prettierrc
14 | * CODE_OF_CONDUCT.md
15 | * README.md
16 |
17 | > Note: you should keep the LICENSE file in your project.
18 |
19 | ## How do I use Node's fs in this template?
20 | Please check out [this guide](https://github.com/reZach/secure-electron-template/blob/master/docs/newtoelectron.md).
21 |
22 | ## Do you have a plain JS version of the template?
23 | No, but you can start with this template and follow the steps [outlined here](https://github.com/reZach/secure-electron-template/issues/57#issuecomment-777891491).
24 |
25 | ## Can I use `yarn` to install dependencies?
26 | Yes, but you'll have to follow [a few steps](https://github.com/reZach/secure-electron-template/issues/62) to get it working.
27 |
28 | ## How do I set up my own license keys?
29 | Please refer to [these instructions](https://github.com/reZach/secure-electron-license-keys) first. If you have further questions, you may post a question in the appropriate repo.
30 |
31 | ## Can I use typescript with this template?
32 | Yes, you can! Simply convert any of the files in the app/src directory to a Typescript extension. If you desire to convert some of the Electron-related files to Typescript, I'd suggest you pull inspiration from the [discussion here](https://github.com/reZach/secure-electron-template/issues/47).
33 |
34 | #### Question not answered?
35 | Please [post an issue](https://github.com/reZach/secure-electron-template/issues/new) and we will add to this page with questions that you have!
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 | .DS_Store
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # TypeScript cache
43 | *.tsbuildinfo
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 | .env.test
63 |
64 | # parcel-bundler cache (https://parceljs.org/)
65 | .cache
66 |
67 | # next.js build output
68 | .next
69 |
70 | # nuxt.js build output
71 | .nuxt
72 |
73 | # vuepress build output
74 | .vuepress/dist
75 |
76 | # Serverless directories
77 | .serverless/
78 |
79 | # FuseBox cache
80 | .fusebox/
81 |
82 | # DynamoDB Local files
83 | .dynamodb/
84 |
85 | # Webpack
86 | .webpack/
87 |
88 | # Electron-Forge
89 | out/
90 |
91 | # Bundled files
92 | app/dist/
93 |
94 | # Packed apps
95 | dist/
96 |
97 | # VSCode specific
98 | .vscode/
99 |
100 | # Logfile specific for development builds
101 | dev-scripts/webpack-dev-server.log
102 | dev-scripts/webpack-dev-server-error.log
103 |
104 | # License-specific files
105 | license.data
106 | public.key
--------------------------------------------------------------------------------
/docs/scripts.md:
--------------------------------------------------------------------------------
1 | # Scripts
2 | This page is specific to the scripts in the package.json file; what they do and why we have them.
3 |
4 | #### Running locally
5 | To run the template locally, run `npm run dev`.
6 |
7 | When this command is run, it will make use of code within the **dev-scripts** folder. [See here](https://github.com/reZach/secure-electron-template/blob/master/docs/architecture.md#dev-scripts) if you'd like additional information.
8 |
9 | #### Running production
10 | You can test your production builds with the `npm run prod` command, this will load your application with electron and the production config of webpack. It is the production build that is used when packaging your application (below).
11 |
12 | #### Running E2E tests
13 | You can run E2E tests with the `npm run test` command.
14 |
15 | #### Packaging your application
16 | You can package up your application using any of the following commands:
17 | ```
18 | npm run dist-mac
19 | npm run dist-linux
20 | npm run dist-windows
21 | npm run dist-all
22 | ```
23 |
24 | These commands make use of [electron-builder](https://www.electron.build) to build your app for production.
25 |
26 | #### Generating translation files
27 | Translations for multiple languages can be generated automatically without manual effort. To create translations, run `npm run translate`.
28 | > Note - There are additional details/setup that must be done the first time in `app/electron/localization/translateMissing.js` before running the command successfully. There is also additional information in this file how the translation process works.
29 |
30 | #### Audit your application
31 | Thanks to [`@doyensec/electronegativity`](https://github.com/doyensec/electronegativity), we can audit that our application meets all of the secure practices as recommended by the Electron team. To run it, run `npm run audit-app`.
32 | > Note - there are limitations of AST/DOM parsing (which the package uses) to verify secure practices. Some results of the report are false positives (ie. `LIMIT_NAVIGATION_GLOBAL_CHECK` and `PERMISSION_REQUEST_HANDLER_GLOBAL_CHECK`).
--------------------------------------------------------------------------------
/docs/src.md:
--------------------------------------------------------------------------------
1 | # Src
2 | Here's what the template looks like on a fresh install:
3 | ```
4 | components/
5 | constants/
6 | core/
7 | pages/
8 | redux/
9 | index.html
10 | index.jsx
11 | ```
12 |
13 | #### components
14 | This folder holds reusable react components you may use in your application. This folder is different than the **pages** folder in that a page represents a container that would hold one to many components.
15 |
16 | > Think of a component as something _smaller_ than a page, like a reusable module or the like.
17 |
18 | #### constants
19 | Constant values that your app might need. All this folder has is a dictionary of keys/routes necessary for [react-router](https://github.com/ReactTraining/react-router) to work. If you were to add another page, this would be one file you'd have to modify.
20 |
21 | #### core
22 | Contains the "bones" that sets up redux as well as your routes. You'll also need to modify the routes file in this folder if you add another page in your app.
23 |
24 | #### pages
25 | Contains pages in your application. Think of a page as a distinct screen. If you had a multi-screen app, you'd need many pages.
26 |
27 | #### redux
28 | Contains all redux-specific files, such as slices, reducers and your redux store.
29 |
30 | #### index.html
31 | This file is the _template_ for your application. With some webpack plugins that are setup for the application, this file will be transformed into bundled .html file that your application will render. The bundled .html file lives in `./app/dist/`.
32 |
33 | When building the application for production, this file gets the [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag added it to load resources over a non-file:/// origin because [it is more secure](https://github.com/reZach/secure-electron-template/issues/2). Otherwise, the differences between production and non-production are identical.
34 |
35 | #### index.jsx
36 | The [entry point](https://webpack.js.org/concepts/entry-points/) of your application where webpack generates your application bundle code from. You likely won't need to touch this file at all, but it's important to know what it's there for.
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 | # Architecture
2 | This template is laid out in order to maintain a clear separation-of-concerns (SoC) and composability in order for you to take the template in any way you need to build your app. At the root level we have a few folders:
3 |
4 | ```
5 | app/
6 | dev-scripts/
7 | docs/
8 | resources/
9 | test/
10 | ```
11 |
12 | #### app
13 | Contains everything for your app. All of your js/css files will go here as well as the electron-specific code. You can go [here](https://github.com/reZach/secure-electron-template/blob/master/docs/app.md) to find more information about this directory.
14 |
15 | #### dev-scripts
16 | Due to limitations in running electron _after_ a webpack development server has been started [and successfully compiled], additional scripts that run the development Electron configuration are in this directory that ensure we only start our _development_ Electron configuration _after_ webpack has loaded completely.
17 |
18 | #### docs
19 | Houses documentation pages such as this one.
20 |
21 | #### resources
22 | Any resources your electron app needs in for building/distributing executables should go here - icons are a great example.
23 |
24 | #### test
25 | Contains [mocha](https://mochajs.org/) tests you may use for E2E (end-to-end) testing.
26 |
27 | ## configs
28 | At the root level we also have some configuration files.
29 |
30 | ```
31 | .babelrc
32 | package.json
33 | webpack configs
34 | ```
35 |
36 | #### .babelrc
37 | In the babel configuration file we've set up aliases in order for you to import files with a little less typing. More information can [be found here](https://www.npmjs.com/package/babel-plugin-module-resolver). There are also a few babel presets for ES2015 features and react (so that we can handle .jsx files).
38 |
39 | #### package.json
40 | Where all the NPM modules are stored, as well as build and package scripts. If you want more detail on these scripts, [head over here](https://github.com/reZach/secure-electron-template/blob/master/docs/scripts.md).
41 |
42 | #### webpack[.config|.development|.production].js
43 | These files hold the webpack config for the template. The base template, `webpack.config.js` is used for both environments (development and production) while the other two are used for their respective environment.
--------------------------------------------------------------------------------
/app/localization/whitelist.js:
--------------------------------------------------------------------------------
1 | // Contains a whitelist of languages for our app
2 | const whitelistMap = {
3 | af: "Afrikaans", //Afrikaans
4 | ar: "عربى", // Arabic
5 | am: "አማርኛ", // Amharic
6 | bg: "български", // Bulgarian
7 | ca: "Català", // Catalan
8 | cs: "čeština", // Czech
9 | da: "Dansk", // Danish
10 | de: "Deutsche", // German
11 | el: "Ελληνικά", // Greek
12 | en: "English",
13 | es: "Español", // Spanish
14 | et: "Eestlane", // Estonian
15 | fa: "فارسی", // Persian
16 | fi: "Suomalainen", // Finnish
17 | fil: "Pilipino", // Filipino
18 | fr: "Français", // French
19 | gu: "ગુજરાતી", // Gujarati
20 | he: "עברית", // Hebrew
21 | hi: "हिंदी", // Hindi
22 | hr: "Hrvatski", // Croatian
23 | hu: "Magyar", // Hungarian
24 | id: "Indonesia", // Indonesian
25 | it: "Italiano", // Italian
26 | ja: "日本語", // Japanese
27 | kn: "ಕನ್ನಡ", // Kannada
28 | ko: "한국어", // Korean
29 | lt: "Lietuvis", // Lithuanian
30 | lv: "Latvietis", // Latvian
31 | ml: "മലയാളം", // Malayalam
32 | mr: "मराठी", // Marathi
33 | ms: "Melayu", // Malay
34 | nl: "Nederlands", // Dutch
35 | no: "norsk", // Norwegian
36 | pl: "Polskie", // Polish
37 | pt: "Português", // Portuguese
38 | ro: "Română", // Romanian
39 | ru: "Pусский", // Russian
40 | sk: "Slovenský", // Slovak
41 | sr: "Српски", // Serbian
42 | sv: "Svenska", // Swedish
43 | sw: "Kiswahili", // Swahili
44 | ta: "தமிழ்", // Tamil
45 | te: "తెలుగు", // Telugu
46 | th: "ไทย", // Thai
47 | tr: "Türk", // Turkish
48 | uk: "Українська", // Ukranian
49 | vi: "Tiếng Việt", // Vietnamese
50 | zh_CN: "简体中文" // Chinese
51 | };
52 |
53 | const Whitelist = (function() {
54 | const keys = Object.keys(whitelistMap);
55 | const clickFunction = function(channel, lng, i18nextMainBackend) {
56 | return function(menuItem, browserWindow, event) {
57 |
58 | // Solely within the top menu
59 | i18nextMainBackend.changeLanguage(lng);
60 |
61 | // Between renderer > main process
62 | browserWindow.webContents.send(channel, {
63 | lng
64 | });
65 | };
66 | };
67 |
68 | return {
69 | langs: keys,
70 | buildSubmenu: function(channel, i18nextMainBackend) {
71 | let submenu = [];
72 |
73 | for (const key of keys) {
74 | submenu.push({
75 | label: whitelistMap[key],
76 | click: clickFunction(channel, key, i18nextMainBackend)
77 | });
78 | }
79 |
80 | return submenu;
81 | }
82 | };
83 | })();
84 |
85 | module.exports = Whitelist;
86 |
--------------------------------------------------------------------------------
/app/src/pages/motd/motd.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { changeMessage } from "Redux/components/home/homeSlice";
4 | import {
5 | writeConfigRequest,
6 | useConfigInMainRequest,
7 | } from "secure-electron-store";
8 |
9 | class Motd extends React.Component {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | message: "",
15 | };
16 |
17 | this.onChangeMessage = this.onChangeMessage.bind(this);
18 | this.onSubmitMessage = this.onSubmitMessage.bind(this);
19 | }
20 |
21 | componentDidMount() {
22 | // Request so that the main process can use the store
23 | window.api.store.send(useConfigInMainRequest);
24 | }
25 |
26 | onChangeMessage(event) {
27 | const { value } = event.target;
28 | this.setState((_state) => ({
29 | message: value,
30 | }));
31 | }
32 |
33 | onSubmitMessage(event) {
34 | event.preventDefault(); // prevent navigation
35 | this.props.changeMessage(this.state.message); // update redux store
36 | window.api.store.send(writeConfigRequest, "motd", this.state.message); // save message to store (persist)
37 |
38 | // reset
39 | this.setState((_state) => ({
40 | message: "",
41 | }));
42 | }
43 |
44 | render() {
45 | return (
46 |
47 |
48 |
49 |
{this.props.home.message}
50 |
51 | Your message of the day will persist
52 | if you close and re-open the app.
53 |
54 |
55 |
56 |
57 |
58 |
71 |
72 |
73 |
74 | );
75 | }
76 | }
77 |
78 | const mapStateToProps = (state, _props) => ({
79 | home: state.home,
80 | });
81 | const mapDispatch = { changeMessage };
82 |
83 | export default connect(mapStateToProps, mapDispatch)(Motd);
84 |
--------------------------------------------------------------------------------
/docs/secureapps.md:
--------------------------------------------------------------------------------
1 | # Building a secure app
2 | What makes an app secure? Generally this means to follow the principle of **least privilege**, that is, to only give you the bare minimum necessary privileges necessary. This means your app should not request administrator access if it doesn't need it, and unnecessary libraries should [not be included if not used](https://martinfowler.com/bliki/Yagni.html).
3 |
4 | Before electron v5, this concept wasn't followed as closely as it probably should have been. Electron apps designed pre-v5 were built like this:
5 |
6 | 
7 |
8 | The bridge between the renderer and main components were the remote module and nodeIntegration. Node integration is what makes electron so powerful, but also [very vulnerable to hacking](https://snyk.io/vuln/npm:electron). Tightly integrating the node modules in the renderer (or interactible/visible parts of your app) expose you to RCE and XSS, to name a few problems.
9 |
10 | [Beginning with version 5](https://electronjs.org/docs/api/breaking-changes#planned-breaking-api-changes-50), electron by default is turning off these unsafe options and preferring more safe ones by default. The communication that happens between the renderer and main process is decoupled, and more secure:
11 |
12 | 
13 |
14 | IPC (inter-process communication) can be used to exchange messages between processes, and the preload script can extend the capabilites of the renderer process (e.g: inject modules from the main processes). This separation of concern gives us the ability to apply **the principle of least privilege**.
15 |
16 | My personal experience with electron, is that their [release schedule is crazy](https://electronjs.org/docs/tutorial/electron-timelines), with only a few months between each major release. Electron is a relatively young framework, but it's under very active development which makes it hard to keep up with! This quick release cadence is in part in place to keep [bugs fixed sooner than later](https://electronjs.org/docs/tutorial/security#17-use-a-current-version-of-electron). While it's good for security, it can sometimes be tedious for developers to keep up to date with the framework.
17 |
18 | What's even more troubling is many of the frameworks that integrate with electron are still applying these old/insecure (pre v5) patterns. If you wish to be more secure then you might have to rewrite some of them.
19 |
20 | There is a little bit more work required to use IPC, but I'm working on it! Regardless, electron developers have worked hard to enable application developers to write more secure apps!
21 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | - cron: '0 13 * * 3'
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ['javascript']
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 | with:
35 | # We must fetch at least the immediate parents so that if this is
36 | # a pull request then we can checkout the head.
37 | fetch-depth: 2
38 |
39 | # If this run was triggered by a pull request event, then checkout
40 | # the head of the pull request instead of the merge commit.
41 | - run: git checkout HEAD^2
42 | if: ${{ github.event_name == 'pull_request' }}
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/app/electron/protocol.js:
--------------------------------------------------------------------------------
1 | /*
2 | Reasonably Secure Electron
3 | Copyright (C) 2021 Bishop Fox
4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 |
6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 |
8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 | -------------------------------------------------------------------------
10 | Implementing a custom protocol achieves two goals:
11 | 1) Allows us to use ES6 modules/targets for Angular
12 | 2) Avoids running the app in a file:// origin
13 | */
14 |
15 | const fs = require("fs");
16 | const path = require("path");
17 |
18 | const DIST_PATH = path.join(__dirname, "../../app/dist");
19 | const scheme = "app";
20 |
21 | const mimeTypes = {
22 | ".js": "text/javascript",
23 | ".mjs": "text/javascript",
24 | ".html": "text/html",
25 | ".htm": "text/html",
26 | ".json": "application/json",
27 | ".css": "text/css",
28 | ".svg": "image/svg+xml",
29 | ".ico": "image/vnd.microsoft.icon",
30 | ".png": "image/png",
31 | ".jpg": "image/jpeg",
32 | ".map": "text/plain"
33 | };
34 |
35 | function charset(mimeExt) {
36 | return [".html", ".htm", ".js", ".mjs"].some((m) => m === mimeExt) ?
37 | "utf-8" :
38 | null;
39 | }
40 |
41 | function mime(filename) {
42 | const mimeExt = path.extname(`${filename || ""}`).toLowerCase();
43 | const mimeType = mimeTypes[mimeExt];
44 | return mimeType ? { mimeExt, mimeType } : { mimeExt: null, mimeType: null };
45 | }
46 |
47 | function requestHandler(req, next) {
48 | const reqUrl = new URL(req.url);
49 | let reqPath = path.normalize(reqUrl.pathname);
50 | if (reqPath === "/") {
51 | reqPath = "/index.html";
52 | }
53 | const reqFilename = path.basename(reqPath);
54 | fs.readFile(path.join(DIST_PATH, reqPath), (err, data) => {
55 | const { mimeExt, mimeType } = mime(reqFilename);
56 | if (!err && mimeType !== null) {
57 | next({
58 | mimeType,
59 | charset: charset(mimeExt),
60 | data
61 | });
62 | } else {
63 | console.error(err);
64 | }
65 | });
66 | }
67 |
68 | module.exports = {
69 | scheme,
70 | requestHandler
71 | };
--------------------------------------------------------------------------------
/dev-scripts/launchDevServer.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const {
3 | exec
4 | } = require("child_process");
5 | const logFilePath = "./dev-scripts/webpack-dev-server.log";
6 | const errorLogFilePath = "./dev-scripts/webpack-dev-server-error.log";
7 | const interval = 100;
8 | const showHint = 600 * 3; // show hint after 3 minutes (60 sec * 3)
9 | let hintCounter = 1;
10 |
11 | // Poll webpack-dev-server.log until the webpack bundle has compiled successfully
12 | const intervalId = setInterval(function () {
13 | try {
14 | if (fs.existsSync(logFilePath)) {
15 | const log = fs.readFileSync(logFilePath, {
16 | encoding: "utf8"
17 | });
18 |
19 | // "compiled successfully" is the string we need to find
20 | // to know that webpack is done bundling everything and we
21 | // can load our Electron app with no issues. We split up the
22 | // validation because the output contains non-standard characters.
23 | const compiled = log.indexOf("compiled");
24 | if (compiled >= 0 && log.indexOf("successfully", compiled) >= 0) {
25 | console.log("Webpack development server is ready, launching Electron app.");
26 | clearInterval(intervalId);
27 |
28 | // Start our electron app
29 | const electronProcess = exec("cross-env NODE_ENV=development electron .");
30 | electronProcess.stdout.on("data", function(data) {
31 | process.stdout.write(data);
32 | });
33 | electronProcess.stderr.on("data", function(data) {
34 | process.stdout.write(data);
35 | });
36 | } else if (log.indexOf("Module build failed") >= 0) {
37 |
38 | if (fs.existsSync(errorLogFilePath)) {
39 | const errorLog = fs.readFileSync(errorLogFilePath, {
40 | encoding: "utf8"
41 | });
42 |
43 | console.log(errorLog);
44 | console.log(`Webpack failed to compile; this error has also been logged to '${errorLogFilePath}'.`);
45 | clearInterval(intervalId);
46 |
47 | return process.exit(1);
48 | } else {
49 | console.log("Webpack failed to compile, but the error is unknown.")
50 | clearInterval(intervalId);
51 |
52 | return process.exit(1);
53 | }
54 | } else {
55 | hintCounter++;
56 |
57 | // Show hint so user is not waiting/does not know where to
58 | // look for an error if it has been thrown and/or we are stuck
59 | if (hintCounter > showHint){
60 | console.error(`Webpack is likely failing for an unknown reason, please check '${errorLogFilePath}' for more details.`);
61 | clearInterval(intervalId);
62 |
63 | return process.exit(1);
64 | }
65 | }
66 | }
67 | } catch (error) {
68 | // Exit with an error code
69 | console.error("Webpack or electron fatal error" + error);
70 | clearInterval(intervalId);
71 |
72 | return process.exit(1);
73 | }
74 | }, interval);
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const {
2 | CleanWebpackPlugin
3 | } = require("clean-webpack-plugin");
4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin");
5 | const webpack = require("webpack");
6 | const path = require("path");
7 |
8 | module.exports = {
9 | target: "web", // Our app can run without electron
10 | entry: ["./app/src/index.tsx"], // The entry point of our app; these entry points can be named and we can also have multiple if we'd like to split the webpack bundle into smaller files to improve script loading speed between multiple pages of our app
11 | output: {
12 | path: path.resolve(__dirname, "app/dist"), // Where all the output files get dropped after webpack is done with them
13 | filename: "bundle.js" // The name of the webpack bundle that's generated
14 | },
15 | resolve: {
16 | fallback: {
17 | "crypto": require.resolve("crypto-browserify"),
18 | "buffer": require.resolve("buffer/"),
19 | "path": require.resolve("path-browserify"),
20 | "stream": require.resolve("stream-browserify")
21 | }
22 | },
23 | module: {
24 | rules: [{
25 | // loads .html files
26 | test: /\.(html)$/,
27 | include: [path.resolve(__dirname, "app/src")],
28 | use: {
29 | loader: "html-loader",
30 | options: {
31 | sources: {
32 | "list": [{
33 | "tag": "img",
34 | "attribute": "data-src",
35 | "type": "src"
36 | }]
37 | }
38 | }
39 | }
40 | },
41 | // loads .js/jsx/tsx files
42 | {
43 | test: /\.[jt]sx?$/,
44 | include: [path.resolve(__dirname, "app/src")],
45 | loader: "babel-loader",
46 | resolve: {
47 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json"]
48 | }
49 | },
50 | // loads .css files
51 | {
52 | test: /\.css$/,
53 | include: [
54 | path.resolve(__dirname, "app/src"),
55 | path.resolve(__dirname, "node_modules/"),
56 | ],
57 | use: [
58 | MiniCssExtractPlugin.loader,
59 | "css-loader"
60 | ],
61 | resolve: {
62 | extensions: [".css"]
63 | }
64 | },
65 | // loads common image formats
66 | {
67 | test: /\.(svg|png|jpg|gif)$/,
68 | include: [
69 | path.resolve(__dirname, "resources/images")
70 | ],
71 | type: "asset/inline"
72 | },
73 | // loads common font formats
74 | {
75 | test: /\.(eot|woff|woff2|ttf)$/,
76 | include: [
77 | path.resolve(__dirname, "resources/fonts")
78 | ],
79 | type: "asset/inline"
80 | }
81 | ]
82 | },
83 | plugins: [
84 | // fix "process is not defined" error;
85 | // https://stackoverflow.com/a/64553486/1837080
86 | new webpack.ProvidePlugin({
87 | process: "process/browser.js",
88 | }),
89 | new CleanWebpackPlugin()
90 | ]
91 | };
--------------------------------------------------------------------------------
/docs/sandbox.md:
--------------------------------------------------------------------------------
1 | # Sandbox
2 |
3 | Whenever you're deploying your application, whether in a packaged form or running it from the command line, it's worth verifying that electron renderer is actually running in sandboxed mode.
4 |
5 | This document currently explains how the procedure to check if the sandbox is enabled for the following operating systems:
6 | - Linux (seccomp-bpf) (todo: namespace sandbox)
7 | - OSX
8 |
9 | Not supported:
10 | - Windows
11 |
12 | If you however do know a way of testing those too, then please update this document with the information.
13 |
14 | A good _indication_ that the sanbox _might_ be enabled is that the `--no-sandbox` is nowhere to be found.
15 |
16 | ## linux: verify seccomp-bpf sandbox
17 |
18 | Run the application you want to test.
19 | This can be from the actual source code or even a packaged distributable (.zip, .deb, snap..).
20 |
21 | We need to get a list of the process ids (PIDs for short).
22 | ```
23 | $ ps aux | grep "electron"
24 | ```
25 |
26 | Only the renderer processes are supposed to be sandboxed, so grab the PIDs of the processes which have the --renderer flag
27 | ```
28 | user 22350 0.0 0.0 4340 772 pts/0 S+ 21:50 0:00 sh -c electron --enable-sandbox .
29 | user 22351 1.3 0.4 742836 24072 pts/0 Sl+ 21:50 0:00 node /home/user/projects/electron/electron-sandbox/sandbox-preload-simple/node_modules/.bin/electron --enable-sandbox .
30 | user 22357 8.6 1.7 1147784 91584 pts/0 Sl+ 21:50 0:00 /somepath/electron --enable-sandbox .
31 | user 22360 0.0 0.5 323788 29296 pts/0 S+ 21:50 0:00 /somepath/electron --type=zygote
32 | user 22362 0.0 0.1 323788 8400 pts/0 S+ 21:50 0:00 /somepath/electron --type=zygote
33 | user 22394 2.0 1.2 717784 67312 pts/0 Sl+ 21:50 0:00 /somepath/electron --type=renderer --primordial-pipe-token=61D5BD0CAC441B2B2628002A0299952A --lang=en-US --enable-sandbox --app-path=/home/user/projects/electron/electron-sandbox/sandbox-preload-simple --node-integration=false --webview-tag=false --enable-sandbox --preload=/home/user/projects/electron/electron-sandbox/sandbox-preload-simple/preload-simple.js --context-isolation --enable-pinch --num-raster-threads=4 --enable-main-frame-before-activation --content-image-texture-target=... --renderer-client-id=4 --shared-files=v8_natives_data:100,v8_snapshot_data:101
34 | user 22407 0.0 0.0 12728 2096 pts/1 S+ 21:50 0:00 grep electron
35 | ```
36 | We make sure that the `--no-sandbox` flag is NOWHERE to be found. If you see a --no-sandbox in a renderer, then it will not be sandboxed.
37 |
38 | We grab the PID of the renderer process, which is `22394` in this particular instance.
39 |
40 | We check if the Seccomp BPF sandbox is running with the following command
41 |
42 | ```
43 | $ cat /proc/22394/status | grep "Seccomp"
44 | Seccomp: 2
45 | ```
46 |
47 | If it returns 2 the it means the sandbox is enabled!
48 | ```
49 | 0 // SECCOMP_MODE_DISABLED
50 | 1 // SECCOMP_MODE_STRICT
51 | 2 // SECCOMP_MODE_FILTER
52 | ```
53 |
54 | ## MacOS: verify sandbox
55 | * Launch Activity Monitor (available in `/Applications/Utilities`).
56 | * In Activity Monitor, choose View > Columns.
57 | * Ensure that the Sandbox menu item is checked.
58 | * In the Sandbox column, confirm that the value for the Quick Start app is Yes.
59 | * To make it easier to locate the app in Activity monitor, enter the name of the Quick Start app in the Filter field.
60 |
61 | src: https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxQuickStart/AppSandboxQuickStart.html#//apple_ref/doc/uid/TP40011183-CH2-SW3
62 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "secure-electron-template",
3 | "version": "22.0.1",
4 | "description": "The best way to build Electron apps with security in mind.",
5 | "private": true,
6 | "main": "app/electron/main.js",
7 | "scripts": {
8 | "postinstall": "electron-builder install-app-deps",
9 | "audit-app": "npx electronegativity -i ./ -x LimitNavigationGlobalCheck,PermissionRequestHandlerGlobalCheck",
10 | "translate": "node ./app/localization/translateMissing.js",
11 | "dev-server": "cross-env NODE_ENV=development webpack serve --config ./webpack.development.js > dev-scripts/webpack-dev-server.log 2> dev-scripts/webpack-dev-server-error.log",
12 | "dev": "concurrently --success first \"node dev-scripts/prepareDevServer.js\" \"node dev-scripts/launchDevServer.js\" -k",
13 | "prod-build": "cross-env NODE_ENV=production npx webpack --mode=production --config ./webpack.production.js",
14 | "prod": "npm run prod-build && electron .",
15 | "pack": "electron-builder --dir",
16 | "dist": "npm run test && npm run prod-build && electron-builder",
17 | "dist-mac": "npm run test && npm run prod-build && electron-builder --mac",
18 | "dist-linux": "npm run test && npm run prod-build && electron-builder --linux",
19 | "dist-windows": "npm run prod-build && electron-builder --windows",
20 | "dist-all": "npm run test && npm run prod-build && electron-builder --mac --linux --windows",
21 | "test": "mocha"
22 | },
23 | "build": {
24 | "productName": "YourProductName",
25 | "appId": "com.yourcompany|electron.yourproductname",
26 | "directories": {
27 | "buildResources": "resources"
28 | },
29 | "files": [
30 | "app/dist/**/*",
31 | "app/electron/**/*",
32 | "app/localization/!(locales)",
33 | "LICENSE"
34 | ],
35 | "extraFiles": [
36 | "app/localization/locales/**/*",
37 | "license.data",
38 | "public.key"
39 | ],
40 | "win": {
41 | "target": [
42 | "nsis",
43 | "msi"
44 | ]
45 | },
46 | "linux": {
47 | "target": [
48 | "deb",
49 | "rpm",
50 | "snap",
51 | "AppImage"
52 | ]
53 | }
54 | },
55 | "repository": {
56 | "type": "git",
57 | "url": "git+https://github.com/reZach/secure-electron-template.git"
58 | },
59 | "keywords": [
60 | "electron",
61 | "security",
62 | "secure",
63 | "template",
64 | "javascript",
65 | "react",
66 | "redux",
67 | "webpack",
68 | "i18n",
69 | "boilerplate"
70 | ],
71 | "author": "reZach",
72 | "license": "MIT",
73 | "bugs": {
74 | "url": "https://github.com/reZach/secure-electron-template/issues"
75 | },
76 | "homepage": "https://github.com/reZach/secure-electron-template#readme",
77 | "browserslist": [
78 | "last 2 Chrome versions"
79 | ],
80 | "devDependencies": {
81 | "@babel/core": "^7.18.9",
82 | "@babel/plugin-syntax-dynamic-import": "^7.8.3",
83 | "@babel/plugin-transform-react-jsx": "^7.18.6",
84 | "@babel/preset-env": "^7.18.9",
85 | "@babel/preset-react": "^7.18.6",
86 | "@babel/preset-typescript": "^7.18.6",
87 | "@doyensec/electronegativity": "^1.9.1",
88 | "@google-cloud/translate": "^7.0.0",
89 | "@types/react": "^18.0.15",
90 | "@types/react-dom": "^18.0.6",
91 | "babel-loader": "^8.2.5",
92 | "babel-plugin-module-resolver": "^4.1.0",
93 | "buffer": "^6.0.3",
94 | "clean-webpack-plugin": "^4.0.0",
95 | "concurrently": "^7.3.0",
96 | "cross-env": "^7.0.3",
97 | "crypto-browserify": "^3.12.0",
98 | "csp-html-webpack-plugin": "^5.1.0",
99 | "css-loader": "^6.7.1",
100 | "css-minimizer-webpack-plugin": "^4.0.0",
101 | "electron": "^19.0.10",
102 | "electron-builder": "^23.1.0",
103 | "electron-debug": "^3.2.0",
104 | "html-loader": "^4.1.0",
105 | "html-webpack-plugin": "^5.5.0",
106 | "mini-css-extract-plugin": "^2.6.1",
107 | "mocha": "^10.0.0",
108 | "path-browserify": "^1.0.1",
109 | "spectron": "^19.0.0",
110 | "stream-browserify": "^3.0.0",
111 | "typescript": "4.7.4",
112 | "webpack": "^5.74.0",
113 | "webpack-cli": "^4.10.0",
114 | "webpack-dev-server": "^4.9.3",
115 | "webpack-merge": "^5.8.0"
116 | },
117 | "dependencies": {
118 | "@loadable/component": "^5.15.2",
119 | "@reduxjs/toolkit": "^1.8.3",
120 | "bulma": "^0.9.4",
121 | "easy-redux-undo": "^1.0.5",
122 | "electron-devtools-installer": "^3.2.0",
123 | "i18next": "^21.8.14",
124 | "i18next-electron-fs-backend": "^3.0.0",
125 | "i18next-fs-backend": "^1.1.4",
126 | "lodash": "4.17.21",
127 | "lodash.merge": "^4.6.2",
128 | "process": "^0.11.10",
129 | "react": "^18.2.0",
130 | "react-dom": "^18.2.0",
131 | "react-i18next": "^11.18.3",
132 | "react-redux": "^8.0.2",
133 | "react-router": "^6.3.0",
134 | "react-router-dom": "^6.3.0",
135 | "redux": "^4.2.0",
136 | "redux-first-history": "^5.0.12",
137 | "secure-electron-context-menu": "^1.3.3",
138 | "secure-electron-license-keys": "^1.1.3",
139 | "secure-electron-store": "^4.0.2"
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/app/localization/translateMissing.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const {
3 | readdirSync,
4 | statSync
5 | } = require("fs");
6 | const {
7 | join
8 | } = require("path")
9 | const {
10 | Translate
11 | } = require("@google-cloud/translate").v2;
12 |
13 | // READ THIS NOTICE
14 | /*
15 | In order to run this file, you must do the following steps:
16 |
17 | 1. Select or create a Cloud Platform project
18 | 2. Enable billing for your project
19 | 3. Enable the Cloud Translation API
20 | 4. Set up authentication with a service account [so you can access the API from your local workstation]
21 | (These steps are found with more details here: https://www.npmjs.com/package/@google-cloud/translate)
22 |
23 | Once this is done, update 'projectId' below with your GCP project id, and remove the return statement below this comment.
24 |
25 | ----
26 |
27 | BASIC WORKFLOW
28 |
29 | In order to use this file effectively, which is run with the command 'npm run translate', you would
30 | create translated strings like in menu.js or localization.jsx. You would then run the app and change
31 | languages in order that the keys for these translated strings are populated in the various other
32 | languages' missing.json files. Once this is done for all languages you'd like to create translations for, you may run `npm run translate` in order that the missing translation files be translated with
33 | the Google Translate API.
34 |
35 | Note - it is important that 'fromLanguage' be updated to the language that the keys are in the various
36 | translation[.missing].json files. It is this variable that's used by Google to determine the source
37 | language from which to translate.
38 | */
39 | console.log("The translateMissing.js file must be updated before it can be ran.");
40 | return;
41 |
42 | const projectId = "YOUR_PROJECT_ID";
43 |
44 | // Instantiates a client
45 | const translate = new Translate({
46 | projectId
47 | });
48 |
49 | async function updateTranslations() {
50 |
51 | try {
52 | const root = "./app/localization/locales";
53 | const fromLanguage = "en";
54 |
55 | // Get valid languages from Google Translate API
56 | let [googleLanguages] = await translate.getLanguages(); // ie. { code: "en", name: "English" }
57 | googleLanguages = googleLanguages.map(gl => gl.code.replace("-", "_"))
58 |
59 | // Uncomment me to view the various languages Google can translate to/from
60 | //console.log(googleLanguages);
61 |
62 | // Get all language directories;
63 | // https://stackoverflow.com/a/35759360/1837080
64 | const getDirectories = p => readdirSync(p).filter(f => statSync(join(p, f)).isDirectory());
65 | const languageDirectories = getDirectories(root).filter(d => googleLanguages.includes(d));
66 |
67 | // For each language, read in any missing translations
68 | // and translate
69 | for (const languageDirectory of languageDirectories) {
70 |
71 | // Check to make sure each language has the proper files
72 | try {
73 | const languageRoot = `${root}/${languageDirectory}`;
74 | const translationFile = `${languageRoot}/translation.json`;
75 | const missingTranslationFile = `${languageRoot}/translation.missing.json`;
76 |
77 | const translationExists = fs.existsSync(translationFile);
78 | const translationMissingExists = fs.existsSync(missingTranslationFile);
79 |
80 | if (translationExists && translationMissingExists) {
81 |
82 | // Read in contents of files
83 | const translations = JSON.parse(fs.readFileSync(translationFile, {
84 | encoding: "utf8"
85 | }));
86 | const missing = JSON.parse(fs.readFileSync(missingTranslationFile, {
87 | encoding: "utf8"
88 | }));
89 |
90 | // Only translate files with actual values
91 | const missingKeys = Object.keys(missing);
92 | if (missingKeys.length > 0){
93 |
94 | // Translate each of the missing keys to the target language
95 | for (const missingKey of missingKeys){
96 | const googleTranslation = await translate.translate(missingKey, {
97 | from: fromLanguage,
98 | to: languageDirectory
99 | });
100 |
101 | // Only set if a value is returned
102 | if (googleTranslation.length > 0){
103 | translations[missingKey] = googleTranslation[0];
104 | }
105 | }
106 |
107 | // Write output back to file
108 | fs.writeFileSync(translationFile, JSON.stringify(translations, null, 2));
109 | fs.writeFileSync(missingTranslationFile, JSON.stringify({}, null, 2));
110 |
111 | console.log(`Successfully updated translations for ${languageDirectory}`);
112 | } else {
113 | console.log(`Skipped creating translations for ${languageDirectory}; none found!`);
114 | }
115 | } else {
116 |
117 | // Log if we failed
118 | if (!translationExists) {
119 | console.error(`Could not generate translations for language '${languageDirectory}' because ${translationFile} does not exist, skipping!`);
120 | } else if (!translationMissingExists) {
121 | console.error(`Could not generate translations for language '${languageDirectory}' because ${missingTranslationFile} does not exist, skipping!`);
122 | }
123 | }
124 | } catch (error) {
125 | console.error("Failed due to fatal error");
126 | console.error(error);
127 | }
128 | }
129 | } catch (e) {
130 | console.error("Failed due to fatal error");
131 | console.error(e);
132 | }
133 | }
134 |
135 | updateTranslations();
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # secure-electron-template
2 | A current electron app template with the most popular frameworks, designed and built with security in mind. (If you are curious about what makes an electron app secure, please check out [this page](https://github.com/reZach/secure-electron-template/blob/master/docs/secureapps.md)).
3 |
4 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-template)
5 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-template)
6 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-template)
7 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-template)
8 | [](https://sonarcloud.io/dashboard?id=reZach_secure-electron-template)
9 |
10 | ## How to get started
11 | To get started, clone the repository by clicking the [](https://github.com/reZach/secure-electron-template/generate) button, or through the command line (`git clone https://github.com/reZach/secure-electron-template.git`).
12 |
13 | Once cloned, install the dependencies for the repo by running the following commands (you do _not_ have to run the first command if your command line is already inside the newly cloned repository):
14 |
15 | ```
16 | cd secure-electron-template
17 | npm i
18 | npm run dev
19 | ```
20 |
21 | > Are you using `yarn`? You'll want to [read this issue](https://github.com/reZach/secure-electron-template/issues/62).
22 |
23 | When you'd like to test your app in production, or package it for distribution, please navigate to [this page](https://github.com/reZach/secure-electron-template/blob/master/docs/scripts.md) for more details on how to do this.
24 |
25 | ## Demo
26 | 
27 |
28 | ## Features
29 | Taken from the [best-practices](https://electronjs.org/docs/tutorial/security) official page, here is what this repository offers!
30 |
31 | 1. [Only load secure content](https://electronjs.org/docs/tutorial/security#1-only-load-secure-content) - ✅ (But the developer is responsible for loading secure assets only 🙂)
32 | 2. [Do not enable node.js integration for remote content](https://electronjs.org/docs/tutorial/security#2-do-not-enable-nodejs-integration-for-remote-content) - ✅
33 | 3. [Enable context isolation for remote content](https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content) - ✅
34 | 4. [Handle session permission requests from remote content](https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content) - ✅
35 | 5. [Do not disable websecurity](https://electronjs.org/docs/tutorial/security#5-do-not-disable-websecurity) - ✅
36 | 6. [Define a content security policy](https://electronjs.org/docs/tutorial/security#6-define-a-content-security-policy) - ✅
37 | 7. [Do not set allowRunningInsecureContent to true](https://electronjs.org/docs/tutorial/security#7-do-not-set-allowrunninginsecurecontent-to-true) - ✅
38 | 8. [Do not enable experimental features](https://electronjs.org/docs/tutorial/security#8-do-not-enable-experimental-features) - ✅
39 | 9. [Do not use enableBlinkFeatures](https://electronjs.org/docs/tutorial/security#9-do-not-use-enableblinkfeatures) - ✅
40 | 10. [Do not use allowpopups](https://electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups) - ✅
41 | 11. [<webview> verify options and params](https://electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation) - ✅
42 | 12. [Disable or limit navigation](https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation) - ✅
43 | 13. [Disable or limit creation of new windows](https://electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows) - ✅
44 | 14. [Do not use openExternal with untrusted content](https://electronjs.org/docs/tutorial/security#14-do-not-use-openexternal-with-untrusted-content) - ✅
45 | 15. [Disable remote module](https://electronjs.org/docs/tutorial/security#15-disable-the-remote-module) - ✅
46 | 16. [Filter the remote module](https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module) - ✅
47 | 17. [Use a current version of electron](https://electronjs.org/docs/tutorial/security#17-use-a-current-version-of-electron) - ✅
48 |
49 | ## Included frameworks
50 | Built-in to this template are a number of popular frameworks already wired up to get you on the road running.
51 |
52 | - [Electron](https://electronjs.org/)
53 | - [React](https://reactjs.org/)
54 | - [Typescript](https://www.typescriptlang.org)
55 | - [Redux](https://redux.js.org/) (with [Redux toolkit](https://redux-toolkit.js.org/))
56 | - [Babel](https://babeljs.io/)
57 | - [Webpack](https://webpack.js.org/) (with [webpack-dev-server](https://github.com/webpack/webpack-dev-server))
58 | - [Electron builder](https://www.electron.build/) (for packaging up your app)
59 | - [Mocha](https://mochajs.org/)
60 |
61 | ## Bonus modules
62 | What would a template be without some helpful additions?
63 |
64 | - [i18next](https://www.i18next.com/) (with [this plugin](https://github.com/reZach/i18next-electron-fs-backend) for localization).
65 | - [Store](https://github.com/reZach/secure-electron-store) (for saving config/data)
66 | - [Context menu](https://github.com/reZach/secure-electron-context-menu) (supports custom context menus)
67 | - [Easy redux undo](https://github.com/reZach/easy-redux-undo) (for undo/redoing your redux actions)
68 | - [License key validation](https://github.com/reZach/secure-electron-license-keys) (for validating a user has the proper license to use your app) **new!**
69 |
70 | ## Architecture
71 | For a more detailed view of the architecture of the template, please check out [here](https://github.com/reZach/secure-electron-template/blob/master/docs/architecture.md). I would _highly_ recommend reading this document to get yourself familiarized with this template.
72 |
73 | ## FAQ
74 | Please see [our faq](https://github.com/reZach/secure-electron-template/blob/master/docs/faq.md) for any common questions you might have.
75 | **NEW TO ELECTRON?** Please visit [this page](https://github.com/reZach/secure-electron-template/blob/master/docs/newtoelectron.md).
76 |
77 | ## Show us your apps!
78 | If you've built any applications with our template, we'd [love to see them!](https://github.com/reZach/secure-electron-template/blob/master/docs/yourapps.md).
79 |
--------------------------------------------------------------------------------
/app/src/pages/undoredo/undoredo.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import { UNDO, REDO, CLEAR, GROUPBEGIN, GROUPEND } from "easy-redux-undo";
4 | import { increment, decrement } from "Redux/components/counter/counterSlice";
5 | import { add, remove } from "Redux/components/complex/complexSlice";
6 | import "./undoredo.css";
7 |
8 | class UndoRedo extends React.Component {
9 | constructor() {
10 | super();
11 |
12 | // Counter-specific
13 | this.inc = this.inc.bind(this);
14 | this.dec = this.dec.bind(this);
15 |
16 | // Complex-specific
17 | this.add = this.add.bind(this);
18 | this.remove = this.remove.bind(this);
19 |
20 | // Undo-specific
21 | this.undo = this.undo.bind(this);
22 | this.redo = this.redo.bind(this);
23 | this.undo2 = this.undo2.bind(this);
24 | this.redo2 = this.redo2.bind(this);
25 | this.clear = this.clear.bind(this);
26 | this.groupbegin = this.groupbegin.bind(this);
27 | this.groupend = this.groupend.bind(this);
28 | }
29 |
30 | inc(_event) {
31 | this.props.increment();
32 | }
33 |
34 | dec(_event) {
35 | this.props.decrement();
36 | }
37 |
38 | add(_event) {
39 | this.props.add();
40 | }
41 |
42 | remove(_event) {
43 | this.props.remove();
44 | }
45 |
46 | undo(_event) {
47 | this.props.UNDO();
48 | }
49 |
50 | redo(_event) {
51 | this.props.REDO();
52 | }
53 |
54 | undo2(_event) {
55 | this.props.UNDO(2);
56 | }
57 |
58 | redo2(_event) {
59 | this.props.REDO(2);
60 | }
61 |
62 | clear(_event) {
63 | this.props.CLEAR();
64 | }
65 |
66 | groupbegin(_event) {
67 | this.props.GROUPBEGIN();
68 | }
69 |
70 | groupend(_event) {
71 | this.props.GROUPEND();
72 | }
73 |
74 | render() {
75 | return (
76 |
77 |
78 |
79 |
Undo/Redo
80 |
81 | Try out modifying, and then undo/redoing the redux history below!
82 |
165 | );
166 | }
167 |
168 | render() {
169 | return (
170 |
262 | );
263 | }
264 | }
265 |
266 | function WithNavigate(props){
267 | const navigate = useNavigate();
268 | return
269 | }
270 |
271 | export default WithNavigate;
272 |
--------------------------------------------------------------------------------
/app/electron/main.js:
--------------------------------------------------------------------------------
1 | const {
2 | app,
3 | protocol,
4 | BrowserWindow,
5 | session,
6 | ipcMain,
7 | Menu
8 | } = require("electron");
9 | const {
10 | default: installExtension,
11 | REDUX_DEVTOOLS,
12 | REACT_DEVELOPER_TOOLS
13 | } = require("electron-devtools-installer");
14 | const SecureElectronLicenseKeys = require("secure-electron-license-keys");
15 | const Protocol = require("./protocol");
16 | const MenuBuilder = require("./menu");
17 | const i18nextBackend = require("i18next-electron-fs-backend");
18 | const i18nextMainBackend = require("../localization/i18n.mainconfig");
19 | const Store = require("secure-electron-store").default;
20 | const ContextMenu = require("secure-electron-context-menu").default;
21 | const path = require("path");
22 | const fs = require("fs");
23 | const crypto = require("crypto");
24 | const isDev = process.env.NODE_ENV === "development";
25 | const port = 40992; // Hardcoded; needs to match webpack.development.js and package.json
26 | const selfHost = `http://localhost:${port}`;
27 |
28 | // Keep a global reference of the window object, if you don't, the window will
29 | // be closed automatically when the JavaScript object is garbage collected.
30 | let win;
31 | let menuBuilder;
32 |
33 | async function createWindow() {
34 |
35 | // If you'd like to set up auto-updating for your app,
36 | // I'd recommend looking at https://github.com/iffy/electron-updater-example
37 | // to use the method most suitable for you.
38 | // eg. autoUpdater.checkForUpdatesAndNotify();
39 |
40 | if (!isDev) {
41 | // Needs to happen before creating/loading the browser window;
42 | // protocol is only used in prod
43 | protocol.registerBufferProtocol(Protocol.scheme, Protocol.requestHandler); /* eng-disable PROTOCOL_HANDLER_JS_CHECK */
44 | }
45 |
46 | const store = new Store({
47 | path: app.getPath("userData")
48 | });
49 |
50 | // Use saved config values for configuring your
51 | // BrowserWindow, for instance.
52 | // NOTE - this config is not passcode protected
53 | // and stores plaintext values
54 | //let savedConfig = store.mainInitialStore(fs);
55 |
56 | // Create the browser window.
57 | win = new BrowserWindow({
58 | width: 800,
59 | height: 600,
60 | title: "Application is currently initializing...",
61 | webPreferences: {
62 | devTools: isDev,
63 | nodeIntegration: false,
64 | nodeIntegrationInWorker: false,
65 | nodeIntegrationInSubFrames: false,
66 | contextIsolation: true,
67 | enableRemoteModule: false,
68 | additionalArguments: [`--storePath=${store.sanitizePath(app.getPath("userData"))}`],
69 | preload: path.join(__dirname, "preload.js"),
70 | /* eng-disable PRELOAD_JS_CHECK */
71 | disableBlinkFeatures: "Auxclick"
72 | }
73 | });
74 |
75 | // Sets up main.js bindings for our i18next backend
76 | i18nextBackend.mainBindings(ipcMain, win, fs);
77 |
78 | // Sets up main.js bindings for our electron store;
79 | // callback is optional and allows you to use store in main process
80 | const callback = function (success, initialStore) {
81 | console.log(`${!success ? "Un-s" : "S"}uccessfully retrieved store in main process.`);
82 | console.log(initialStore); // {"key1": "value1", ... }
83 | };
84 |
85 | store.mainBindings(ipcMain, win, fs, callback);
86 |
87 | // Sets up bindings for our custom context menu
88 | ContextMenu.mainBindings(ipcMain, win, Menu, isDev, {
89 | "loudAlertTemplate": [{
90 | id: "loudAlert",
91 | label: "AN ALERT!"
92 | }],
93 | "softAlertTemplate": [{
94 | id: "softAlert",
95 | label: "Soft alert"
96 | }]
97 | });
98 |
99 | // Setup bindings for offline license verification
100 | SecureElectronLicenseKeys.mainBindings(ipcMain, win, fs, crypto, {
101 | root: process.cwd(),
102 | version: app.getVersion()
103 | });
104 |
105 | // Load app
106 | if (isDev) {
107 | win.loadURL(selfHost);
108 | } else {
109 | win.loadURL(`${Protocol.scheme}://rse/index.html`);
110 | }
111 |
112 | win.webContents.on("did-finish-load", () => {
113 | win.setTitle(`Getting started with secure-electron-template (v${app.getVersion()})`);
114 | });
115 |
116 | // Only do these things when in development
117 | if (isDev) {
118 |
119 | // Errors are thrown if the dev tools are opened
120 | // before the DOM is ready
121 | win.webContents.once("dom-ready", async () => {
122 | await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
123 | .then((name) => console.log(`Added Extension: ${name}`))
124 | .catch((err) => console.log("An error occurred: ", err))
125 | .finally(() => {
126 | require("electron-debug")(); // https://github.com/sindresorhus/electron-debug
127 | win.webContents.openDevTools();
128 | });
129 | });
130 | }
131 |
132 | // Emitted when the window is closed.
133 | win.on("closed", () => {
134 | // Dereference the window object, usually you would store windows
135 | // in an array if your app supports multi windows, this is the time
136 | // when you should delete the corresponding element.
137 | win = null;
138 | });
139 |
140 | // https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content
141 | const ses = session;
142 | const partition = "default";
143 | ses.fromPartition(partition) /* eng-disable PERMISSION_REQUEST_HANDLER_JS_CHECK */
144 | .setPermissionRequestHandler((webContents, permission, permCallback) => {
145 | const allowedPermissions = []; // Full list here: https://developer.chrome.com/extensions/declare_permissions#manifest
146 |
147 | if (allowedPermissions.includes(permission)) {
148 | permCallback(true); // Approve permission request
149 | } else {
150 | console.error(
151 | `The application tried to request permission for '${permission}'. This permission was not whitelisted and has been blocked.`
152 | );
153 |
154 | permCallback(false); // Deny
155 | }
156 | });
157 |
158 | // https://electronjs.org/docs/tutorial/security#1-only-load-secure-content;
159 | // The below code can only run when a scheme and host are defined, I thought
160 | // we could use this over _all_ urls
161 | // ses.fromPartition(partition).webRequest.onBeforeRequest({urls:["http://localhost./*"]}, (listener) => {
162 | // if (listener.url.indexOf("http://") >= 0) {
163 | // listener.callback({
164 | // cancel: true
165 | // });
166 | // }
167 | // });
168 |
169 | menuBuilder = MenuBuilder(win, app.name);
170 |
171 | // Set up necessary bindings to update the menu items
172 | // based on the current language selected
173 | i18nextMainBackend.on("initialized", (loaded) => {
174 | i18nextMainBackend.changeLanguage("en");
175 | i18nextMainBackend.off("initialized"); // Remove listener to this event as it's not needed anymore
176 | });
177 |
178 | // When the i18n framework starts up, this event is called
179 | // (presumably when the default language is initialized)
180 | // BEFORE the "initialized" event is fired - this causes an
181 | // error in the logs. To prevent said error, we only call the
182 | // below code until AFTER the i18n framework has finished its
183 | // "initialized" event.
184 | i18nextMainBackend.on("languageChanged", (lng) => {
185 | if (i18nextMainBackend.isInitialized){
186 | menuBuilder.buildMenu(i18nextMainBackend);
187 | }
188 | });
189 | }
190 |
191 | // Needs to be called before app is ready;
192 | // gives our scheme access to load relative files,
193 | // as well as local storage, cookies, etc.
194 | // https://electronjs.org/docs/api/protocol#protocolregisterschemesasprivilegedcustomschemes
195 | protocol.registerSchemesAsPrivileged([{
196 | scheme: Protocol.scheme,
197 | privileges: {
198 | standard: true,
199 | secure: true
200 | }
201 | }]);
202 |
203 | // This method will be called when Electron has finished
204 | // initialization and is ready to create browser windows.
205 | // Some APIs can only be used after this event occurs.
206 | app.on("ready", createWindow);
207 |
208 | // Quit when all windows are closed.
209 | app.on("window-all-closed", () => {
210 | // On macOS it is common for applications and their menu bar
211 | // to stay active until the user quits explicitly with Cmd + Q
212 | if (process.platform !== "darwin") {
213 | app.quit();
214 | } else {
215 | i18nextBackend.clearMainBindings(ipcMain);
216 | ContextMenu.clearMainBindings(ipcMain);
217 | SecureElectronLicenseKeys.clearMainBindings(ipcMain);
218 | }
219 | });
220 |
221 | app.on("activate", () => {
222 | // On macOS it's common to re-create a window in the app when the
223 | // dock icon is clicked and there are no other windows open.
224 | if (win === null) {
225 | createWindow();
226 | }
227 | });
228 |
229 | // https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation
230 | app.on("web-contents-created", (event, contents) => {
231 | contents.on("will-navigate", (contentsEvent, navigationUrl) => {
232 | /* eng-disable LIMIT_NAVIGATION_JS_CHECK */
233 | const parsedUrl = new URL(navigationUrl);
234 | const validOrigins = [selfHost];
235 |
236 | // Log and prevent the app from navigating to a new page if that page's origin is not whitelisted
237 | if (!validOrigins.includes(parsedUrl.origin)) {
238 | console.error(
239 | `The application tried to navigate to the following address: '${parsedUrl}'. This origin is not whitelisted and the attempt to navigate was blocked.`
240 | );
241 |
242 | contentsEvent.preventDefault();
243 | }
244 | });
245 |
246 | contents.on("will-redirect", (contentsEvent, navigationUrl) => {
247 | const parsedUrl = new URL(navigationUrl);
248 | const validOrigins = [];
249 |
250 | // Log and prevent the app from redirecting to a new page
251 | if (!validOrigins.includes(parsedUrl.origin)) {
252 | console.error(
253 | `The application tried to redirect to the following address: '${navigationUrl}'. This attempt was blocked.`
254 | );
255 |
256 | contentsEvent.preventDefault();
257 | }
258 | });
259 |
260 | // https://electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation
261 | contents.on("will-attach-webview", (contentsEvent, webPreferences, params) => {
262 | // Strip away preload scripts if unused or verify their location is legitimate
263 | delete webPreferences.preload;
264 | delete webPreferences.preloadURL;
265 |
266 | // Disable Node.js integration
267 | webPreferences.nodeIntegration = false;
268 | });
269 | // enable i18next translations in popup window
270 | contents.on("did-create-window", (window) => {
271 | i18nextBackend.mainBindings(ipcMain, window, fs);
272 | });
273 | // destroy bindings on popup window closed
274 | contents.on("destroyed", () => {
275 | i18nextBackend.clearMainBindings(ipcMain);
276 | });
277 |
278 | // https://electronjs.org/docs/tutorial/security#13-disable-or-limit-creation-of-new-windows
279 | // This code replaces the old "new-window" event handling;
280 | // https://github.com/electron/electron/pull/24517#issue-447670981
281 | contents.setWindowOpenHandler(({
282 | url
283 | }) => {
284 | const parsedUrl = new URL(url);
285 | const validOrigins = [];
286 |
287 | // Log and prevent opening up a new window
288 | if (!validOrigins.includes(parsedUrl.origin)) {
289 | console.error(
290 | `The application tried to open a new window at the following address: '${url}'. This attempt was blocked.`
291 | );
292 |
293 | return {
294 | action: "deny"
295 | };
296 | }
297 |
298 | return {
299 | action: "allow"
300 | };
301 | });
302 | });
303 |
--------------------------------------------------------------------------------
/docs/newtoelectron.md:
--------------------------------------------------------------------------------
1 | > **Update 2022**: A more comprehensive guide explaining Electron and secure practices can be [found here](https://www.debugandrelease.com/the-ultimate-electron-guide/).
2 |
3 | # Are you new to electron?
4 | Welcome, we are glad you are here and decided to learn more about electron. We'll be giving you a primer so you'll be well on your way to understanding electron and how you can write your apps with this template.
5 |
6 | ## Understanding web languages
7 | It is assumed you have some elementary knowledge about web programming; namely that HTML positions elements, CSS styles elements, and JS adds interactivity into the page. You will know if you've been doing web programming for some time that the capabilities these languages offer you is limited [with comparison to say, desktop applications for example].
8 |
9 | ## What does electron provide?
10 | Electron gives you access to electron / node apis that allow you to have more powerful functionality in your web applications. Think file system access, os access or c++ addons.
11 |
12 | ## How does electron provide these features?
13 | Electron provides these features through Node through [`require`](https://nodejs.org/en/knowledge/getting-started/what-is-require/), or generally module-loading. In web pages, we add more functionality (generally) by including different script tags, in the Node environment, we get more functionality by including more modules. Features such as file system access come from [Node](https://nodejs.org/api/fs.html). Any Node api can be added to an electron app.
14 |
15 | ## How do I import modules into my app?
16 | You _used_ to (and still can, but it's **not** recommended) use Node modules with the electron [`remote`](https://www.electronjs.org/docs/api/remote) api, but [it's not very secure](https://github.com/electron/electron/issues/9920#issuecomment-575839738). Beginning with [electron v5](https://www.electronjs.org/docs/breaking-changes#planned-breaking-api-changes-50) (which was released in April 24, 2019), the team recommended to use a different architecture to make use of these Node modules. This is IPC, inter-process communication.
17 |
18 | 
19 |
20 | ## How does IPC work
21 | IPC in itself doesn't _do_ anything, it simply allows you to send messages between the main and renderer processes. The idea behind IPC is that your **main** process controls and loads Node apis, while your **renderer** process tells the main process whenever it needs to use something that calls a Node api.
22 |
23 | Setting up your app like this ensures that _bad actors_ cannot misuse the Node apis in your app. With the _old_ way (ie. `require`) of importing Node modules in your app, the client-facing part of your app had access to Node modules directly (which could end up with [bad consequences](https://blog.devsecurity.eu/en/blog/joplin-electron-rce)).
24 |
25 | ## Take a step back, what are main and renderer processes?
26 | An electron app is broken up into [*normally; at least] two processes; a renderer and main process. The main process creates the app, using a [`BrowserWindow`](https://www.electronjs.org/docs/api/browser-window), which loads up a "browser", which is how electron loads up your HTML/CSS/JS content. The other process is your HTML/CSS/JS, all contained in a renderer process.
27 |
28 | *Electron apps can load multiple windows, so you may have more than one renderer process in this case.
29 |
30 | ## Show me an example of how to use IPC to use a Node module
31 |
32 | **main.js**
33 | ```javascript
34 | const {
35 | app,
36 | BrowserWindow,
37 | ipcMain
38 | } = require("electron");
39 | const path = require("path");
40 | const fs = require("fs");
41 |
42 | // Keep a global reference of the window object, if you don't, the window will
43 | // be closed automatically when the JavaScript object is garbage collected.
44 | let win;
45 |
46 | async function createWindow() {
47 |
48 | // Create the browser window.
49 | win = new BrowserWindow({
50 | width: 800,
51 | height: 600,
52 | webPreferences: {
53 | nodeIntegration: false, // is default value after Electron v5
54 | contextIsolation: true, // protect against prototype pollution
55 | enableRemoteModule: false, // turn off remote
56 | preload: path.join(__dirname, "preload.js") // use a preload script
57 | }
58 | });
59 |
60 | // Load app
61 | win.loadFile(path.join(__dirname, "dist/index.html"));
62 |
63 | // rest of code..
64 | }
65 |
66 | app.on("ready", createWindow);
67 |
68 | ipcMain.on("toMain", (event, args) => {
69 | fs.readFile("path/to/file", (error, data) => {
70 | // Do something with file contents
71 |
72 | // Send result back to renderer process
73 | win.webContents.send("fromMain", responseObj);
74 | });
75 | });
76 | ```
77 |
78 | **preload.js**
79 | ```javascript
80 | const {
81 | contextBridge,
82 | ipcRenderer
83 | } = require("electron");
84 |
85 | // Expose protected methods that allow the renderer process to use
86 | // the ipcRenderer without exposing the entire object
87 | contextBridge.exposeInMainWorld(
88 | "api", {
89 | send: (channel, data) => {
90 | // whitelist channels
91 | let validChannels = ["toMain"];
92 | if (validChannels.includes(channel)) {
93 | ipcRenderer.send(channel, data);
94 | }
95 | },
96 | receive: (channel, func) => {
97 | let validChannels = ["fromMain"];
98 | if (validChannels.includes(channel)) {
99 | // Deliberately strip event as it includes `sender`
100 | ipcRenderer.on(channel, (event, ...args) => func(...args));
101 | }
102 | }
103 | }
104 | );
105 | ```
106 |
107 | **index.html**
108 | ```html
109 |
110 |
111 |
112 |
113 | Title
114 |
115 |
116 |
125 |
126 |
127 | ```
128 |
129 | ## You didn't explain what preload.js does!
130 | If you noticed, in **main.js** we have this line:
131 |
132 | ```javascript
133 | preload: path.join(__dirname, "preload.js") // use a preload script
134 | ```
135 |
136 | This is the line that loads our preload script. A preload script is an electron concept. The preload script can define new variables for our renderer script, and (importantly) has access to `require`. Do you notice how we were able to use `window.api`?
137 |
138 | Let's now look again at preload.js.
139 |
140 | **preload.js**
141 | ```javascript
142 | const {
143 | contextBridge,
144 | ipcRenderer
145 | } = require("electron");
146 |
147 | // Expose protected methods that allow the renderer process to use
148 | // the ipcRenderer without exposing the entire object
149 | contextBridge.exposeInMainWorld(
150 | "api", {
151 | send: (channel, data) => {
152 | // whitelist channels
153 | let validChannels = ["toMain"];
154 | if (validChannels.includes(channel)) {
155 | ipcRenderer.send(channel, data);
156 | }
157 | },
158 | receive: (channel, func) => {
159 | let validChannels = ["fromMain"];
160 | if (validChannels.includes(channel)) {
161 | // Deliberately strip event as it includes `sender`
162 | ipcRenderer.on(channel, (event, ...args) => func(...args));
163 | }
164 | }
165 | }
166 | );
167 | ```
168 |
169 | We expose an `api` property on the window object with two functions, **send** and **receive**. These functions allow us to talk to (ie. send messages to) the main ipc process and react to it's responses. Now, the renderer process has [indirect] access to Node apis!
170 |
171 | ## Can you explain what these "channels" are for?
172 |
173 | If you noticed in the previous code blocks, we are defining `validChannels`. `validChannels` are not an Electron concept, but a safelist of named identifiers in order that only necessary methods of a `require`'d module are available to be used in code. In other words, instead of allowing _all_ of the methods [`fs`](https://nodejs.dev/learn/the-nodejs-fs-module) has, we only allow features/methods we need. This follows the principle of [least privilege](https://www.cyberark.com/what-is/least-privilege/) - and is more secure.
174 |
175 | Notice in this example how we make use of `fs` in our **main.js** file.
176 |
177 | ```javascript
178 | const {
179 | app,
180 | BrowserWindow,
181 | ipcMain
182 | } = require("electron");
183 | const path = require("path");
184 | const fs = require("fs");
185 |
186 | // Keep a global reference of the window object, if you don't, the window will
187 | // be closed automatically when the JavaScript object is garbage collected.
188 | let win;
189 |
190 | async function createWindow() {
191 |
192 | // Create the browser window.
193 | win = new BrowserWindow({
194 | width: 800,
195 | height: 600,
196 | webPreferences: {
197 | nodeIntegration: false, // is default value after Electron v5
198 | contextIsolation: true, // protect against prototype pollution
199 | enableRemoteModule: false, // turn off remote
200 | preload: path.join(__dirname, "preload.js") // use a preload script
201 | }
202 | });
203 |
204 | // Load app
205 | win.loadFile(path.join(__dirname, "dist/index.html"));
206 |
207 | // rest of code..
208 | }
209 |
210 | app.on("ready", createWindow);
211 |
212 | ipcMain.on("toMain", (event, args) => {
213 | fs.readFile("path/to/file", (error, data) => {
214 | // Do something with file contents
215 |
216 | // Send result back to renderer process
217 | win.webContents.send("fromMain", responseObj);
218 | });
219 | });
220 | ```
221 |
222 | We _only_ allow the [`.readFile`](https://nodejs.org/api/fs.html#fsreadfilepath-options-callback) method to be called. However, if our **preload**/**main** file looked something like this...
223 |
224 | **preload.js**
225 | ```javascript
226 | const {
227 | contextBridge,
228 | ipcRenderer
229 | } = require("electron");
230 |
231 | // POOR example of a secure way to send IPC messages
232 | contextBridge.exposeInMainWorld(
233 | "api", {
234 | send: (channel, data) => {
235 | ipcRenderer.send(channel, data);
236 | },
237 | receive: (channel, func) => {
238 | // Deliberately strip event as it includes `sender`
239 | ipcRenderer.on(channel, (event, ...args) => func(...args));
240 | }
241 | }
242 | );
243 | ```
244 |
245 | **main.js**
246 | ```javascript
247 | const {
248 | app,
249 | BrowserWindow,
250 | ipcMain
251 | } = require("electron");
252 | const path = require("path");
253 | const fs = require("fs");
254 |
255 | // Keep a global reference of the window object, if you don't, the window will
256 | // be closed automatically when the JavaScript object is garbage collected.
257 | let win;
258 |
259 | async function createWindow() {
260 |
261 | // Create the browser window.
262 | win = new BrowserWindow({
263 | width: 800,
264 | height: 600,
265 | webPreferences: {
266 | nodeIntegration: false, // is default value after Electron v5
267 | contextIsolation: true, // protect against prototype pollution
268 | enableRemoteModule: false, // turn off remote
269 | preload: path.join(__dirname, "preload.js") // use a preload script
270 | }
271 | });
272 |
273 | // Load app
274 | win.loadFile(path.join(__dirname, "dist/index.html"));
275 |
276 | // rest of code..
277 | }
278 |
279 | app.on("ready", createWindow);
280 |
281 | // Code would only be hit if channel was "fs"
282 | ipcMain.on("fs", (event, args) => {
283 | var result = true;
284 |
285 | // We allow _any_ methods to be called here
286 | // dynamically. This example is what we should
287 | // NOT be doing.
288 | fs[args.method.toString()].apply(null, args.arguments);
289 |
290 | // Send result back to renderer process
291 | win.webContents.send("fromfs", result); // return result to renderer process
292 | });
293 | ```
294 |
295 | Besides not being fully tested, the _idea_ is that the above code would allow _any_ method from `fs` to be called. This is a _bad security practice_ because it allows our code to call [`.copyFile`](https://nodejs.org/api/fs.html#fscopyfilesrc-dest-mode-callback), [`.mkdir`](https://nodejs.org/api/fs.html#fsmkdirpath-options-callback), [`.rmdir`](https://nodejs.org/api/fs.html#fsrmdirpath-options-callback) or potentially any other method that `fs` has access to call! This security breach would happen if the front-end code was compromised in any way (ie. if IPC messages were sent from our renderer process).
296 |
297 | The general idea behind a safelist of channels is that we **define** what methods our code/app should support [through matching channel names in our preload/main files], instead of allowing _any_ methods that may not be used/introduce a possible vulnerability in our app.
298 |
299 | ## What the preload.js _used_ to look like (advanced info)
300 |
301 | It's _important_ to recognize that older solutions before contextBridge were to set properties on the `window`, ie:
302 |
303 | ```javascript
304 | const {
305 | ipcRenderer
306 | } = require("electron");
307 |
308 | window.send = function(channel, data){
309 | // whitelist channels
310 | let validChannels = ["toMain"];
311 | if (validChannels.includes(channel)) {
312 | ipcRenderer.send(channel, data);
313 | }
314 | };
315 |
316 | window.recieve = function(channel, func){
317 | let validChannels = ["fromMain"];
318 | if (validChannels.includes(channel)) {
319 | // Deliberately strip event as it includes `sender`
320 | ipcRenderer.on(channel, (event, ...args) => func(...args));
321 | }
322 | };
323 | ```
324 |
325 | The obvious problem with this is that you can [override functions](https://stackoverflow.com/a/5409468/1837080) in javascript. A determined attacker could modify this function definition and then your backend (ie. main.js code) would not be safe. [With this api](https://www.electronjs.org/docs/api/context-bridge#contextbridge), we can be ensure the functions we expose to our renderer process cannot be tampered with.
326 |
327 | From the electron docs:
328 | > Any data / primitives sent in the API object become immutable and updates on either side of the bridge do not result in an update on the other side.
329 |
330 | In other terms, because we use `contextBridge.exposeInMainWorld`, our renderer process cannot modify the definition of the functions we expose, protecting us from a possible security attack vector.
331 |
332 | ## Wrapping up
333 | With these details, I hope I have explained the basics of electron to you and given you a quick run-down on how to _correctly_ set up a **secure** electron app [when you'd like to use Node apis]. You should be well on your way to developing the next killer app!
334 |
335 | In case you were looking for a good starting point, this template [`secure-electron-template`](https://github.com/reZach/secure-electron-template) has the security features (and more!) we just described built-in. There are plenty of comments in the template describing these, and more, security enhancements. Check us out today!
--------------------------------------------------------------------------------