├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── actions │ ├── prepare-app-repo │ │ └── action.yml │ └── prepare-fortify-setup-repo │ │ └── action.yml └── workflows │ └── pipeline.yml ├── .gitignore ├── .yarnrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── locale ├── cs.json ├── de.json ├── el.json ├── en.json ├── es.json ├── fr.json ├── it.json ├── ja.json ├── nl.json ├── pt.json ├── ru.json ├── tr.json └── zh.json ├── package.json ├── scripts ├── postinstall.ts ├── sign_data │ ├── crypto.ts │ ├── index.ts │ ├── keys.ts │ └── types.d.ts ├── utils.ts ├── webpack.base.config.js ├── webpack.main.config.js └── webpack.renderer.config.js ├── src ├── main │ ├── application.ts │ ├── config.ts │ ├── constants │ │ ├── design.ts │ │ ├── files.ts │ │ ├── index.ts │ │ ├── links.ts │ │ └── third_party.ts │ ├── container.ts │ ├── crypto.ts │ ├── errors.ts │ ├── firefox_providers.ts │ ├── index.ts │ ├── ipc_messages.ts │ ├── jws.ts │ ├── l10n.ts │ ├── logger │ │ ├── analytics.ts │ │ ├── analytics_transport.ts │ │ └── index.ts │ ├── server.ts │ ├── server_storage.ts │ ├── services │ │ ├── index.ts │ │ └── ssl │ │ │ ├── firefox.ts │ │ │ ├── generator.ts │ │ │ ├── index.ts │ │ │ ├── installer.ts │ │ │ ├── nss.ts │ │ │ └── pem_converter.ts │ ├── tray │ │ ├── base_template.ts │ │ ├── development_template.ts │ │ └── index.ts │ ├── types.ts │ ├── updater.ts │ ├── utils │ │ ├── index.ts │ │ ├── printf.ts │ │ └── request.ts │ └── windows │ │ ├── browser_window.ts │ │ ├── dialogs_storage.ts │ │ ├── index.ts │ │ └── windows_controller.ts ├── renderer │ ├── app.tsx │ ├── components │ │ ├── document_title.tsx │ │ ├── intl │ │ │ ├── index.ts │ │ │ ├── intl_context.tsx │ │ │ └── intl_provider.tsx │ │ ├── layouts │ │ │ ├── dialog_layout.tsx │ │ │ ├── index.ts │ │ │ ├── modal_layout.tsx │ │ │ └── styles │ │ │ │ ├── dialog_layout.sass │ │ │ │ └── modal_layout.sass │ │ ├── window_event.tsx │ │ └── window_provider.tsx │ ├── constants │ │ ├── index.ts │ │ └── iso_langs.ts │ ├── containers │ │ ├── index.ts │ │ ├── key_pin │ │ │ ├── container.tsx │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ └── container.sass │ │ ├── message │ │ │ ├── container.tsx │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ └── container.sass │ │ ├── p11_pin │ │ │ ├── container.tsx │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ └── container.sass │ │ └── preferences │ │ │ ├── about.tsx │ │ │ ├── container.tsx │ │ │ ├── index.tsx │ │ │ ├── settings.tsx │ │ │ ├── sites.tsx │ │ │ ├── styles │ │ │ ├── about.sass │ │ │ ├── container.sass │ │ │ ├── settings.sass │ │ │ ├── sites.sass │ │ │ └── updates.sass │ │ │ ├── types.d.ts │ │ │ └── updates.tsx │ └── index.tsx ├── resources │ ├── new_card.tmpl │ └── osx-ssl.sh ├── shared │ ├── constants │ │ ├── index.ts │ │ └── windows.ts │ └── index.ts └── static │ ├── icons │ ├── attention_icon.svg │ ├── chrome.png │ ├── edge.png │ ├── error_icon.svg │ ├── firefox.png │ ├── github_icon.svg │ ├── globe_icon.svg │ ├── ie.png │ ├── logo.svg │ ├── question_icon.svg │ ├── safari.png │ ├── search_icon.svg │ ├── token_icon.svg │ ├── tray │ │ ├── mac │ │ │ └── icon.icns │ │ ├── png │ │ │ ├── icon.png │ │ │ ├── icon@1.5x.png │ │ │ ├── icon@16x.png │ │ │ ├── icon@2x.png │ │ │ ├── icon@32x.png │ │ │ ├── icon@3x.png │ │ │ ├── icon@4x.png │ │ │ ├── icon@64x.png │ │ │ └── icon@8x.png │ │ └── win │ │ │ └── icon.ico │ ├── tray_notification │ │ └── png │ │ │ ├── icon.png │ │ │ ├── icon@1.5x.png │ │ │ ├── icon@16x.png │ │ │ ├── icon@2x.png │ │ │ ├── icon@32x.png │ │ │ ├── icon@3x.png │ │ │ ├── icon@4x.png │ │ │ ├── icon@64x.png │ │ │ └── icon@8x.png │ └── twitter_icon.svg │ └── index.html ├── test ├── nss.ts └── resources │ └── ca.pem ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/out/** 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "airbnb", 4 | "airbnb-typescript" 5 | ], 6 | "parserOptions": { 7 | "project": "./tsconfig.json" 8 | }, 9 | "rules": { 10 | "react/prop-types": 1, 11 | "react/jsx-props-no-spreading": 0, 12 | "react/static-property-placement": 0, 13 | "import/no-extraneous-dependencies": 1, 14 | "newline-before-return": 2, 15 | "import/prefer-default-export": 0, 16 | "react/jsx-one-expression-per-line": 0, 17 | "no-bitwise": 0, 18 | "no-await-in-loop": 0, 19 | "no-use-before-define": 0, 20 | "no-param-reassign": 0, 21 | "react/function-component-definition": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/actions/prepare-app-repo/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare app repository 2 | runs: 3 | using: "composite" 4 | steps: 5 | - run: yarn install --frozen-lockfile 6 | shell: bash 7 | - run: yarn build 8 | shell: bash 9 | -------------------------------------------------------------------------------- /.github/actions/prepare-fortify-setup-repo/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare fortify-setup repository 2 | runs: 3 | using: "composite" 4 | steps: 5 | - run: git clone https://$ACCESS_TOKEN:x-oauth-basic@github.com/PeculiarVentures/fortify-setup.git --depth=1 --branch=master 6 | shell: bash 7 | - run: yarn --cwd ./fortify-setup install --frozen-lockfile 8 | shell: bash 9 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Create installers and draft release 2 | 3 | on: push 4 | 5 | env: 6 | # fortify-setup package dependency 7 | APP_REPO_FOLDER: ../ 8 | # fortify-setup package dependency 9 | RELEASE_FOLDER: ../release 10 | # fortify-setup package dependency 11 | ELECTRON_VERSION: '13.6.9' 12 | # sign-data script dependency 13 | OUTPUT_FOLDER_PATH: ./release 14 | # sign-data script dependency 15 | PRIVATE_KEY_BASE64: ${{ secrets.PRIVATE_KEY_BASE64 }} 16 | # sign-data script dependency 17 | PUBLIC_KEY_BASE64: ${{ secrets.PUBLIC_KEY_BASE64 }} 18 | # fortify-setup clone token 19 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 20 | # actions/create-release dependency 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | jobs: 24 | checkout: 25 | if: >- 26 | contains(github.event.head_commit.message, '[release]') 27 | runs-on: ubuntu-20.04 28 | steps: 29 | - name: Logging 30 | run: | 31 | echo "Let's create the draft release" 32 | 33 | macos: 34 | name: Create macos installer 35 | runs-on: macos-12 36 | needs: [checkout] 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-node@v4 40 | with: 41 | node-version: 16 42 | cache: 'yarn' 43 | architecture: ${{ matrix.platform }} 44 | - uses: actions/setup-python@v5 45 | with: 46 | python-version: '3.11' 47 | - name: Prepare app repository 48 | uses: ./.github/actions/prepare-app-repo 49 | - name: Prepare fortify-setup repository 50 | uses: ./.github/actions/prepare-fortify-setup-repo 51 | - name: Create installer 52 | run: yarn --cwd ./fortify-setup build 53 | - name: Sign data 54 | run: yarn sign_data 55 | - name: Archive build artifacts 56 | uses: actions/upload-artifact@v3 57 | with: 58 | name: artifacts 59 | path: | 60 | ./release/*.jws 61 | ./release/*.pkg 62 | 63 | ubuntu: 64 | name: Create ubuntu installer 65 | runs-on: ubuntu-20.04 66 | needs: [checkout] 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: actions/setup-node@v4 70 | with: 71 | node-version: 16 72 | cache: 'yarn' 73 | architecture: ${{ matrix.platform }} 74 | - name: Install OS dependencies 75 | run: sudo apt-get install libpcsclite-dev 76 | - name: Prepare app repository 77 | uses: ./.github/actions/prepare-app-repo 78 | - name: Prepare fortify-setup repository 79 | uses: ./.github/actions/prepare-fortify-setup-repo 80 | - name: Create installer 81 | run: yarn --cwd ./fortify-setup build 82 | - name: Archive build artifacts 83 | uses: actions/upload-artifact@v3 84 | with: 85 | name: artifacts 86 | path: ./release/*.deb 87 | 88 | windows: 89 | name: Create windows installers 90 | runs-on: windows-2019 91 | needs: [checkout] 92 | strategy: 93 | matrix: 94 | platform: [x86, x64] 95 | env: 96 | Platform: ${{ matrix.platform }} 97 | steps: 98 | - uses: actions/checkout@v4 99 | - uses: actions/setup-node@v4 100 | with: 101 | node-version: 16 102 | cache: 'yarn' 103 | architecture: ${{ matrix.platform }} 104 | - name: Setup msbuild 105 | uses: microsoft/setup-msbuild@v1.1 106 | - name: Prepare app repository 107 | uses: ./.github/actions/prepare-app-repo 108 | - name: Prepare fortify-setup repository 109 | uses: ./.github/actions/prepare-fortify-setup-repo 110 | - name: Create installer 111 | run: yarn --cwd ./fortify-setup build 112 | - name: Archive build artifacts 113 | uses: actions/upload-artifact@v3 114 | with: 115 | name: artifacts 116 | path: ./release/*.msi 117 | 118 | create_release: 119 | name: Prepare and create draft release 120 | runs-on: ubuntu-20.04 121 | needs: [macos, ubuntu, windows] 122 | steps: 123 | - name: Download artifacts 124 | uses: actions/download-artifact@v3 125 | with: 126 | name: artifacts 127 | path: artifacts 128 | 129 | - name: Display structure of downloaded files 130 | run: ls -R 131 | working-directory: artifacts 132 | 133 | - name: Create draft release 134 | id: create_release 135 | uses: softprops/action-gh-release@v1 136 | with: 137 | tag_name: ${{ github.ref }} 138 | name: Draft release ${{ github.ref }} 139 | draft: true 140 | prerelease: false 141 | files: artifacts/* 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /build 4 | .vscode 5 | out 6 | node_modules 7 | *pvpkcs11*? 8 | .idea 9 | 10 | nss 11 | config.schema.json -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | runtime "electron" 2 | target "13.6.9" 3 | target_arch "x64" 4 | disturl "https://electronjs.org/headers" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.8.4](https://github.com/PeculiarVentures/fortify/releases/tag/1.8.4) (20.06.2022) 2 | 3 | ### Features 4 | 5 | - Update `electron` dependency to `13.6.9`. 6 | 7 | ### Bug Fixes 8 | 9 | - Fix ERR_CERT_AUTHORITY_INVALID exception on Mac ([#475](https://github.com/PeculiarVentures/fortify/issues/475)). 10 | - Fix Firefox in Ubuntu can't communicate with Fortify ([#461](https://github.com/PeculiarVentures/fortify/issues/461)). 11 | - Fix Unable to open fortify tools in Chrome ([#409](https://github.com/PeculiarVentures/fortify/issues/409)). 12 | - Fix App doesn't install CA certificate to Firefox ([#327](https://github.com/PeculiarVentures/fortify/issues/327)). 13 | - Fix Fortify modals stay open if client vanishes ([#485](https://github.com/PeculiarVentures/fortify/issues/485)). 14 | - Fix Fortify is not valid win32 application ([#440](https://github.com/PeculiarVentures/fortify/issues/440)). 15 | - Fix CardConfig ignores cards from options ([#272](https://github.com/PeculiarVentures/webcrypto-local/pull/272)). 16 | 17 | ### Other Changes 18 | 19 | - Update `minimist ` dependency to `1.2.6`. 20 | - Update `@webcrypto-local/*` dependency to `1.7.3`. 21 | - Update `typescript` dependency to `4.6.4`. 22 | - Update `asn1js` dependency to `3.0.3`. 23 | - Update `ts-node` dependency to `10.7.0`. 24 | - Update `protobufjs` dependency to `6.11.3`. 25 | - Use `nanoid` instead of `uuid`. 26 | - Update `it.json` ([#492](https://github.com/PeculiarVentures/fortify/issues/492)). 27 | 28 | 29 | ## [1.8.3](https://github.com/PeculiarVentures/fortify/releases/tag/1.8.3) (27.10.2021) 30 | 31 | ### Features 32 | 33 | - Add support to use custom driver ([#439](https://github.com/PeculiarVentures/fortify/issues/439)). 34 | - Update `electron` dependency to `11.5.0`. 35 | - Use `ts-loader` instead of `awesome-typescript-loader`. 36 | - Update `webcrypto-core` version to `1.3.0`. 37 | - Update `pvtsutils` version to `1.2.1`. 38 | - Update `pkijs` version to `2.2.1`. 39 | - Update `pkcs11js` version to `1.2.6`. 40 | - Update `asn1js` version to `2.1.1`. 41 | - Update `@peculiar/asn1-*` version to `2.0.38`. 42 | - Update `@webcrypto-local` version to `1.6.8`. 43 | 44 | ### Bug Fixes 45 | 46 | - Fix startup error in Ubuntu ([#436](https://github.com/PeculiarVentures/fortify/issues/436)). 47 | - Fortify is not valid win32 application ([#440](https://github.com/PeculiarVentures/fortify/issues/440)). 48 | - Fix PIN entered not shown until window moves ([#453](https://github.com/PeculiarVentures/fortify/issues/453)). 49 | 50 | ## [1.8.2](https://github.com/PeculiarVentures/fortify/releases/tag/1.8.2) (05.07.2021) 51 | 52 | ### Bug Fixes 53 | 54 | - Fix error on `ossl` property reading of null object. 55 | - Change driver for the `3BDF18008131FE7D006B020C0182011101434E53103180FC` token ([#423](https://github.com/PeculiarVentures/fortify/issues/423)). 56 | - Fix error on key generation ([#422](https://github.com/PeculiarVentures/fortify/issues/422)). 57 | - Fix error on IDPrime card removing ([#421](https://github.com/PeculiarVentures/fortify/issues/421)). 58 | 59 | ## [1.8.1](https://github.com/PeculiarVentures/fortify/releases/tag/1.8.1) (01.06.2021) 60 | 61 | ### Features 62 | 63 | - Added log with `PKCS#11` information. 64 | ```json 65 | {"source":"provider","library":"/usr/local/lib/libsoftokn3.dylib","manufacturerId":"Mozilla Foundation","cryptokiVersion":{"major":2,"minor":40},"libraryVersion":{"major":3,"minor":64},"firmwareVersion":{"major":0,"minor":0},"level":"info","message":"PKCS#11 library information","timestamp":"2021-05-26T09:57:30.827Z"} 66 | ``` 67 | - Supported configuration for `PKCS#11` templates. 68 | ```json 69 | { 70 | "id": "39b3d7a3662c4b48bb120d008dd18648", 71 | "name": "SafeNet Authentication Client", 72 | "config": { 73 | "template": { 74 | "copy": { 75 | "private": { 76 | "token": true, 77 | "sensitive": true, 78 | "extractable": false 79 | } 80 | } 81 | } 82 | } 83 | } 84 | ``` 85 | - Updated `PKCS#11` lib. It doesn't show System certificates. 86 | 87 | ## [1.8.0](https://github.com/PeculiarVentures/fortify/releases/tag/1.8.0) (17.02.2021) 88 | 89 | ### Features 90 | 91 | - Update preferences window UI ([#395](https://github.com/PeculiarVentures/fortify/pull/395)). 92 | - Update `electron` dependency to `11.2.3` ([#397](https://github.com/PeculiarVentures/fortify/pull/397). 93 | 94 | ## [1.7.0](https://github.com/PeculiarVentures/fortify/releases/tag/1.7.0) (08.02.2021) 95 | 96 | ### Features 97 | 98 | - Implement analytics ([#374](https://github.com/PeculiarVentures/fortify/pull/374)). 99 | - Update `@webcrypto-local` dependency to `1.6.0` ([#390](https://github.com/PeculiarVentures/fortify/pull/390), [#387](https://github.com/PeculiarVentures/fortify/issues/387)). 100 | 101 | ### Bug Fixes 102 | 103 | - Update `electron` dependency to `9.4.1` ([#388](https://github.com/PeculiarVentures/fortify/pull/388), [#386](https://github.com/PeculiarVentures/fortify/pull/386)). 104 | 105 | 106 | ## [1.5.0](https://github.com/PeculiarVentures/fortify/releases/tag/1.5.0) (23.11.2020) 107 | 108 | ### Features 109 | 110 | - Add scripts to create `update.jw`s and `card.jws`. 111 | - Add CI workflow to create installers for `macOS`, `linux (ubuntu)` and `windows (x64, x86)`. 112 | 113 | ### Bug Fixes 114 | 115 | - Update `@webcrypto-local/server` dependency to `1.5.2`. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!CAUTION] 2 | > **This Repository is no longer maintained**
3 | > This repository has been archived and is no longer maintained, also we are not going to be updating issues or pull requests on this repository. The application has been moved to a new [fortify-releases](https://github.com/PeculiarVentures/fortify-releases) repository. 4 | > If you are having a question about Fortify then please contact [support](mailto:support@fortifyapp.com). 5 | 6 | --- 7 | 8 |

9 | Fortify logo

10 |

11 | 12 |

Fortify Desktop

13 | 14 |

Fortify enables web applications to use smart cards, local certificate stores and do certificate enrollment. For Mac, Windows, and Linux.

15 | 16 |

17 | License: AGPL v3 18 | github release version 19 | github release downloads 20 |

21 | 22 | - [Background](#background) 23 | - [Architecture](#architecture) 24 | - [How does it work?](#how-does-it-work) 25 | - [How can I use it?](#how-can-i-use-it) 26 | - [Installing](#installing) 27 | - [Binaries](#binaries) 28 | - [Building from source](#building-from-source) 29 | 30 | ## Background 31 | Fortify is a client application that you install that runs in the background as a tray application in Windows, OSX, and Linux that provides these missing capabilities to authorized applications. 32 | 33 | It does this by binding to 127.0.0.1 and listening to a high-order well-known port for incoming requests. Browsers allow web applications to initiate sessions to this address, over that session a Fortify enabled application establishes a secure session and if approved by the user is allowed to access these missing capabilities. 34 | 35 | ## Architecture 36 | Fortify is a Node.js application based on Electron and it accesses all cryptographic implementations via node-webcrypto-p11. This library was designed to provide a WebCrypto compatible API to Node.js applications but it also extends the WebCrypto API to provide basic access to certificate stores. 37 | 38 | It uses another Peculiar Ventures project called PVPKCS11 to access the OSX KeyStore, Mozilla NSS or Windows CryptoAPI via this PKCS#11 wrapper. 39 | 40 | It also uses pcsclite to listen for a smart card or security token insertions and removals, when new insertions are detected it inspects the ATR of the card. If it is a known card the client attempts to load the PKCS#11 library associated with the card. If that succeeds events in the `webcrypto-socket` protocol are used to let the web application know about the availability of the new cryptographic and certificate provider. 41 | 42 | Ironically, despite the complication of the PKCS#11 API, this approach enables the code to maintain a fairly easy to understand structure. 43 | 44 | The application also includes a tray application that is used to help with debugging, access a test application and manage which domains can access the service. 45 | 46 | ## How does it work? 47 | At the core of Fortify is a library called 2key-ratchet. This implements a `Double Ratchet` protocol similar to what is used by Signal. In this protocol each peer has an identity key pair, we use the public keys from each participant to compute a short numeric value since in the protocol the peers prove control of the respective private keys we know that once the keys are authenticated we are talking to the same “identity”. 48 | 49 | Since 2key-ratchet uses WebCrypto we leverage the fact that keys generated in a web application are bound to the same origin, we also (when possible) utilize non-exportable keys to mitigate the risks of these approved keys from being stolen. 50 | 51 | This gives us an origin bound identity for the web application that the Fortify client uses as the principal in an Access Control List. This means if you visit a new site (a new origin), even if operated by the same organization, you will need to approve their access to use Fortify. 52 | 53 | For good measure (and browser compatibility) this exchange is also performed over a TLS session. At installation time a local CA is created, this CA is used to create an SSL certificate for 127.0.0.1. The private key of the CA is then deleted once the SSL certificate is created and the Root CA of the certificate chain is installed as a locally trusted CA. This prevents the CA from being abused to issue certificates for other origins. 54 | 55 | The protocol used by Fortify use a /.wellknown/ (not yet registered) location for capability discovery. The core protocol itself is Protobuf based. 56 | 57 | We call this protocol webcrypto-socket. You can think of the protocol as a Remote Procedure Call or (RPC) to the local cryptographic and certificate implementations in your operating system. 58 | 59 | ## How can I use it? 60 | 61 | Since the client SDK that implements the `webcrypto-socket` protocol is a superset of WebCrypto, with slight modifications, if you have an web application that uses WebCrypto you can also use locally enrolled certificates and/or smart cards. 62 | 63 | We have also created a number of web componentss that make using it easy, for example: 64 | 65 | - [Certificate Enrollment](https://fortifyapp.com/examples/certificate-enrollment) 66 | - [Certificate Selection](https://fortifyapp.com/examples/certificate-management) 67 | - [Signing](https://fortifyapp.com/examples/signing) 68 | 69 | 70 | ## Installing 71 | 72 | ### Binaries 73 | 74 | Visit the [the official website](https://fortifyapp.com/#download) to find the installer you need. 75 | 76 | ### Building from source 77 | 78 | ``` 79 | git clone git@github.com:PeculiarVentures/fortify.git 80 | cd fortify 81 | yarn 82 | yarn build 83 | yarn start 84 | ``` 85 | -------------------------------------------------------------------------------- /locale/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Jazyk", 3 | "yes": "Ano", 4 | "no": "Ne", 5 | "error": "Chyba", 6 | "question": "Dotaz", 7 | "warning": "Varování", 8 | "about": "O aplikaci", 9 | "about.app": "O Fortify", 10 | "exit": "Konec", 11 | "i_understand": "Rozumím", 12 | "logging": "Logování", 13 | "view.log": "Zobrazit log", 14 | "tools": "Nástroje", 15 | "sites": "Adresy", 16 | "search": "Hledat", 17 | "by": "Od", 18 | "smart.card": "Čipová karta", 19 | "close": "Zavřít", 20 | "ok": "OK", 21 | "cancel": "Zrušit", 22 | "approve": "Povolit", 23 | "show.again": "Nezobrazovat tento dialog znovu", 24 | "version": "Verze", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "Všechna práva vyhrazena", 28 | "pin": "PIN", 29 | "keys.empty": "Doposud nemáte žádné adresy jako důvěryhodné.", 30 | "p11-pin": "Čipová karta nebo Token", 31 | "p11-pin.1": "Webová stránka požaduje oprávnění k vašemu lokálnímu certifikátu a klíči.", 32 | "p11-pin.2": "Pro povolení akce musíte zadat přístupový PIN nebo heslo.", 33 | "key-pin": "Přístupová oprávnění", 34 | "key-pin.1": "%1 požaduje oprávnění k vašemu lokálnímu certifikátu a klíči.", 35 | "key-pin.2": "Pokud je uvedené číslo shodné s tím, které je zobrazeno v aplikaci, a chcete umožnit přístup, vyberte %1.", 36 | "error.ssl.install": "Nepodařilo se nainstalovat SSL certifikát pro Fortify mezi důvěryhodné kořenové certifikáty. Do vyřešení tohoto problému nebude aplikace pracovat správně.", 37 | "question.new.token": "Byla zjištěna nepodporovaná čipová karta nebo token.\nPřejete se odeslat žádost o zaregistrování nové karty/tokenu?", 38 | "question.2key.remove": "Chcete odebrat %1 ze seznamu důvěryhodných?", 39 | "warning.title.oh_no": "Ale ne, něco se nepovedlo!", 40 | "warn.ssl.install": "Pro správnou funkci je nutné označit Fortify SSL certifikát jako důvěryhodný. Při nastavení můžete být vyzván k zadání hesla správce.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "Vložená karta je aplikací Fortify podporována ale nepodařilo se zjistit správný ovladač. Ujistěte se, že soubor (%1) existuje. Pokud ne, nainstalujte ovladače pro čipovou kartu a zkuste znovu.", 43 | "warn.token.crypto_wrong": "Nepodařilo se spustit ovladače (%1) pro vaši čipovou kartu nebo token.\nUjistěte se, že je ovladač nainstalován správně a zkuste znovu.", 44 | "warn.pcsc.cannot_start": "Zdá se, že `Správce prostředků čipových karet` není spuštěn. Spusťte tuto službu a poté restartujte Fortify, prosím.", 45 | "keys.empty.search": "Tomuto vyhledávání neodpovídají žádné webové stránky.", 46 | "remove": "Odstranit", 47 | "settings": "Nastavení", 48 | "telemetry": "Telemetrie", 49 | "telemetry.enable": "Povolte zprávy o použití a selhání, které nám pomohou zajistit, aby budoucí verze Fortify řešily problémy, se kterými se můžete setkat", 50 | "theme": "Téma", 51 | "theme.system": "Použít nastavení systému", 52 | "theme.light": "Světlo", 53 | "theme.dark": "Tmavý", 54 | "updates": "Aktualizace", 55 | "updates.check": "Zkontrolovat aktualizace...", 56 | "updates.checking": "Hledání aktualizací", 57 | "updates.latest": "Používáte nejnovější verzi Fortify.", 58 | "updates.available": "Je k dispozici nová aktualizace.", 59 | "updates.available.learn": "Další informace o této aktualizaci", 60 | "download": "Stáhnout", 61 | "preferences": "Předvolby...", 62 | "quit": "Ukončit Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Sprache", 3 | "yes": "Ja", 4 | "no": "Nein", 5 | "error": "Fehler", 6 | "question": "Frage", 7 | "warning": "Warnung", 8 | "about": "Über", 9 | "about.app": "About Fortify", 10 | "exit": "Verlassen", 11 | "i_understand": "Ich verstehe", 12 | "logging": "Logging", 13 | "view.log": "Log einsehen", 14 | "tools": "Tools", 15 | "sites": "Seiten", 16 | "search": "Suche", 17 | "by": "nach", 18 | "smart.card": "Smart Card", 19 | "close": "Schliessen", 20 | "ok": "Ok", 21 | "cancel": "Abbrechen", 22 | "approve": "Zustimmen", 23 | "show.again": "Diesen Dialog nicht mehr anzeigen", 24 | "version": "Version", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": " Alle Rechte vorbehalten", 28 | "pin": "PIN", 29 | "keys.empty": "Sie vertrauen noch keiner Website.", 30 | "p11-pin": "Smart Card oder Token", 31 | "p11-pin.1": " Eine Website beantragt Zugriff auf lokale Zertifikate und Schlüssel ", 32 | "p11-pin.2": "Um den Zugriff zu erlauben werden der entsprechende PIN und das Passwort benötigt. ", 33 | "key-pin": "Zugriffsberechtigung", 34 | "key-pin.1": "%1 beantragt Zugriff auf lokale Zertifikate, Schlüssel und Smart Cards.", 35 | "key-pin.2": "Wenn diese Zahl mit der von der Applikation angezeigten Zahl übereinstimmt und Sie Zugriff erlauben wollen, wählen Sie %1.", 36 | "error.ssl.install": "Keine Möglichkeit ein SSL-Zertifikat für Fortify als Trusted-Root-Zertifikat zu installieren. Die Applikation kann nicht funktionieren, bis dies behoben wird.", 37 | "question.new.token": "Wir haben eine nicht unterstützte Smart Card oder Token erkannt.\nMöchten Sie Support für diesen Token hinzufügen?", 38 | "question.2key.remove": "Möchten Sie %1 von der Trusted-List entfernen?", 39 | "warning.title.oh_no": "Dies hat leider nicht funktioniert.", 40 | "warn.ssl.install": "Dem Fortify SSL-Zertifikat muss vertraut werden. Wenn dies ausgeführt wird, muss das Administrator Passwort eingegeben werden.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "Die verwendete Smart Card wird von Fortify unterstützt, jedoch wurde die Middleware für diese Karte nicht gefunden. Stellen Sie sicher das (%1) existiert, falls nicht, installieren Sie die entsprechende Middleware der Karte und versuchen Sie es erneut.", 43 | "warn.token.crypto_wrong": "Ein Problem ist beim Laden der Middleware (%1) des Security Tokens oder Smart Card aufgetreten. \nBitte stellen Sie sicher, dass die Middleware korrekt installiert wurde und versuchen Sie es erneut.", 44 | "warn.pcsc.cannot_start": "Der `Smart Card Resource Manager` ist möglicherweise nicht in Betrieb. Bitte starten Sie den Service und versuchen Sie es erneut.", 45 | "keys.empty.search": "Es gibt keine Websites, die dieser Suche entsprechen.", 46 | "remove": "Entfernen", 47 | "settings": "die Einstellungen", 48 | "telemetry": "Telemetrie", 49 | "telemetry.enable": "Aktivieren Sie Verwendungs- und Absturzberichte, um sicherzustellen, dass zukünftige Versionen von Fortify die Probleme beheben, die möglicherweise auftreten", 50 | "theme": "Thema", 51 | "theme.system": "Systemeinstellung verwenden", 52 | "theme.light": "Licht", 53 | "theme.dark": "Dunkel", 54 | "updates": "Aktualisierung", 55 | "updates.check": "Nach Updates suchen...", 56 | "updates.checking": "Nach Updates suchen", 57 | "updates.latest": "Sie haben die neueste Version von Fortify.", 58 | "updates.available": "Ein neues Update ist verfügbar.", 59 | "updates.available.learn": "Erfahren Sie mehr über dieses Update", 60 | "download": "Herunterladen", 61 | "preferences": "Einstellungen...", 62 | "quit": "Beenden Sie Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/el.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Γλώσσα", 3 | "yes": "Ναι", 4 | "no": "Οχι", 5 | "error": "Σφάλμα", 6 | "question": "Ερώτηση", 7 | "warning": "Προειδοποίηση", 8 | "about": "Σχετικά με", 9 | "about.app": "Σχετικά με το Fortify", 10 | "exit": "Έξοδος", 11 | "i_understand": "Καταλαβαίνω", 12 | "logging": "Σύνδεση", 13 | "view.log": "Προβολή καταγραφής", 14 | "tools": "Εργαλεία", 15 | "sites": "Ιστοσελίδες", 16 | "search": "Αναζήτηση", 17 | "by": "ανά", 18 | "smart.card": "Smart card", 19 | "close": "Κλείσιμο", 20 | "ok": "OK", 21 | "cancel": "Ακύρωση", 22 | "approve": "Εγκρίνω", 23 | "show.again": "Μη δείξεις ξανά αυτόν τον διάλογο", 24 | "version": "Έκδοση", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "Όλα τα δικαιώματα διατηρούνται", 28 | "pin": "PIN", 29 | "keys.empty": "Δεν εμπιστεύεσαι αυτήν την ιστοσελίδα ακόμα", 30 | "p11-pin": "Smart Card ή Token", 31 | "p11-pin.1": "Η ιστοσελίδα έχει ζητήσει πρόσβαση να χρησιμοποιήσει τα τοπικά πιστοποιητικά και τα κλειδιά σας.", 32 | "p11-pin.2": "Για να της παρέχετε πρόσβαση, πρέπει να δώσετε το PIN ή τον κωδικό που τα προστατεύει.", 33 | "key-pin": "Άδεια πρόσβασης", 34 | "key-pin.1": "%1 έχει ζητήσει πρόσβαση να χρησιμοποιήσει τα τοπικά πιστοποιητικά, τα κλειδιά και τα smart cards σας.", 35 | "key-pin.2": "Αν ο αριθμός ταιριάζει με αυτόν που προβάλλεται στην εφαρμογή και θέλετε να δώσετε πρόσβαση, επιλέξτε %1.", 36 | "error.ssl.install": "Δεν είναι δυνατή η εγκατάσταση του πιστοποιητικού SSL για το Fortify ως αξιόπιστου root πιστοποιητικού. Η εφαρμογή δεν θα λειτουργήσει μέχρι να επιλυθεί αυτό.", 37 | "question.new.token": "Ανιχνεύσαμε μη υποστηριζόμενo smart card ή token.\n Θα θέλατε να αιτηθείτε την προσθήκη υποστήριξης για αυτό το token;", 38 | "question.2key.remove": "Θέλετε να αφαιρέσετε το %1 από την αξιόπιστη λίστα", 39 | "warning.title.oh_no": "Ωχ όχι, αυτό δεν λειτούργησε!", 40 | "warn.ssl.install": "Πρέπει να κάνουμε το Fortify SSL πιστοποιητικό αξιόπιστο. Όταν το κάνουμε, θα σας ζητηθεί ο κωδικός διαχειριστή.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "Η τοποθετημένη smart card υποστηρίζεται από το Fortify αλλά δεν μπορέσαμε να βρούμε το middleware για αυτήν. Σιγουρευτείτε ότι το (%1) υπάρχει, αν όχι, εγκαταστήστε το middlewware της smart card και προσπαθήστε ξανά.", 43 | "warn.token.crypto_wrong": "Προέκυψε ένα πρόβλημα φορτώνοντας το middleware (%1) για το security token ή την smart card σας.\nΠαρακαλούμε σιγουρευτείτε ότι το middleware είναι σωστά εγκατεστημένοκαι προσπαθήστε ξανά.", 44 | "warn.pcsc.cannot_start": "Φαίνεται πως το `Smart Card Resource Manager` δεν τρέχει.Παρακαλούμε εκκινήστε την υπηρεσία και εκκινήστε το Fortify ξανά.", 45 | "keys.empty.search": "Δεν υπάρχουν ιστοσελίδες που να ταιριάζουν με αυτή την αναζήτηση.", 46 | "remove": "Αφαίρεση", 47 | "settings": "Ρυθμίσεις", 48 | "telemetry": "Τηλεμετρία", 49 | "telemetry.enable": "Ενεργοποιήστε τις αναφορές χρήσης και σφαλμάτων για να μας βοηθήσετε να διασφαλίσουμε ότι οι μελλοντικές εκδόσεις του Fortify αντιμετωπίζουν τα προβλήματα που ενδέχεται να αντιμετωπίσετε", 50 | "theme": "Θέμα", 51 | "theme.system": "Χρήση ρύθμισης συστήματος", 52 | "theme.light": "Φως", 53 | "theme.dark": "Σκούρο", 54 | "updates": "Ενημερώσεις", 55 | "updates.check": "Έλεγχος για ενημερώσεις...", 56 | "updates.checking": "Αναζητώντας ενημερώσεις", 57 | "updates.latest": "Βρίσκεστε στην τελευταία έκδοση του Fortify.", 58 | "update.available": "Μια νέα ενημέρωση είναι διαθέσιμη.", 59 | "updates.available.learn": "Μάθετε περισσότερα σχετικά με αυτήν την ενημέρωση", 60 | "download": "Λήψη", 61 | "preferences": "Προτιμήσεις...", 62 | "quit": "Κλείστε το Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Language", 3 | "yes": "Yes", 4 | "no": "No", 5 | "error": "Error", 6 | "question": "Question", 7 | "warning": "Warning", 8 | "about": "About", 9 | "about.app": "About Fortify", 10 | "exit": "Exit", 11 | "i_understand": "I understand", 12 | "logging": "Logging", 13 | "view.log": "View log", 14 | "tools": "Tools", 15 | "sites": "Sites", 16 | "search": "Search", 17 | "by": "by", 18 | "smart.card": "Smart Card", 19 | "close": "Close", 20 | "ok": "Ok", 21 | "cancel": "Cancel", 22 | "approve": "Approve", 23 | "show.again": "Don't show this dialog again", 24 | "version": "Version", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "All rights reserved", 28 | "pin": "PIN", 29 | "keys.empty": "You do not trust any sites yet.", 30 | "p11-pin": "Smart card or Token", 31 | "p11-pin.1": "A website has requested permission to use your local certificates and keys.", 32 | "p11-pin.2": "To provide it access you will need to provide the PIN or password protecting them.", 33 | "key-pin": "Access permission", 34 | "key-pin.1": "%1 has requested permission to use your local certificates, keys, and smart cards.", 35 | "key-pin.2": "If this number matches what is displayed by the application and you want to provide access choose %1.", 36 | "error.ssl.install": "Unable to install the SSL certificate for Fortify as a trusted root certificate. The application will not work until this is resolved.", 37 | "question.new.token": "We detected a unsupported smart card or token.\nWould you like to request support be added for this token?", 38 | "question.2key.remove": "Do you want to remove %1 from the trusted list?", 39 | "warning.title.oh_no": "Oh no, that did not work!", 40 | "warn.ssl.install": "We need to make the Fortify SSL certificate trusted. When we do this you will be asked for your administrator password.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "The inserted smart card is supported by Fortify but we were unable to find middleware for the card. Make sure (%1) exists, if not install the smart cards middleware and try again.", 43 | "warn.token.crypto_wrong": "A problem occurred loading the middleware (%1) for your security token or smart card.\nPlease make sure the middleware is installed correctly and try again.", 44 | "warn.pcsc.cannot_start": "It looks like the `Smart Card Resource Manager` is not running. Please start this service and start Fortify again.", 45 | "keys.empty.search": "There are no websites matching that search.", 46 | "remove": "Remove", 47 | "settings": "Settings", 48 | "telemetry": "Telemetry", 49 | "telemetry.enable": "Enable usage and crash reports to help us ensure future versions of Fortify address the issues you may experience", 50 | "theme": "Theme", 51 | "theme.system": "Use system setting", 52 | "theme.light": "Light", 53 | "theme.dark": "Dark", 54 | "updates": "Updates", 55 | "updates.check": "Check For Updates...", 56 | "updates.checking": "Cheking for updates", 57 | "updates.latest": "You're on the latest version of Fortify.", 58 | "updates.available": "A new update is available.", 59 | "updates.available.learn": "Learn more about this update", 60 | "download": "Download", 61 | "preferences": "Preferences...", 62 | "quit": "Quit Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Idioma", 3 | "yes": "Sí", 4 | "no": "No", 5 | "error": "Error", 6 | "question": "Pregunta", 7 | "warning": "Advertencia", 8 | "about": "Acerca de", 9 | "about.app": "Acerca de Fortify", 10 | "exit": "Salir", 11 | "i_understand": "Entiendo", 12 | "logging": "Gestionar log", 13 | "view.log": "Ver el log", 14 | "tools": "Herramientas", 15 | "sites": "Sitios", 16 | "search": "Buscar", 17 | "by": "Por", 18 | "smart.card": "Smartcard", 19 | "close": "Cerrar", 20 | "ok": "OK", 21 | "cancel": "Cancelar", 22 | "approve": "Autorizar", 23 | "show.again": "No volver a mostrar este diálogo", 24 | "version": "Versión", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "Todos derechos reservados", 28 | "pin": "PIN", 29 | "keys.empty": "Todavía no confía en ningún sitio.", 30 | "p11-pin": "Smartcard o Token", 31 | "p11-pin.1": "Un sitio web ha solicitado la autorización para usar sus certificados y claves locales.", 32 | "p11-pin.2": "Para darle acceso, necesita proveer el PIN o la contraseña que los protege.", 33 | "key-pin": "Autorización de acceso", 34 | "key-pin.1": "%1 ha solicitado la autorización para usar sus certificados, claves y smartcards locales.", 35 | "key-pin.2": "Si este número corresponde a lo que muestra la aplicación, y si quiere autorizar el acceso elegir %1.", 36 | "error.ssl.install": "No se puede instalar el certifiado SSL para Fortify como un certificado raíz confiable. La aplicación no funcionará hasta que este problema esté resuelto.", 37 | "question.new.token": "Hemos detectado un smartcard o token no soportado.\n¿Quiere solicitar que se añada soporte para este dispositivo?", 38 | "question.2key.remove": "¿Quiere sacar %1 de la lista de confianza?", 39 | "warning.title.oh_no": "¡Oh no, falló!", 40 | "warn.ssl.install": "Necesitamos instalar el certificado SSL para Fortify como certificado confiable. Cuando lo hagamos, se le solicitará su contraseña de administración.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "La smartcard insertada está soportada por Fortify, pero no se pudo encontrar el middleware para esta tarjeta. Asegurese que (%1) existe, si no, instale el middleware de la smartcard e intente de nuevo.", 43 | "warn.token.crypto_wrong": "Un problema se produjo al cargar el middleware (%1) para su smartcard o token.\nPor favor asegurese que el middleware está instalado correctamente y vuelva a intentar.", 44 | "warn.pcsc.cannot_start": "Parece que el `Smart Card Resource Manager` no está corriendo. Por favor iniciar este servicio y volver a iniciar Fortify.", 45 | "keys.empty.search": "No hay sitios web que coincidan con esa búsqueda.", 46 | "remove": "Eliminar", 47 | "settings": "Configuraciones", 48 | "telemetry": "Telemetría", 49 | "telemetry.enable": "Habilite los informes de uso y fallas para ayudarnos a garantizar que las versiones futuras de Fortify aborden los problemas que pueda experimentar", 50 | "theme": "Tema", 51 | "theme.system": "Usar configuración del sistema", 52 | "theme.light": "Luz", 53 | "theme.dark": "Oscuro", 54 | "updates": "Actualizaciones", 55 | "updates.check": "Buscar actualizaciones...", 56 | "updates.checking": "Buscando actualizaciones", 57 | "updates.latest": "Estás en la última versión de Fortify.", 58 | "updates.available": "Hay una nueva actualización disponible.", 59 | "updates.available.learn": "Obtenga más información sobre esta actualización", 60 | "download": "Descargar", 61 | "preferences": "Preferencias...", 62 | "quit": "Salir de Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Langue", 3 | "yes": "Oui", 4 | "no": "Non", 5 | "error": "Erreur", 6 | "question": "Question", 7 | "warning": "Attention", 8 | "about": "À propos", 9 | "about.app": "À propos de Fortify", 10 | "exit": "Quitter", 11 | "i_understand": "Compris!", 12 | "logging": "Journaux", 13 | "view.log": "Afficher le journal", 14 | "tools": "Outils", 15 | "sites": "Sites", 16 | "search": "Chercher", 17 | "by": "Par", 18 | "smart.card": "Smartcard", 19 | "close": "Fermer", 20 | "ok": "OK", 21 | "cancel": "Annuler", 22 | "approve": "Autoriser", 23 | "show.again": "Ne plus afficher ce dialogue", 24 | "version": "Version", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "Tous droits réservés", 28 | "pin": "PIN", 29 | "keys.empty": "Vous ne faites pour l'instant confiance à aucun site.", 30 | "p11-pin": "Smartcard ou Token", 31 | "p11-pin.1": "Un site web a demandé la permission pour utiliser vos certificats et clés locaux.", 32 | "p11-pin.2": "Pour autoriser l'accès, vous devrez fournir le PIN ou le mot de passe qui les protège.", 33 | "key-pin": "Demande d'accès", 34 | "key-pin.1": "%1 a demandé la permission pour utiliser vos certificats, clés et smartcards locaux.", 35 | "key-pin.2": "Si le numéro correspond à ce qui est affiché par l'application, et si vous souhaitez fournir l'accès, choisissez %1.", 36 | "error.ssl.install": "Impossible d'installer le certificat SSL pour Fortify comme un certificat racine de confiance. L'application ne pourra pas fonctionner tant que ce problème ne sera pas résolu.", 37 | "question.new.token": "Nous avons détecté une smartcard ou token non prise en charge.\nVoulez-vous solliciter la prise en charge de ce token ?", 38 | "question.2key.remove": "Voulez-vous retirer %1 de la liste de confiance ?", 39 | "warning.title.oh_no": "Oh non, ça n'a pas marché !", 40 | "warn.ssl.install": "Nous avons besoin que le certificat SSL pour Fortify soit considéré de confiance. Lorsque nous allons le configurer, votre mot de passe d'administration vous sera demandé.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "La smartcard insérée est prise en charge par Fortify mais nous n'arrivons pas à trouver le middleware pour cette smartcard. Assurez-vous que (%1) existe, sinon installez le middleware de la smartcard et essayez à nouveau.", 43 | "warn.token.crypto_wrong": "Un problème s'est produit au chargement du middleware (%1) de votre token ou smartcard.\nVeuillez vérifier que le middleware est installé correctement et essayez à nouveau.", 44 | "warn.pcsc.cannot_start": "Il semble que `Smart Card Resource Manager` ne soit pas lancé. Veuillez démarrer le service puis redémarrer Fortify.", 45 | "keys.empty.search": "Aucun site Web ne correspond à cette recherche.", 46 | "remove": "Retirer", 47 | "settings": "Réglages", 48 | "telemetry": "Télémétrie", 49 | "telemetry.enable": "Activez l'utilisation et les rapports d'erreur pour nous aider à nous assurer que les futures versions de Fortify résoudront les problèmes que vous pourriez rencontrer", 50 | "theme": "Thème", 51 | "theme.system": "Utiliser les paramètres système", 52 | "theme.light": "Lumière", 53 | "theme.dark": "Sombre", 54 | "updates": "Mises à jour", 55 | "updates.check": "Rechercher des mises à jour...", 56 | "updates.checking": "À la recherche de mises à jour", 57 | "updates.latest": "Vous utilisez la dernière version de Fortify.", 58 | "updates.available": "Une nouvelle mise à jour est disponible.", 59 | "updates.available.learn": "En savoir plus sur cette mise à jour", 60 | "download": "Télécharger", 61 | "preferences": "Preferences...", 62 | "quit": "Quitter Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Lingua", 3 | "yes": "Sì", 4 | "no": "No", 5 | "error": "Errore", 6 | "question": "Domanda", 7 | "warning": "Avviso", 8 | "about": "Informazioni", 9 | "about.app": "Informazioni su Fortify", 10 | "exit": "Esci", 11 | "i_understand": "Ho capito", 12 | "logging": "Logging", 13 | "view.log": "Visualizza log", 14 | "tools": "Strumenti", 15 | "sites": "Siti", 16 | "search": "Ricerca", 17 | "by": "Da", 18 | "smart.card": "Smart card", 19 | "close": "Chiudi", 20 | "ok": "Ok", 21 | "cancel": "Cancella", 22 | "approve": "Accetta", 23 | "show.again": "Non mostrare più questa finestra di dialogo", 24 | "version": "Versione", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Realizzato con ❤️ in tutto il mondo", 27 | "all.rights": "Tutti i diritti riservati", 28 | "pin": "PIN", 29 | "keys.empty": "Non ci sono siti attendibili.", 30 | "p11-pin": "Smart card o token", 31 | "p11-pin.1": "Un sito internet ha richiesto l’autorizzazione per utilizzare i tuoi certificati e chiavi locali.", 32 | "p11-pin.2": "Per garantire l’accesso, è necessario inserire il PIN o la password di protezione.", 33 | "key-pin": "Autorizzazione d’accesso", 34 | "key-pin.1": "%1 ha chiesto l’autorizzazione per utilizzare i tuoi certificati e chiavi locali e le tue smart card.", 35 | "key-pin.2": "Se questo numero corrisponde a quanto indicato dall’applicazione e desideri fornire l’accesso, scegli %1.", 36 | "error.ssl.install": "Impossibile installare il certificato SSL radice di Fortify come certificato radice attendibile. L’applicazione non funzionerà fino a quando questo problema non sarà risolto.", 37 | "question.new.token": "Abbiamo rilevato una smart card o un token non supportata/o.\nDesideri richiedere che sia aggiunto il supporto per questo token?", 38 | "question.2key.remove": "Desideri rimuovere %1 dall'elenco dei siti attendibili?", 39 | "warning.title.oh_no": "Oh no, non ha funzionato!", 40 | "warn.ssl.install": "È necessario installare il certificato SSL radice di Fortify. Per eseguire questa operazione potrebbe essere necessaria la password di amministratore.", 41 | "warn.ssl.renew": "Il certificato SSL di Fortify deve essere rinnovato. Esegui l'installer di Fortify per rinnovare il certificato.", 42 | "warn.token.crypto_not_found": "La smart card inserita è supportata da Fortify, ma non è stato trovato il driver per il dispositivo. Assicurarsi che il file (%1) esista e in caso contrario installare il driver della smart card.", 43 | "warn.token.crypto_wrong": "Si è verificato un problema durante il caricamento del driver (%1) per il tuo token di sicurezza o la tua smart card.\nAssicurarsi che il driver sia installato correttamente e riprovare.", 44 | "warn.pcsc.cannot_start": "Sembra che lo `Smart Card Resource Manager` non funzioni. Imposta questo servizio e riavvia Fortify.", 45 | "keys.empty.search": "Non ci sono siti che corrispondono alla ricerca.", 46 | "remove": "Rimuovi", 47 | "settings": "Impostazioni", 48 | "telemetry": "Telemetria", 49 | "telemetry.enable": "Abilita i rapporti sull'utilizzo e sugli arresti anomali per assicurarci che le versioni future di Fortify risolvano i problemi che potresti riscontrare", 50 | "theme": "Tema", 51 | "theme.system": "Usa impostazioni di sistema", 52 | "theme.light": "Luce", 53 | "theme.dark": "Scuro", 54 | "updates": "Aggiornamenti", 55 | "updates.check": "Controlla gli aggiornamenti...", 56 | "updates.checking": "Ricerca aggiornamenti in corso", 57 | "updates.latest": "Sei nella versione più recente di Fortify.", 58 | "updates.available": "È disponibile un nuovo aggiornamento.", 59 | "updates.available.learn": "Ulteriori informazioni su questo aggiornamento", 60 | "download": "Scarica", 61 | "preferences": "Preferenze...", 62 | "quit": "Esci da Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "言語", 3 | "yes": "はい", 4 | "no": "いいえ", 5 | "error": "エラー", 6 | "question": "質問", 7 | "warning": "警告", 8 | "about": "本アプリについて", 9 | "about.app": "Fortifyについて", 10 | "exit": "アプリを閉じる", 11 | "i_understand": "わかりました", 12 | "logging": "ロギング", 13 | "view.log": "ログを見る", 14 | "tools": "ツール", 15 | "sites": "サイト", 16 | "search": "検索", 17 | "by": "提供元", 18 | "smart.card": "スマートカード", 19 | "close": "閉じる", 20 | "ok": "OK", 21 | "cancel": "キャンセル", 22 | "approve": "承認", 23 | "show.again": "このダイアログをもう表示しない", 24 | "version": "バージョン", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "All rights reserved", 28 | "pin": "PIN", 29 | "keys.empty": "どのサイトもまだ信頼していません。", 30 | "p11-pin": "スマートカードまたはトークン", 31 | "p11-pin.1": "ウェブサイトはローカルの証明書並びに鍵を利用するための権限をリクエストしました。", 32 | "p11-pin.2": "このアクセスを提供するためには保護しているPINまたはパスワードが必要となります。", 33 | "key-pin": "アクセス権限", 34 | "key-pin.1": "%1 はローカルの証明書、鍵やスマートカードを利用するための権限をリクエストしました。", 35 | "key-pin.2": "この番号がアプリケーションによって表示されるものと一致し、アクセスを提供する場合は、%1 を選択してください。", 36 | "error.ssl.install": "信頼されるルート証明書としてFortifyで利用されるSSL証明書をインストールできません。本問題を解決するまで本アプリケーションは利用できません。", 37 | "question.new.token": "サポートしていないスマートカードまたはトークンを検知しました。\nこのトークンを追加するようサポートへリクエストを希望しますか?", 38 | "question.2key.remove": "信頼リストから %1 を削除しますか?", 39 | "warning.title.oh_no": "これは動作しませんでした!", 40 | "warn.ssl.install": "Fortify SSL証明書を信頼することが必要となります。信頼する際に管理者パスワードを聞かれます。", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "挿入されているスマートカードはFortifyによりサポートされていますが、カードのミドルウエアを見つけることができませんでした。 本スマートカードミドルウエアがインストールされていない場合、 (%1) が存在するかを確認し、再試行願います。", 43 | "warn.token.crypto_wrong": "セキュリティトークンもしくはスマートカードのミドルウエア (%1) のロード中に問題が発生しました。\n該当のミドルウエアが正しくインストールされているか確認し、再試行願います。", 44 | "warn.pcsc.cannot_start": "`Smart Card Resource Manager`が動作していないようにみえます。本サービスを開始し、再度Fortifyを起動してください。", 45 | "keys.empty.search": "その検索に一致するウェブサイトはありません。", 46 | "remove": "削除する", 47 | "settings": "設定", 48 | "telemetry": "テレメトリー", 49 | "telemetry.enable": "使用状況とクラッシュレポートを有効にして、Fortifyの将来のバージョンで発生する可能性のある問題に確実に対処できるようにします", 50 | "theme": "テーマ", 51 | "theme.system": "システム設定を使用", 52 | "theme.light": "光", 53 | "theme.dark": "闇", 54 | "updates": "更新", 55 | "updates.check": "アップデートを確認...", 56 | "updates.checking": "更新の確認", 57 | "updates.latest": "Fortifyの最新バージョンを使用しています。", 58 | "updates.available": "新しいアップデートが利用可能です。", 59 | "updates.available.learn": "このアップデートの詳細", 60 | "download": "ダウンロード", 61 | "preferences": "環境設定...", 62 | "quit": "Fortifyを終了します" 63 | } 64 | -------------------------------------------------------------------------------- /locale/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Taal", 3 | "yes": "Ja", 4 | "no": "Nee", 5 | "error": "Foutmelding", 6 | "question": "Vraag", 7 | "warning": "Waarschuwing", 8 | "about": "Over", 9 | "about.app": "Over Fortify", 10 | "exit": "Verlaten", 11 | "i_understand": "Ik begrijp het", 12 | "logging": "Logging", 13 | "view.log": "Bekijk Log", 14 | "tools": "Hulpmiddelen", 15 | "sites": "Sites", 16 | "search": "Zoek", 17 | "by": "Door", 18 | "smart.card": "Smart Card", 19 | "close": "Sluit", 20 | "ok": "OK", 21 | "cancel": "Annuleer", 22 | "approve": "Goedkeuren", 23 | "show.again": "Laat dit dialoogvenster niet meer verschijnen", 24 | "version": "Versie", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "Alle rechten voorbehouden", 28 | "pin": "PIN", 29 | "keys.empty": "U heeft nog geen vertrouwde sites.", 30 | "p11-pin": "Smart Card of Token", 31 | "p11-pin.1": "Een website heeft gevraagd uw lokale certificaten en sleutels te gebruiken.", 32 | "p11-pin.2": "U dient uw PIN of Password op te geven om toegang te verkrijgen.", 33 | "key-pin": "Toegangsrechten", 34 | "key-pin.1": "%1 heeft toestemming gevraagd uw lokale certificaten, sleutels en Smart Cards te gebruiken.", 35 | "key-pin.2": "Wanneer dit nummer overeenkomt met het getoonde nummer van de applicatie en u toegang wilt verkrijgen, kies %1.", 36 | "error.ssl.install": "Het systeem is niet in staat om het SSL certificaat voor Fortify te installeren als een vertouwd rootcertificaat. De applicatie zal niet werken tot dit probleem is opgelost.", 37 | "question.new.token": "Wij signaleren een niet ondersteunde Smart Card of Token.\nWilt u een verzoek indienen aan support om deze toe te voegen?", 38 | "question.2key.remove": "Wilt u %1 verwijderen uit de Vertouwde Lijst?", 39 | "warning.title.oh_no": "Helaas, dit werkt niet!", 40 | "warn.ssl.install": "Het Fortify SSL certificaat dient als betrouwbaar te worden gezien. Wanneer wij dit doen, zult u uw Administrator Password moeten invoeren.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "De ingestoken Smart Card wordt ondersteund door Fortify, maar de middleware kan niet worden gelokaliseerd. Controleer of deze is geïnstalleerd, zo niet, installeer deze en probeer het nogmaals.", 43 | "warn.token.crypto_wrong": "Er is een probleem opgetreden bij het laden van de middleware (%1) voor uw beveiligde token of Smart Card.\nControleer of de middelware correct is geïnstalleerd en probeer het nogmaals.", 44 | "warn.pcsc.cannot_start": "Het lijkt erop dat de 'Smart Card Resource Manager' niet actief is. Start deze service en herstart Fortify.", 45 | "keys.empty.search": "Er zijn geen websites die overeenkomen met die zoekopdracht.", 46 | "remove": "Verwijderen", 47 | "settings": "Instellingen", 48 | "telemetry": "Telemetrie", 49 | "telemetry.enable": "Schakel gebruiks- en crashrapporten in om ons te helpen ervoor te zorgen dat toekomstige versies van Fortify de problemen aanpakken die u mogelijk ondervindt", 50 | "theme": "Thema", 51 | "theme.system": "Gebruik systeeminstelling", 52 | "theme.light": "Licht", 53 | "theme.dark": "Donker", 54 | "updates": "Updates", 55 | "updates.check": "Controleer op updates...", 56 | "updates.checking": "Zoeken naar updates", 57 | "updates.latest": "Je gebruikt de laatste versie van Fortify.", 58 | "updates.available": "Er is een nieuwe update beschikbaar.", 59 | "updates.available.learn": "Meer informatie over deze update", 60 | "download": "Downloaden", 61 | "preferences": "Voorkeuren...", 62 | "quit": "Sluit Fortify af" 63 | } 64 | -------------------------------------------------------------------------------- /locale/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Idioma", 3 | "yes": "Sim", 4 | "no": "Não", 5 | "error": "Erro", 6 | "question": "Pergunta", 7 | "warning": "Aviso", 8 | "about": "Sobre", 9 | "about.app": "Sobre Fortify", 10 | "exit": "Sair", 11 | "i_understand": "Eu compreendo", 12 | "logging": "Logging", 13 | "view.log": "Ver log", 14 | "tools": "Ferramentas", 15 | "sites": "Sites", 16 | "search": "Procurar", 17 | "by": "por", 18 | "smart.card": "SmartCard", 19 | "close": "Fechar", 20 | "ok": "Ok", 21 | "cancel": "Cancelar", 22 | "approve": "Aprovar", 23 | "show.again": "Não exibir essa caixa de diálogo novamente", 24 | "version": "Versão", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Feito com ❤️ através do mundo", 27 | "all.rights": "Todos os direitos reservados", 28 | "pin": "PIN", 29 | "keys.empty": "Você não possui sites de confiança ainda.", 30 | "p11-pin": "Smart card or Token", 31 | "p11-pin.1": "Um website pediu permissão para utilizar seus certificados e chaves locais.", 32 | "p11-pin.2": "Para fornecer acesso, você precisará fornecer o PIN ou a senha que os protege.", 33 | "key-pin": "Permissão de acesso", 34 | "key-pin.1": "%1 pedui permissão para utilizar seus certificados locais, chaves, e smartcards.", 35 | "key-pin.2": "Se este número corresponder ao que é exibido pelo aplicativo e você deseja fornecer acesso, escolha %1.", 36 | "error.ssl.install": "Não foi possível instalar o certificado SSL para Fortify como um certificado raiz confiável. O aplicativo não funcionará até que isso seja resolvido.", 37 | "question.new.token": "Nós detectamos um Smartcard ou Token não compatível.\n Deseja solicitar suporte para este token?", 38 | "question.2key.remove": "Você gostaria de remover %1 da lista de confiáveis?", 39 | "warning.title.oh_no": "Ah não, isto não funcionou!", 40 | "warn.ssl.install": "Precisamos tornar o certificado SSL do Fortify confiável. Quando fizermos isso, será solicitada sua senha de administrador.", 41 | "warn.ssl.renew": "Certificado SSL precisa de renovação. Por favor execute o instalador Fortify para renovar o certificado.", 42 | "warn.token.crypto_not_found": "O Smartcard inserido é suportado pelo Fortify porém nós não conseguimos encontrar um middleware para este cartão. Assegure-se que (%1) exista, se não, instale o middleware do Smartcard e tente novamente.", 43 | "warn.token.crypto_wrong": "Um problema ocorreu na leitura do middleware (%1) para o token de segurança ou Smartcard.\nPor favor assegure-se que o middleware está instalado corretamente e tente novamente.", 44 | "warn.pcsc.cannot_start": "Parece que o `Gerenciador de SmartCard` não está sendo executado. Por favor inicie o serviço e inicie o Fortify novamente.", 45 | "keys.empty.search": "Não há sites que correspondam a essa pesquisa.", 46 | "remove": "Remover", 47 | "settings": "Configurações", 48 | "telemetry": "Telemetria", 49 | "telemetry.enable": "Habilite relatórios de uso e falhas para nos ajudar a garantir que versões futuras do Fortify resolvam os problemas que você pode enfrentar", 50 | "theme": "Tema", 51 | "theme.system": "Usar configuração do sistema", 52 | "theme.light": "Light", 53 | "theme.dark": "Dark", 54 | "updates": "Atualizações", 55 | "updates.check": "Verificar Atualizações...", 56 | "updates.checking": "Procurando por atualizações", 57 | "updates.latest": "Você está usando a versão mais recente do Fortify.", 58 | "updates.available": "Uma nova atualização está disponível.", 59 | "updates.available.learn": "Saiba mais sobre esta atualização", 60 | "download": "Download", 61 | "preferences": "Preferências...", 62 | "quit": "Sair do Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Язык", 3 | "yes": "Да", 4 | "no": "Нет", 5 | "error": "Ошибка", 6 | "question": "Вопрос", 7 | "warning": "Предупреждение", 8 | "about": "О программе", 9 | "about.app": "О Fortify", 10 | "exit": "Выход", 11 | "i_understand": "Я понимаю", 12 | "logging": "Журнал", 13 | "view.log": "Показать", 14 | "tools": "Инструменты", 15 | "sites": "Сайты", 16 | "search": "Поиск", 17 | "by": "от", 18 | "smart.card": "Симарт карта", 19 | "close": "Закрыть", 20 | "ok": "Ок", 21 | "cancel": "Отмена", 22 | "approve": "Разрешить", 23 | "show.again": "Больше не показывать этот диалог", 24 | "version": "Версия", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Сделано с ❤️ по всему миру", 27 | "all.rights": "Все права защищены", 28 | "pin": "ПИН", 29 | "keys.empty": "Список доверенных сайтов пуст.", 30 | "p11-pin": "Смарт карта или Токен", 31 | "p11-pin.1": "Веб сайт запрашивает разрешение на использование ваших сертификатов и ключей.", 32 | "p11-pin.2": "Для обеспечения доступа вам необходимо ввести ПИН.", 33 | "key-pin": "Разрешение доступа", 34 | "key-pin.1": "%1 запрашивает доступ на использование ваших локальных сертификатов, токенов и смарт карт.", 35 | "key-pin.2": "Если числа на экране совпадают с числами в приложении и вы хотите разрешить доступ, то нажмите кнопку %1.", 36 | "error.ssl.install": "Невозможно установить SSL сертификат Fortify в доверенное хранилище. Приложение будет закрыто", 37 | "question.new.token": "Был обнаружен новый токен или смарт карта.\n\nОтправить запрос в поддержку о добавлении нового устройства?", 38 | "question.2key.remove": "Вы хотите удалить %1 из доверенного списка?", 39 | "warning.title.oh_no": "Ох нет, что-то пошло не так!", 40 | "warn.ssl.install": "Необходимо установить SSL сертификат Fortify в доверенное хранилище. Во время установки могут быть запрошены права администратора.", 41 | "warn.ssl.renew": "Сертификат SSL требует продления. Запустите установщик Fortify, чтобы обновить сертификат.", 42 | "warn.token.crypto_not_found": "Отсутствует драйвер для подключенной смарт карты. Убедитесь, что файл '%1' существует, если нет то установите драйвер для вашей смарт карты.", 43 | "warn.token.crypto_wrong": "Возникла проблема с загрузкой драйвера '%1' вашей смарт карты.\nПожалуйста, проверьте корректность установки драйвера и попробуйте снова.", 44 | "warn.pcsc.cannot_start": "Невозможно подключиться к службе `Менеджер смарт-карт`. Возможно служба не запущена. Пожалуйста, запустите службу и заново откройте приложение Fortify.", 45 | "keys.empty.search": "Совпадений не найдено.", 46 | "remove": "Удалить", 47 | "settings": "Настройки", 48 | "telemetry": "Телеметрия", 49 | "telemetry.enable": "Включить отчеты об использовании и сбоях, чтобы гарантировать, что будущие версии Fortify решают проблемы, с которыми вы можете столкнуться", 50 | "theme": "Тема", 51 | "theme.system": "Использовать системные настройки", 52 | "theme.light": "Светлая", 53 | "theme.dark": "Темная", 54 | "updates": "Обновления", 55 | "updates.check": "Проверить наличие обновлений...", 56 | "updates.checking": "Проверка обновлений", 57 | "updates.latest": "Вы используете последнюю версию Fortify.", 58 | "updates.available": "Доступно новое обновление.", 59 | "updates.available.learn": "Подробнее об этом обновлении", 60 | "download": "Скачать", 61 | "preferences": "Параметры...", 62 | "quit": "Выйти из Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /locale/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Dil", 3 | "yes": "Evet", 4 | "no": "Hayır", 5 | "error": "Hata", 6 | "question": "Soru", 7 | "warning": "Uyarı", 8 | "about": "Hakkında", 9 | "about.app": "Fortify Hakkında", 10 | "exit": "Çıkış", 11 | "i_understand": "Anladım", 12 | "logging": "Loglama", 13 | "view.log": "Logları aç", 14 | "tools": "Araçlar", 15 | "sites": "Web Siteleri", 16 | "search": "Arama", 17 | "by": "tarafından", 18 | "smart.card": "Akıllı Kart", 19 | "close": "Kapat", 20 | "ok": "Tamam", 21 | "cancel": "İptal", 22 | "approve": "Onay", 23 | "show.again": "Bu uyarıyı bir daha gösterme", 24 | "version": "Sürüm", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "Tüm hakları saklıdır", 28 | "pin": "PIN", 29 | "keys.empty": "Güvenli web sitesi bulunmuyor.", 30 | "p11-pin": "Akıllı kart veya Token", 31 | "p11-pin.1": "Bir web sitesi lokal sertifika ve anahtarları kullanmak için izin istiyor.", 32 | "p11-pin.2": "Erişim izni veya şifre ile koruma için PIN giriniz.", 33 | "key-pin": "Erişim izni", 34 | "key-pin.1": "%1 lokal sertifika, akıllı kart ve anahtarları kullanmak için izin istedi.", 35 | "key-pin.2": "Eğer bu sayı uygulama tarafından gösterilen sayı ile aynı ise ve erişim izni vermek istiyorsanız lütfen %1 seçeneğini seçin.", 36 | "error.ssl.install": "Fortify için SSL sertifikası güvenilir kök sertifika olarak kurulamadı. Bu problem çözülüne kadar uygulama çalışmayacaktır.", 37 | "question.new.token": "Desteklenmeyen akıllı kart veta token tespit edildi.We detected a unsupported smart card or token.\nWould you like to request support be added for this token?", 38 | "question.2key.remove": "%1 güvenli listeden çıkartılacak. İşleme devam etmek istiyor musunuz?", 39 | "warning.title.oh_no": "Üzgünüm, bu işe yaramadı!", 40 | "warn.ssl.install": "Fortify SSL sertifikasını bilgisayarınızda güvenilir sertifikalar listesine almamız gerekiyor. Bu nedenle size sistem yöneticisi şifresi sorulacaktır.", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "Takılan akıllı kart Fortify tarafından destekleniyor ancak bu kart için ara katman yazılımını bulumadık. (%1) bulunduğundan emin olun, bulunmuyorsa lütfen akıllı kart için ara katman yazılımını kurun ve tekrar deneyin.", 43 | "warn.token.crypto_wrong": "Güvenlik token'ı veya akıllı kart için (%1) ara katman yazılımının yüklenmesi sırasında hata oluştu.\nLütfen ara katman yazılımının doğru şekilde kurulu olduğundan emin olun ve tekrar deneyin.", 44 | "warn.pcsc.cannot_start": "`Smart Card Resource Manager` servisi çalışmıyor. Lütfen servisi çalıştırın ve sonrasında Fortify'ı tekrar açın.", 45 | "keys.empty.search": "Arama kriterlerine uyan web sitesi bulunamadı.", 46 | "remove": "Kaldır", 47 | "settings": "Ayarlar", 48 | "telemetry": "Telemetri", 49 | "telemetry.enable": "Fortify'ın gelecekteki sürümlerinin karşılaşabileceğiniz sorunları ele almasını sağlamamıza yardımcı olmak için kullanım ve kilitlenme raporlarını etkinleştirin", 50 | "theme": "Tema", 51 | "theme.system": "Sistem ayarını kullan", 52 | "theme.light": "Işık", 53 | "theme.dark": "Koyu", 54 | "updates": "Güncellemeler", 55 | "updates.check": "Güncellemeleri Kontrol Et...", 56 | "updates.checking": "Güncellemeleri kontrol etme", 57 | "updates.latest": "Fortify'ın en son sürümündesiniz.", 58 | "updates.available": "Yeni bir güncelleme var.", 59 | "updates.available.learn": "Bu güncelleme hakkında daha fazla bilgi edinin", 60 | "download": "İndir", 61 | "preferences": "Tercihler...", 62 | "quit": "Fortify'dan Çık" 63 | } 64 | -------------------------------------------------------------------------------- /locale/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "语言", 3 | "yes": "是", 4 | "no": "否", 5 | "error": "错误", 6 | "question": "问题", 7 | "warning": "警告", 8 | "about": "关于", 9 | "about.app": "关于Fortify", 10 | "exit": "推出", 11 | "i_understand": "我理解", 12 | "logging": "登录中", 13 | "view.log": "查看日志", 14 | "tools": "工具", 15 | "sites": "网站", 16 | "search": "搜索", 17 | "by": "by", 18 | "smart.card": "智能卡", 19 | "close": "关闭", 20 | "ok": "好", 21 | "cancel": "取消", 22 | "approve": "允许", 23 | "show.again": "不要再显示此对话框", 24 | "version": "版本", 25 | "copyright": "© Peculiar Ventures, LLC", 26 | "made.with": "Made with ❤️ across the globe", 27 | "all.rights": "保留所有权利", 28 | "pin": "PIN码", 29 | "keys.empty": "你还没有添加信任的网站", 30 | "p11-pin": "智能卡或Token", 31 | "p11-pin.1": "网站请求访问本地的证书和私钥。", 32 | "p11-pin.2": "请输入PIN码或者密码以继续访问。", 33 | "key-pin": "访问权限", 34 | "key-pin.1": "%1 已请求使用您的本地证书,密钥和智能卡的许可。", 35 | "key-pin.2": "如果此数字与应用程序显示的数字相匹配,请选择您要提供的访问 %1.", 36 | "error.ssl.install": "无法将Fortify的SSL证书安装为受信任的根证书,程序无法正常工作。", 37 | "question.new.token": "我们检查到一个不支持的智能卡或者Token\n您想要请求支持这个Token吗?", 38 | "question.2key.remove": "您想要从可信列表删除 %1 吗?", 39 | "warning.title.oh_no": "不能正常运行", 40 | "warn.ssl.install": "我们需要使Fortify SSL证书可信。 执行此操作时,系统会要求您输入管理员密码。", 41 | "warn.ssl.renew": "SSL certificate requires renew. Please run the Fortify installer to renew a certificate.", 42 | "warn.token.crypto_not_found": "Fortify支持插入的智能卡,但我们无法找到该卡的中间件请确保(%1)存在。如果没有安装智能卡中间件,请重新尝试。", 43 | "warn.token.crypto_wrong": "加载安全令牌或智能卡的中间件(%1)时出现问题。\n请确保中间件安装正确并重试.", 44 | "warn.pcsc.cannot_start": "看起来“智能卡资源管理器”没有运行。 请启动此服务并再次启动Fortify。", 45 | "keys.empty.search": "沒有與該搜索匹配的網站。", 46 | "remove": "去掉", 47 | "settings": "設定值", 48 | "telemetry": "遙測", 49 | "telemetry.enable": "啟用使用情況和崩潰報告以幫助我們確保Fortify的未來版本能夠解決您可能遇到的問題", 50 | "theme": "主题", 51 | "theme.system": "使用系统设置", 52 | "theme.light": "光", 53 | "theme.dark": "黑暗的", 54 | "updates": "更新", 55 | "updates.check": "检查更新...", 56 | "updates.checking": "正在检查更新", 57 | "updates.latest": "您使用的是Fortify的最新版本。", 58 | "updates.available": "有新的更新可用。", 59 | "updates.available.learn": "了解有关此更新的更多信息", 60 | "download": "下载", 61 | "preferences": "首选项...", 62 | "quit": "退出Fortify" 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fortify", 3 | "productName": "Fortify", 4 | "version": "2.0.0", 5 | "description": "Fortify enables web applications to use smart cards, local certificate stores and do certificate enrollment", 6 | "main": "out/main.js", 7 | "scripts": { 8 | "postinstall": "ts-node scripts/postinstall", 9 | "sign_data": "ts-node scripts/sign_data", 10 | "start": "electron .", 11 | "build:main": "webpack --config scripts/webpack.main.config.js", 12 | "build:renderer": "webpack --config scripts/webpack.renderer.config.js", 13 | "build:prod": "cross-env NODE_ENV=production yarn build:main && cross-env NODE_ENV=production yarn build:renderer", 14 | "build:dev": "yarn build:main && yarn build:renderer", 15 | "build:schema": "typescript-json-schema ./src/main/types.ts IConfigureJson --required --noExtraProps --strictNullChecks > ./config.schema.json", 16 | "build": "yarn build:prod", 17 | "clear": "rimraf out build", 18 | "rebuild": "yarn clear && yarn build", 19 | "lint": "eslint --ext .js,.jsx,.ts,.tsx ./", 20 | "test": "mocha", 21 | "upgrade": "yarn upgrade-interactive" 22 | }, 23 | "keywords": [], 24 | "author": { 25 | "name": "Peculiar Ventures", 26 | "email": "info@peculiarventures.com" 27 | }, 28 | "license": "AGPL", 29 | "devDependencies": { 30 | "@types/classnames": "^2.2.10", 31 | "@types/extract-zip": "^1.6.2", 32 | "@types/mocha": "^9.1.1", 33 | "@types/node": "^12.12.51", 34 | "@types/react": "^16.9.43", 35 | "@types/react-dom": "^16.9.8", 36 | "@types/request": "^2.48.5", 37 | "@types/rimraf": "^3.0.2", 38 | "@types/semver": "^6.2.1", 39 | "@types/websocket": "^0.0.40", 40 | "@types/ws": "^7.2.6", 41 | "@types/xmldom": "^0.1.30", 42 | "@typescript-eslint/eslint-plugin": "^5.23.0", 43 | "@typescript-eslint/parser": "^5.23.0", 44 | "colors": "^1.4.0", 45 | "cross-env": "^7.0.2", 46 | "css-loader": "^5.2.6", 47 | "electron": "13.6.9", 48 | "eslint": "^8.15.0", 49 | "eslint-config-airbnb": "^19.0.4", 50 | "eslint-config-airbnb-typescript": "^17.0.0", 51 | "eslint-plugin-import": "^2.26.0", 52 | "eslint-plugin-jsx-a11y": "^6.5.1", 53 | "eslint-plugin-react": "^7.29.4", 54 | "extract-zip": "^1.7.0", 55 | "fortify-prepare": "github:PeculiarVentures/fortify-prepare", 56 | "json-parser": "^3.1.2", 57 | "mocha": "^10.0.0", 58 | "node-gyp": "^9.0.0", 59 | "prop-types": "^15.7.2", 60 | "request-progress": "^3.0.0", 61 | "rimraf": "^3.0.2", 62 | "sass": "^1.34.0", 63 | "sass-loader": "^10.2.0", 64 | "source-map-loader": "^1.1.3", 65 | "style-loader": "^1.2.1", 66 | "ts-loader": "^8.2.0", 67 | "ts-node": "^10.8.1", 68 | "typescript": "^4.7.3", 69 | "typescript-json-schema": "^0.53.1", 70 | "webpack": "^4.46.0", 71 | "webpack-cli": "^4.9.1", 72 | "webpack-merge": "^4.2.2" 73 | }, 74 | "dependencies": { 75 | "2key-ratchet": "^1.0.18", 76 | "@babel/polyfill": "^7.10.4", 77 | "@peculiar/asn1-schema": "^2.1.9", 78 | "@peculiar/asn1-x509": "^2.1.9", 79 | "@peculiar/webcrypto": "1.0.22", 80 | "@webcrypto-local/cards": "^1.10.2", 81 | "@webcrypto-local/server": "^1.7.8", 82 | "asn1js": "^3.0.5", 83 | "classnames": "^2.2.6", 84 | "get-proxy-settings": "^0.1.11", 85 | "jose-jwe-jws": "github:microshine/js-jose", 86 | "lib-react-components": "^3.0.1", 87 | "mixpanel": "^0.13.0", 88 | "nanoid": "^3.3.2", 89 | "pkcs11js": "^1.3.0", 90 | "pkijs": "^3.0.5", 91 | "public-ip": "^4.0.3", 92 | "pvtsutils": "^1.3.2", 93 | "react": "^16.13.1", 94 | "react-dom": "^16.13.1", 95 | "react-router-dom": "^6.16.0", 96 | "reflect-metadata": "^0.1.13", 97 | "request": "^2.88.2", 98 | "semver": "^7.5.2", 99 | "tsyringe": "^4.7.0", 100 | "webcrypto-core": "^1.7.5", 101 | "winston": "^3.3.3", 102 | "winston-transport": "^4.4.0" 103 | }, 104 | "resolutions": { 105 | "asn1js": "^3.0.5", 106 | "@peculiar/asn1-schema": "^2.1.9", 107 | "pkijs": "^3.0.5" 108 | }, 109 | "mocha": { 110 | "require": [ 111 | "ts-node/register" 112 | ], 113 | "extension": [ 114 | "ts" 115 | ], 116 | "spec": [ 117 | "test/**/*.ts" 118 | ] 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /scripts/sign_data/crypto.ts: -------------------------------------------------------------------------------- 1 | import { Crypto } from '@peculiar/webcrypto'; 2 | 3 | export const crypto = new Crypto(); 4 | -------------------------------------------------------------------------------- /scripts/sign_data/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import * as jose from 'jose-jwe-jws'; 6 | import { crypto } from './crypto'; 7 | import { getKeyPair } from './keys'; 8 | 9 | const { 10 | PRIVATE_KEY_BASE64, 11 | PUBLIC_KEY_BASE64, 12 | OUTPUT_FOLDER_PATH, 13 | } = process.env; 14 | 15 | async function signData(keys: CryptoKeyPairEx, data: any) { 16 | const cryptographer = new jose.Jose.WebCryptographer(); 17 | cryptographer.setContentSignAlgorithm('RS256'); 18 | const signer = new jose.JoseJWS.Signer(cryptographer); 19 | await signer.addSigner(keys.privateKey, keys.kid); 20 | 21 | const doc = await signer.sign(data); 22 | const jws = doc.CompactSerialize(); 23 | 24 | return jws; 25 | } 26 | 27 | async function verifyData(keys: CryptoKeyPairEx, jws: string) { 28 | const cryptographer = new jose.Jose.WebCryptographer(); 29 | cryptographer.setContentSignAlgorithm('RS256'); 30 | 31 | const verifier = new jose.JoseJWS.Verifier(cryptographer, jws); 32 | await verifier.addRecipient(keys.publicKey, keys.kid); 33 | 34 | return verifier.verify(); 35 | } 36 | 37 | async function signUpdateJSON(keys: CryptoKeyPairEx, info: IUpdateInfoJson) { 38 | const jws = await signData(keys, info); 39 | 40 | if (!await verifyData(keys, jws)) { 41 | throw new Error('JWS has bad signature'); 42 | } 43 | 44 | return jws; 45 | } 46 | 47 | async function signCardJSON(keys: CryptoKeyPairEx, path: string) { 48 | // Read card.json 49 | const json = fs.readFileSync(path, 'utf8'); 50 | const card = JSON.parse(json); 51 | 52 | // Sign data 53 | console.log('card.json version: %s', card.version); 54 | 55 | return signData(keys, card); 56 | } 57 | 58 | async function main() { 59 | const { version } = require('../../package.json'); 60 | 61 | if (!PRIVATE_KEY_BASE64) { 62 | throw new Error('Missed required env variable "PRIVATE_KEY_BASE64".'); 63 | } 64 | 65 | if (!PUBLIC_KEY_BASE64) { 66 | throw new Error('Missed required env variable "PUBLIC_KEY_BASE64".'); 67 | } 68 | 69 | if (!OUTPUT_FOLDER_PATH) { 70 | throw new Error('Missed required env variable "OUTPUT_FOLDER_PATH".'); 71 | } 72 | 73 | jose.setCrypto(crypto as any); 74 | 75 | const keys = await getKeyPair(PRIVATE_KEY_BASE64, PUBLIC_KEY_BASE64); 76 | const jws = await signUpdateJSON(keys, { version, createdAt: Date.now() }); 77 | const jwsCard = await signCardJSON(keys, path.resolve('./node_modules/@webcrypto-local/cards/lib/card.json')); 78 | const outPath = path.resolve(OUTPUT_FOLDER_PATH); 79 | const outPathUpdateJws = path.join(outPath, './update.jws'); 80 | const outPathCardJws = path.join(outPath, './card.jws') 81 | 82 | if (!fs.existsSync(outPath)) { 83 | fs.mkdirSync(outPath); 84 | } 85 | 86 | fs.writeFileSync(outPathUpdateJws, jws, { flag: 'w+' }); 87 | console.log('\nupdate.jws created successfully:', outPathUpdateJws); 88 | 89 | fs.writeFileSync(outPathCardJws, jwsCard, { flag: 'w+' }); 90 | console.log('\ncard.jws created successfully:', outPathCardJws); 91 | } 92 | 93 | main() 94 | .catch((error) => { 95 | console.log(error); 96 | 97 | process.exit(1); 98 | }); 99 | -------------------------------------------------------------------------------- /scripts/sign_data/keys.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { Convert } from 'pvtsutils'; 4 | import { crypto } from './crypto'; 5 | 6 | async function getKID(pubKey: CryptoKey) { 7 | const raw = await crypto.subtle.exportKey('spki', pubKey); 8 | const hash = await crypto.subtle.digest('SHA-256', raw); 9 | 10 | return Convert.ToBase64(hash); 11 | } 12 | 13 | async function getPrivateKey(privateKey: string) { 14 | const json = JSON.parse(Convert.ToString(Convert.FromBase64(privateKey))); 15 | const key = await crypto.subtle.importKey('jwk', json.keyJwk, json.algorithm, false, ['sign']); 16 | 17 | return key; 18 | } 19 | 20 | async function getPublicKey(publicKey: any) { 21 | const json = JSON.parse(Convert.ToString(Convert.FromBase64(publicKey))); 22 | const key = await crypto.subtle.importKey('jwk', json.keyJwk, json.algorithm, true, ['verify']); 23 | 24 | return key; 25 | } 26 | 27 | export async function getKeyPair(privateKey: any, publicKey: any) { 28 | const keyPair: CryptoKeyPairEx = { 29 | privateKey: await getPrivateKey(privateKey), 30 | publicKey: await getPublicKey(publicKey), 31 | kid: '', 32 | }; 33 | const kid = await getKID(keyPair.publicKey); 34 | 35 | // Update keys structure 36 | keyPair.kid = kid; 37 | 38 | return keyPair; 39 | } 40 | -------------------------------------------------------------------------------- /scripts/sign_data/types.d.ts: -------------------------------------------------------------------------------- 1 | interface IKeyStorage { 2 | setItem(key: string, item: CryptoKey): void; 3 | getItem(key: string): CryptoKey | null; 4 | } 5 | 6 | declare interface Crypto { 7 | keyStorage: IKeyStorage; 8 | } 9 | 10 | declare interface CryptoKey { 11 | kid: string; 12 | } 13 | 14 | interface IUpdateInfoJson { 15 | version: string; 16 | createdAt: number; 17 | min?: string; 18 | } 19 | 20 | interface CryptoKeyPairEx extends CryptoKeyPair { 21 | kid: string; 22 | } 23 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable no-console */ 3 | import * as childProcess from 'child_process'; 4 | import * as os from 'os'; 5 | import * as fs from 'fs'; 6 | import { stdout } from 'process'; 7 | import * as request from 'request'; 8 | import * as zip from 'extract-zip'; 9 | import 'colors'; 10 | 11 | const progress = require('request-progress'); 12 | 13 | export class Logger { 14 | public static info(message: string) { 15 | this.log(message.cyan); 16 | } 17 | 18 | public static debug(message: string) { 19 | this.log(message.gray); 20 | } 21 | 22 | public static error(error: string | Error) { 23 | if (typeof error === 'string') { 24 | this.log(error.red); 25 | } else { 26 | this.log(`${error.name}: ${error.message}`.red); 27 | this.debug(error.stack || ''); 28 | } 29 | } 30 | 31 | public static log(message: string) { 32 | console.log(message); 33 | } 34 | } 35 | 36 | /** 37 | * Calls commands and print message about it to console 38 | * @param command 39 | * @param args 40 | * @param message 41 | */ 42 | export function spawn(command: string, args: string[] = []) { 43 | return new Promise((resolve, reject) => { 44 | Logger.debug(`> ${command} ${args.join(' ')}`); 45 | 46 | let item: childProcess.ChildProcess; 47 | if (os.platform() === 'win32') { 48 | item = childProcess.spawn(command, args, { stdio: 'inherit', shell: 'cmd' }); 49 | } else { 50 | item = childProcess.spawn(command, args, { stdio: 'inherit', shell: 'bash' }); 51 | } 52 | item 53 | .on('message', (msg) => { 54 | process.stdout.write(msg); 55 | }) 56 | .on('close', (code) => { 57 | if (code) { 58 | reject(new Error(`Command finished with code ${code}`)); 59 | } else { 60 | resolve(); 61 | } 62 | }) 63 | .on('error', reject); 64 | }); 65 | } 66 | 67 | /** 68 | * Runs script and exits from program at the end 69 | * @param cb Script implementation 70 | */ 71 | export async function run(cb: () => Promise) { 72 | try { 73 | await cb(); 74 | 75 | process.exit(0); 76 | } catch (e) { 77 | Logger.error(e instanceof Error || typeof e === "string" ? e : new Error("Unknown error")); 78 | process.exit(1); 79 | } 80 | } 81 | 82 | /** 83 | * Downloads file 84 | * @param url 85 | * @param dest 86 | */ 87 | export async function download(url: string, dest: string) { 88 | return new Promise((resolve, reject) => { 89 | Logger.debug(`Downloading ${url}`); 90 | 91 | progress(request(url) 92 | .on('response', (resp) => { 93 | if (resp.statusCode !== 200) { 94 | fs.unlinkSync(dest); 95 | reject(new Error(`${resp.statusMessage}(${resp.statusCode})`)); 96 | } 97 | })) 98 | .on('progress', (state: any) => { 99 | // write percentage 100 | stdout.write(`Progress ${Math.floor(state.percent * 100)}%\r`.gray); 101 | }) 102 | .on('error', reject) 103 | .on('end', () => { 104 | stdout.write('Progress 100%\n'.gray); 105 | resolve(); 106 | }) 107 | .pipe(fs.createWriteStream(dest)); 108 | }); 109 | } 110 | 111 | export async function extract(zipFile: string, absolutePath: string) { 112 | return new Promise((resolve, reject) => { 113 | zip(zipFile, { dir: absolutePath }, (err) => { 114 | if (err) { 115 | reject(err); 116 | } else { 117 | resolve(); 118 | } 119 | }); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /scripts/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || 'development'; 2 | 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | 6 | const isDev = process.env.NODE_ENV === 'development'; 7 | 8 | module.exports = { 9 | mode: isDev ? 'development' : 'production', 10 | devtool: isDev ? 'source-map' : 'none', 11 | output: { 12 | path: path.resolve(__dirname, '../out'), 13 | filename: '[name].js', 14 | }, 15 | node: { 16 | __dirname: false, 17 | __filename: false, 18 | Buffer: false, 19 | }, 20 | resolve: { 21 | extensions: ['.tsx', '.ts', '.js', '.json'], 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.tsx?$/, 27 | loader: 'ts-loader', 28 | }, 29 | { 30 | enforce: 'pre', 31 | test: /\.js$/, 32 | loader: 'source-map-loader', 33 | }, 34 | ], 35 | }, 36 | plugins: [ 37 | new webpack.DefinePlugin({ 38 | 'process.env.NODE_ENV': JSON.stringify(isDev ? 'development' : 'production'), 39 | }), 40 | ], 41 | optimization: { 42 | minimize: !isDev, 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /scripts/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const baseConfig = require('./webpack.base.config'); 4 | 5 | module.exports = merge.smart(baseConfig, { 6 | target: 'electron-main', 7 | entry: { 8 | main: path.join(__dirname, '../src/main/index.ts'), 9 | }, 10 | externals: { 11 | pkcs11js: 'require("pkcs11js")', 12 | pcsclite: 'require("pcsclite")', 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('webpack-merge'); 3 | const baseConfig = require('./webpack.base.config'); 4 | 5 | module.exports = merge.smart(baseConfig, { 6 | target: 'electron-renderer', 7 | entry: { 8 | index: path.join(__dirname, '../src/renderer/index.tsx'), 9 | }, 10 | optimization: { 11 | splitChunks: { 12 | cacheGroups: { 13 | commons: { 14 | test: /[\\/]node_modules[\\/]/, 15 | name: 'vendors', 16 | chunks: 'all', 17 | }, 18 | }, 19 | }, 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.sass$/, 25 | use: [ 26 | 'style-loader', 27 | { 28 | loader: 'css-loader', 29 | options: { 30 | importLoaders: 1, 31 | modules: { 32 | mode: 'local', 33 | localIdentName: '[local]_[hash:base64:5]', 34 | }, 35 | }, 36 | }, 37 | 'sass-loader', 38 | ], 39 | }, 40 | ], 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /src/main/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { nanoid } from 'nanoid'; 4 | import { APP_CONFIG_FILE } from './constants'; 5 | import { IConfigure } from './types'; 6 | 7 | const defaultConfig: IConfigure = { 8 | userId: nanoid(36), 9 | providers: [], 10 | cards: [], 11 | disableCardUpdate: false, 12 | logging: false, 13 | telemetry: true, 14 | locale: 'en', 15 | theme: 'system', 16 | }; 17 | 18 | /** 19 | * Set application config. 20 | * @param config Config data 21 | */ 22 | export function setConfig(config: IConfigure) { 23 | const json = JSON.stringify(config, null, ' '); 24 | const parentDirname = path.dirname(APP_CONFIG_FILE); 25 | 26 | if (!fs.existsSync(parentDirname)) { 27 | fs.mkdirSync(parentDirname); 28 | } 29 | 30 | fs.writeFileSync(APP_CONFIG_FILE, json, { flag: 'w+' }); 31 | } 32 | 33 | /** 34 | * Get application config. 35 | */ 36 | export function getConfig(): IConfigure { 37 | const isConfigExist = fs.existsSync(APP_CONFIG_FILE); 38 | 39 | if (isConfigExist) { 40 | const json = fs.readFileSync(APP_CONFIG_FILE, 'utf8'); 41 | let config = JSON.parse(json) as IConfigure; 42 | 43 | // Add existing keys to config. 44 | if (Object.keys(defaultConfig).join('') !== Object.keys(config).join('')) { 45 | config = { 46 | ...defaultConfig, 47 | ...config, 48 | }; 49 | 50 | setConfig(config); 51 | } 52 | 53 | return config; 54 | } 55 | 56 | // Save default config. 57 | setConfig(defaultConfig); 58 | 59 | return defaultConfig; 60 | } 61 | -------------------------------------------------------------------------------- /src/main/constants/design.ts: -------------------------------------------------------------------------------- 1 | export const windowSizes = { 2 | small: { 3 | width: 500, 4 | height: 300, 5 | }, 6 | default: { 7 | width: 600, 8 | height: 500, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/main/constants/files.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as path from 'path'; 3 | 4 | /** 5 | * Home directory 6 | */ 7 | export const HOME_DIR = os.homedir(); 8 | /** 9 | * Fortify data available for user only 10 | */ 11 | export const APP_USER_DIR = path.join(HOME_DIR, '.fortify'); 12 | /** 13 | * Fortify data available for all users 14 | */ 15 | export const APP_DATA_DIR = (os.platform() === 'win32') 16 | ? path.join(process.env.ProgramData!, 'Fortify') 17 | : APP_USER_DIR; 18 | 19 | export const APP_DIR = path.join(__dirname, '..'); 20 | export const SRC_DIR = path.join(APP_DIR, 'src'); 21 | export const RESOURCES_DIR = path.join(SRC_DIR, 'resources'); 22 | export const STATIC_DIR = path.join(SRC_DIR, 'static'); 23 | export const HTML_PATH = path.join(STATIC_DIR, 'index.html'); 24 | export const ICON_DIR = path.join(STATIC_DIR, 'icons'); 25 | 26 | export const APP_LOG_FILE = path.join(APP_USER_DIR, 'fortify.log'); 27 | export const APP_CONFIG_FILE = path.join(APP_USER_DIR, 'config.json'); 28 | /** 29 | * Path to dialog.json file. Allows to disable/enable warning dialogs showing 30 | */ 31 | export const APP_DIALOG_FILE = path.join(APP_USER_DIR, 'dialog.json'); 32 | export const APP_SSL_CERT_CA = path.join(APP_DATA_DIR, 'ca.pem'); 33 | export const APP_SSL_CERT = path.join(APP_DATA_DIR, 'cert.pem'); 34 | export const APP_SSL_KEY = path.join(APP_DATA_DIR, 'cert.key'); 35 | export const APP_CARD_JSON = path.join(APP_USER_DIR, 'card.json'); 36 | export const TEMPLATE_NEW_CARD_FILE = path.join(RESOURCES_DIR, 'new_card.tmpl'); 37 | 38 | export const CHECK_UPDATE = true; 39 | export const CHECK_UPDATE_INTERVAL = 24 * 60 * 60e3; // 24h 40 | 41 | export const icons = { 42 | tray: path.join(ICON_DIR, 'tray/png/icon.png'), 43 | trayNotification: path.join(ICON_DIR, 'tray_notification/png/icon.png'), 44 | favicon: path.join(ICON_DIR, 'tray/png/icon@2x.png'), 45 | }; 46 | -------------------------------------------------------------------------------- /src/main/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './design'; 2 | export * from './files'; 3 | export * from './third_party'; 4 | export * from './links'; 5 | 6 | export const isDevelopment = process.env.NODE_ENV === 'development'; 7 | -------------------------------------------------------------------------------- /src/main/constants/links.ts: -------------------------------------------------------------------------------- 1 | export const APP_CARD_JSON_LINK = 'https://fortifyapp.com/packages/card.jws'; 2 | export const JWS_LINK = 'https://fortifyapp.com/packages/update.jws'; 3 | export const DOWNLOAD_LINK = 'https://fortifyapp.com/#download_app'; 4 | export const GITHUB_REPO_LINK = 'https://github.com/PeculiarVentures/fortify'; 5 | export const TOOLS_LINK = 'https://tools.fortifyapp.com/'; 6 | -------------------------------------------------------------------------------- /src/main/constants/third_party.ts: -------------------------------------------------------------------------------- 1 | export const MIXPANEL_TOKEN = '124773c76c7622781c7bbef371a07c93'; 2 | -------------------------------------------------------------------------------- /src/main/container.ts: -------------------------------------------------------------------------------- 1 | import { container, Lifecycle } from 'tsyringe'; 2 | import { Server } from './server'; 3 | 4 | container.register('server', Server, { 5 | lifecycle: Lifecycle.ContainerScoped, 6 | }); 7 | 8 | export default container; 9 | -------------------------------------------------------------------------------- /src/main/crypto.ts: -------------------------------------------------------------------------------- 1 | import { Crypto } from '@peculiar/webcrypto'; 2 | 3 | export const crypto = new Crypto() as globalThis.Crypto; 4 | 5 | (global as any).crypto = crypto; 6 | -------------------------------------------------------------------------------- /src/main/errors.ts: -------------------------------------------------------------------------------- 1 | export class UpdateError extends Error { 2 | public type = 'UpdateError'; 3 | 4 | public name = 'UpdateError'; 5 | 6 | public critical: boolean; 7 | 8 | constructor(message: string, critical = false) { 9 | super(message); 10 | 11 | this.critical = critical; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/firefox_providers.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | import logger from './logger'; 5 | import { IConfigureProvider } from './types'; 6 | 7 | const create = () => { 8 | const providers: IConfigureProvider[] = []; 9 | // Get User's Firefox profile 10 | let firefoxProfilesDir = ''; 11 | let lib = ''; 12 | 13 | switch (os.platform()) { 14 | case 'win32': { 15 | firefoxProfilesDir = path.join(os.homedir(), 'AppData', 'Roaming', 'Mozilla', 'Firefox', 'Profiles'); 16 | lib = path.normalize(`${process.execPath}/../softokn3.dll`); 17 | break; 18 | } 19 | case 'linux': { 20 | firefoxProfilesDir = path.join(os.homedir(), '.mozilla', 'firefox'); 21 | lib = '/usr/lib/x86_64-linux-gnu/nss/libsoftokn3.so'; 22 | break; 23 | } 24 | case 'darwin': { 25 | firefoxProfilesDir = path.join(os.homedir(), 'Library', 'Application Support', 'Firefox', 'Profiles'); 26 | lib = path.normalize(`${process.execPath}/../libsoftokn3.dylib`); 27 | break; 28 | } 29 | default: 30 | // nothing 31 | } 32 | 33 | if (!firefoxProfilesDir) { 34 | logger.info('firefox-providers', 'Cannot get default Firefox profiles folder for OS', { 35 | platform: os.platform(), 36 | }); 37 | 38 | return providers; 39 | } 40 | 41 | if (!fs.existsSync(firefoxProfilesDir)) { 42 | logger.info('firefox-providers', 'Provider does not exist', { 43 | dir: firefoxProfilesDir, 44 | }); 45 | 46 | return providers; 47 | } 48 | 49 | const profiles = fs.readdirSync(firefoxProfilesDir); 50 | 51 | // eslint-disable-next-line 52 | for (const profile of profiles) { 53 | const profileDir = path.join(firefoxProfilesDir, profile); 54 | // get pkcs11.txt file 55 | const pkcs11File = path.join(profileDir, 'pkcs11.txt'); 56 | 57 | if (!fs.existsSync(pkcs11File)) { 58 | logger.info('firefox-providers', 'Cannot get pkcs11.txt', { 59 | dir: profileDir, 60 | }); 61 | 62 | continue; 63 | } 64 | 65 | // get parameters from pkcs11.txt 66 | const pkcs11 = fs.readFileSync(pkcs11File, 'utf8'); 67 | const params = /parameters=(.+)/g.exec(pkcs11); 68 | 69 | if (!params) { 70 | logger.info('firefox-providers', 'Cannot get parameters from pkcs11.txt'); 71 | 72 | continue; 73 | } 74 | 75 | const provider: IConfigureProvider = { 76 | lib, 77 | slots: [1], 78 | libraryParameters: params[1], 79 | name: 'Firefox NSS', 80 | }; 81 | 82 | providers.push(provider); 83 | } 84 | 85 | return providers; 86 | }; 87 | 88 | export const firefoxProviders = { 89 | create, 90 | }; 91 | 92 | export default firefoxProviders; 93 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Application } from './application'; 3 | import container from './container'; 4 | 5 | const application = container.resolve(Application); 6 | 7 | application.start(); 8 | -------------------------------------------------------------------------------- /src/main/ipc_messages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ipcMain, 3 | shell, 4 | IpcMainEvent, 5 | BrowserWindow, 6 | nativeTheme, 7 | } from 'electron'; 8 | import { WindowsName } from '../shared'; 9 | import { APP_LOG_FILE } from './constants'; 10 | import { windowsController } from './windows'; 11 | import { l10n } from './l10n'; 12 | import logger, { loggingSwitch, loggingAnalyticsSwitch } from './logger'; 13 | import { ServerStorage } from './server_storage'; 14 | import { setConfig, getConfig } from './config'; 15 | import container from './container'; 16 | import { autoUpdater } from './updater'; 17 | 18 | const serverStorage = container.resolve(ServerStorage); 19 | 20 | export const sendToRenderers = (channel: string, data?: any) => { 21 | const browserWindows = BrowserWindow.getAllWindows(); 22 | 23 | browserWindows.forEach((window) => { 24 | if (window.webContents) { 25 | window.webContents.send(channel, data); 26 | } 27 | }); 28 | }; 29 | 30 | // TODO: Maybe move to application. 31 | const initServerEvents = () => { 32 | ipcMain 33 | .on('ipc-2key-list-get', async (event: IpcMainEvent) => { 34 | const identities = await serverStorage.getIdentities(); 35 | 36 | event.returnValue = identities; 37 | }) 38 | .on('ipc-identity-changed', () => { 39 | sendToRenderers('ipc-2key-changed'); 40 | }) 41 | .on('ipc-2key-remove', async (event: IpcMainEvent, arg: any) => { 42 | try { 43 | const questionWindowResult = await windowsController.showQuestionWindow({ 44 | text: l10n.get('question.2key.remove', arg), 45 | id: 'question.2key.remove', 46 | result: 0, 47 | }, windowsController.windows[WindowsName.Preferences].window); 48 | 49 | if (questionWindowResult.result) { 50 | logger.info('ipc-messages', 'Removing 2key session key', { 51 | arg, 52 | }); 53 | 54 | await serverStorage.removeIdentity(arg); 55 | 56 | event.sender.send('ipc-2key-changed'); 57 | } 58 | } catch { 59 | // 60 | } 61 | }); 62 | }; 63 | 64 | const initEvents = () => { 65 | ipcMain 66 | .on('ipc-logging-open', () => { 67 | shell.openPath(APP_LOG_FILE); 68 | }) 69 | .on('ipc-logging-status-get', (event: IpcMainEvent) => { 70 | const config = getConfig(); 71 | 72 | event.returnValue = config.logging; 73 | }) 74 | .on('ipc-logging-status-change', (event: IpcMainEvent) => { 75 | const config = getConfig(); 76 | const value = !config.logging; 77 | 78 | setConfig({ 79 | ...config, 80 | logging: value, 81 | }); 82 | 83 | loggingSwitch(value); 84 | 85 | logger.info('logging', 'Logging status changed', { 86 | value, 87 | }); 88 | 89 | event.sender.send('ipc-logging-status-changed', value); 90 | }) 91 | .on('ipc-language-set', (_: IpcMainEvent, lang: string) => { 92 | l10n.setLang(lang); 93 | 94 | sendToRenderers('ipc-language-changed', l10n.lang); 95 | }) 96 | .on('ipc-language-get', (event: IpcMainEvent) => { 97 | event.returnValue = { 98 | lang: l10n.lang, 99 | data: l10n.data, 100 | list: l10n.supportedLangs, 101 | }; 102 | }) 103 | .on('ipc-telemetry-status-get', (event: IpcMainEvent) => { 104 | const config = getConfig(); 105 | 106 | event.returnValue = config.telemetry; 107 | }) 108 | .on('ipc-telemetry-status-change', (event: IpcMainEvent) => { 109 | const config = getConfig(); 110 | const value = !config.telemetry; 111 | 112 | setConfig({ 113 | ...config, 114 | telemetry: value, 115 | }); 116 | 117 | loggingAnalyticsSwitch(value); 118 | 119 | logger.info('telemetry', 'Telemetry status changed', { 120 | value, 121 | }); 122 | 123 | event.sender.send('ipc-telemetry-status-changed', value); 124 | }) 125 | .on('ipc-theme-get', (event: IpcMainEvent) => { 126 | event.returnValue = nativeTheme.themeSource; 127 | }) 128 | .on('ipc-theme-set', (event: IpcMainEvent, theme: ('system' | 'dark' | 'light')) => { 129 | const config = getConfig(); 130 | 131 | setConfig({ 132 | ...config, 133 | theme, 134 | }); 135 | 136 | nativeTheme.themeSource = theme; 137 | event.sender.send('ipc-theme-changed', theme); 138 | }) 139 | .on('ipc-update-check', () => { 140 | autoUpdater.checkForUpdates(); 141 | }) 142 | .on('error', (event: IpcMainEvent) => { 143 | logger.error('ipc-messages', 'Event error', { 144 | event: event.toString(), 145 | }); 146 | }); 147 | }; 148 | 149 | export const ipcMessages = { 150 | initServerEvents, 151 | initEvents, 152 | }; 153 | -------------------------------------------------------------------------------- /src/main/jws.ts: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose-jwe-jws'; 2 | import logger from './logger'; 3 | import { crypto } from './crypto'; 4 | 5 | jose.setCrypto(crypto); 6 | 7 | async function getPublicKey() { 8 | const rawB64 = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArKAm6+BnWqcIleidaAiqM9O74cNvgE23mzVRe7KqVM/eodvhSBU1c+GYVt9a6guHFwoGPrbVoVQmDW+50xH7rEL+MaKT7lrlMvIt0dh+6UqDZaXybjoLc19al9ZJkvB9/icvxvG6ax0TGI2WKn78nVVmBtx+zeyIYE7DGYxR3eFJ8EVSMJHIT5Tsp8j/2UyzQXViuSzydwZuTPWAznpKGXVHwiD56QGLLvHpXO0Au3Hj36rTnOUyh3qdabRu6WQo2GySGNR7jui5upAK+1qGcKwXs3BOkhD0+g/M2wdQkvg/FDqtCxngboCiDPkUPlxK89XkiE4AotSerQzf3nGe2wIDAQAB'; 9 | const raw = Buffer.from(rawB64, 'base64'); 10 | const key = await crypto.subtle.importKey('spki', raw, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, true, ['verify']); 11 | const hash = await crypto.subtle.digest('SHA-256', raw); 12 | const kid = Buffer.from(hash).toString('base64'); 13 | 14 | return { 15 | kid, 16 | key, 17 | }; 18 | } 19 | 20 | export async function getContent(jws: string) { 21 | const joseKey = await getPublicKey(); 22 | 23 | let joseCrypto: IWebCryptographer; 24 | let verifier: IVerifier; 25 | let verifyRes: IVerificationResult[]; 26 | 27 | try { 28 | joseCrypto = new jose.Jose.WebCryptographer(); 29 | joseCrypto.setContentSignAlgorithm('RS256'); 30 | 31 | verifier = new jose.JoseJWS.Verifier(joseCrypto, jws.replace(/[\n\r]/g, '')); 32 | } catch (error) { 33 | const err = error instanceof Error ? error : new Error('Unknown error'); 34 | logger.error('jws', 'Malformed update metadata', { 35 | error: err.message, 36 | stack: err.stack, 37 | }); 38 | 39 | throw new Error('Unable to check JWS. Malformed update metadata.'); 40 | } 41 | 42 | await verifier.addRecipient(joseKey.key, joseKey.kid); 43 | 44 | try { 45 | verifyRes = await verifier.verify(); 46 | } catch (error) { 47 | const err = error instanceof Error ? error : new Error('Unknown error'); 48 | logger.error('jws', 'Cannot verify JWS signature', { 49 | error: err.message, 50 | stack: err.stack, 51 | }); 52 | 53 | throw new Error('Unable to check JWS. Cannot verify JWS signature.'); 54 | } 55 | 56 | if (verifyRes && verifyRes.length === 1 && verifyRes[0].verified) { 57 | const { payload } = verifyRes[0]; 58 | 59 | return JSON.parse(payload!); 60 | } 61 | 62 | throw new Error('Unable to check JWS. Invalid signature on metadata.'); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/l10n.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import logger from './logger'; 5 | import { Assoc } from './types'; 6 | import { printf } from './utils'; 7 | 8 | const LANG_DIR = path.join(__dirname, typeof navigator === 'undefined' ? '' : '..', '..', 'locale'); 9 | 10 | class Localization extends EventEmitter { 11 | readonly defaultLang = 'en'; 12 | 13 | supportedLangs: string[]; 14 | 15 | lang: string; 16 | 17 | data: Assoc; 18 | 19 | constructor() { 20 | super(); 21 | 22 | this.lang = this.defaultLang; 23 | this.data = {}; 24 | this.supportedLangs = this.getLangList(); 25 | } 26 | 27 | public on(event: 'locale-change', cb: (locale: string) => void): this; 28 | 29 | public on(event: string, cb: (...args: any[]) => void) { 30 | return super.on(event, cb); 31 | } 32 | 33 | public emit(event: 'locale-change', lang: string): boolean; 34 | 35 | public emit(event: string, ...args: any[]) { 36 | return super.emit(event, ...args); 37 | } 38 | 39 | public get(key: string, ...args: any[]): string { 40 | const text = this.data[key]; 41 | 42 | return text ? printf(text, args) : `{${key}}`; 43 | } 44 | 45 | // eslint-disable-next-line class-methods-use-this 46 | private getLangList() { 47 | if (!fs.existsSync(LANG_DIR)) { 48 | throw new Error(`Cannot read ${LANG_DIR}. Folder doesn't exist`); 49 | } 50 | 51 | const items = fs.readdirSync(LANG_DIR); 52 | const langList: string[] = []; 53 | // eslint-disable-next-line 54 | for (const item of items) { 55 | const itemPath = path.join(LANG_DIR, item); 56 | const itemStat = fs.statSync(itemPath); 57 | 58 | if (itemStat.isFile()) { 59 | const parts = /(\w+)\.json/.exec(item); 60 | 61 | if (parts) { 62 | langList.push(parts[1]); 63 | } 64 | } 65 | } 66 | 67 | return langList; 68 | } 69 | 70 | public setLang(lang: string) { 71 | if (!this.supportedLangs.includes(lang)) { 72 | return; 73 | } 74 | 75 | logger.info('l10n', 'Change language', { 76 | lang, 77 | }); 78 | 79 | const data = this.loadLang(lang); 80 | 81 | this.lang = lang; 82 | this.data = data; 83 | 84 | this.emit('locale-change', lang); 85 | } 86 | 87 | // eslint-disable-next-line class-methods-use-this 88 | private loadLang(lang: string) { 89 | const localePath = path.join(LANG_DIR, `${lang}.json`); 90 | 91 | if (!fs.existsSync(localePath)) { 92 | throw new Error(`Cannot load ${localePath}. File does not exist`); 93 | } 94 | 95 | const json = fs.readFileSync(localePath, { encoding: 'utf8' }); 96 | const data = JSON.parse(json); 97 | 98 | return data; 99 | } 100 | } 101 | 102 | export const l10n = new Localization(); 103 | -------------------------------------------------------------------------------- /src/main/logger/analytics.ts: -------------------------------------------------------------------------------- 1 | import * as Mixpanel from 'mixpanel'; 2 | import * as publicIp from 'public-ip'; 3 | import { MIXPANEL_TOKEN } from '../constants'; 4 | 5 | const mixpanel = Mixpanel.init(MIXPANEL_TOKEN); 6 | 7 | export interface IAnalyticsOptions { 8 | userId: string; 9 | } 10 | 11 | /** 12 | * https://www.npmjs.com/package/mixpanel 13 | */ 14 | 15 | export class Analytics { 16 | private userId: string; 17 | 18 | private userIp!: string; 19 | 20 | constructor(options: IAnalyticsOptions) { 21 | this.userId = options.userId; 22 | 23 | this.initIp(); 24 | } 25 | 26 | private async initIp() { 27 | try { 28 | this.userIp = await publicIp.v4(); 29 | } catch { 30 | // 31 | } 32 | } 33 | 34 | event(category: string, action: string, params: Record) { 35 | mixpanel.track(action, { 36 | distinct_id: this.userId, 37 | ip: this.userIp, 38 | category, 39 | ...params, 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/logger/analytics_transport.ts: -------------------------------------------------------------------------------- 1 | import * as Transport from 'winston-transport'; 2 | import { Analytics, IAnalyticsOptions } from './analytics'; 3 | 4 | export class AnalyticsTransport extends Transport { 5 | analytics: Analytics; 6 | 7 | constructor(options: IAnalyticsOptions & Transport.TransportStreamOptions) { 8 | super(options); 9 | 10 | this.analytics = new Analytics(options); 11 | } 12 | 13 | log(info: any, callback: () => void) { 14 | const { 15 | level, 16 | source, 17 | message, 18 | ...other 19 | } = info; 20 | 21 | this.analytics.event(source, message, other); 22 | 23 | this.emit('logged', info); 24 | 25 | if (callback) { 26 | callback(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/logger/index.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston'; 2 | import { AnalyticsTransport } from './analytics_transport'; 3 | import { APP_LOG_FILE, isDevelopment } from '../constants'; 4 | import { getConfig } from '../config'; 5 | 6 | const config = getConfig(); 7 | 8 | const transportConsole = new winston.transports.Console({ 9 | format: winston.format.combine( 10 | winston.format.colorize(), 11 | winston.format.printf((info) => { 12 | const { 13 | level, 14 | message, 15 | source, 16 | ...other 17 | } = info; 18 | 19 | if (other && Object.keys(other).length) { 20 | return `${level}: [${source}] ${message} ${JSON.stringify(other)}`; 21 | } 22 | 23 | return `${level}: [${source}] ${message}`; 24 | }), 25 | ), 26 | }); 27 | 28 | const transportFile = new winston.transports.File({ 29 | filename: APP_LOG_FILE, 30 | format: winston.format.combine( 31 | winston.format.timestamp(), 32 | winston.format.json(), 33 | ), 34 | }); 35 | 36 | const transportAnalytics = new AnalyticsTransport({ 37 | userId: config.userId, 38 | }); 39 | 40 | const winstonlogger = winston.createLogger({ 41 | exitOnError: false, 42 | transports: [ 43 | transportConsole, 44 | transportFile, 45 | transportAnalytics, 46 | ], 47 | }); 48 | 49 | export const loggingSwitch = (enabled: boolean) => { 50 | if (isDevelopment) { 51 | transportConsole.silent = false; 52 | transportFile.silent = false; 53 | 54 | return; 55 | } 56 | 57 | if (enabled) { 58 | transportConsole.silent = false; 59 | transportFile.silent = false; 60 | } else { 61 | transportConsole.silent = true; 62 | transportFile.silent = true; 63 | } 64 | }; 65 | 66 | export const loggingAnalyticsSwitch = (enabled: boolean) => { 67 | if (isDevelopment) { 68 | transportAnalytics.silent = true; 69 | 70 | return; 71 | } 72 | 73 | if (enabled) { 74 | transportAnalytics.silent = false; 75 | } else { 76 | transportAnalytics.silent = true; 77 | } 78 | }; 79 | 80 | export default { 81 | log: (level: string, source: string, message: string, params: object = {}) => { 82 | winstonlogger.log(level, message, { 83 | source, 84 | ...params, 85 | }); 86 | }, 87 | info: (source: string, message: string, params: object = {}) => { 88 | winstonlogger.info(message, { 89 | source, 90 | ...params, 91 | }); 92 | }, 93 | error: (source: string, message: string, params: object = {}) => { 94 | winstonlogger.error(message, { 95 | source, 96 | ...params, 97 | }); 98 | }, 99 | warn: (source: string, message: string, params: object = {}) => { 100 | winstonlogger.warn(message, { 101 | source, 102 | ...params, 103 | }); 104 | }, 105 | }; 106 | -------------------------------------------------------------------------------- /src/main/server_storage.ts: -------------------------------------------------------------------------------- 1 | import * as wsServer from '@webcrypto-local/server'; 2 | import { 3 | inject, 4 | injectable, 5 | } from 'tsyringe'; 6 | import { Server } from './server'; 7 | 8 | interface Identity { 9 | browser: string; 10 | userAgent: string; 11 | created: Date; 12 | id: string; 13 | origin: string | 'edge' | 'ie' | 'chrome' | 'safari' | 'firefox' | 'other'; 14 | } 15 | 16 | interface CurrentIdentity { 17 | origin: string | null; 18 | created: Date | null; 19 | browsers: string[]; 20 | } 21 | 22 | /** 23 | * 24 | * @param {WebCryptoLocal.RemoteIdentityEx} identity 25 | */ 26 | function PrepareIdentity(identity: wsServer.RemoteIdentity) { 27 | const userAgent = identity.userAgent!; 28 | const res: Identity = {} as any; 29 | 30 | if (/edge\/([\d.]+)/i.exec(userAgent)) { 31 | res.browser = 'edge'; 32 | } else if (/msie/i.test(userAgent)) { 33 | res.browser = 'ie'; 34 | } else if (/Trident/i.test(userAgent)) { 35 | res.browser = 'ie'; 36 | } else if (/chrome/i.test(userAgent)) { 37 | res.browser = 'chrome'; 38 | } else if (/safari/i.test(userAgent)) { 39 | res.browser = 'safari'; 40 | } else if (/firefox/i.test(userAgent)) { 41 | res.browser = 'firefox'; 42 | } else { 43 | res.browser = 'Other'; 44 | } 45 | 46 | res.created = identity.createdAt; 47 | res.origin = identity.origin!; 48 | 49 | return res; 50 | } 51 | 52 | @injectable() 53 | export class ServerStorage { 54 | constructor( 55 | @inject('server') public server: Server, 56 | ) {} 57 | 58 | async getIdentities() { 59 | const storage = this.server.server.server.storage as wsServer.FileStorage; 60 | 61 | if (!Object.keys(storage.remoteIdentities).length) { 62 | // NOTE: call protected method of the storage 63 | // @ts-ignore 64 | await storage.loadRemote(); 65 | } 66 | 67 | const identities = storage.remoteIdentities; 68 | const preparedList = []; 69 | 70 | for (const i in identities) { 71 | const identity = PrepareIdentity(identities[i]); 72 | 73 | preparedList.push(identity); 74 | } 75 | 76 | // sort identities 77 | preparedList.sort((a, b) => { 78 | if (a.origin > b.origin) { 79 | return 1; 80 | } if (a.origin < b.origin) { 81 | return -1; 82 | } 83 | if (a.browser > b.browser) { 84 | return 1; 85 | } if (a.browser < b.browser) { 86 | return -1; 87 | } 88 | 89 | return 0; 90 | }); 91 | 92 | // prepare data 93 | const res: CurrentIdentity[] = []; 94 | let currentIdentity: CurrentIdentity = { 95 | origin: null, 96 | created: null, 97 | browsers: [], 98 | }; 99 | 100 | preparedList.forEach((identity) => { 101 | if (currentIdentity.origin !== identity.origin) { 102 | if (currentIdentity.origin !== null) { 103 | res.push(currentIdentity); 104 | } 105 | 106 | currentIdentity = { 107 | origin: identity.origin, 108 | created: identity.created, 109 | browsers: [identity.browser], 110 | }; 111 | } else { 112 | if (currentIdentity.created! > identity.created) { 113 | currentIdentity.created = identity.created; 114 | } 115 | 116 | if (!currentIdentity.browsers.some((browser) => browser === identity.browser)) { 117 | currentIdentity.browsers.push(identity.browser); 118 | } 119 | } 120 | }); 121 | 122 | if (currentIdentity.origin !== null) { 123 | res.push(currentIdentity); 124 | } 125 | 126 | return res; 127 | } 128 | 129 | async removeIdentity(origin: string) { 130 | const storage = this.server.server.server.storage as wsServer.FileStorage; 131 | const remList = []; 132 | 133 | for (const i in storage.remoteIdentities) { 134 | const identity = storage.remoteIdentities[i]; 135 | 136 | if (identity.origin === origin) { 137 | remList.push(i); 138 | } 139 | } 140 | 141 | remList.forEach((item) => { 142 | delete storage.remoteIdentities[item]; 143 | }); 144 | 145 | await storage.removeRemoteIdentity(origin); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ssl'; 2 | -------------------------------------------------------------------------------- /src/main/services/ssl/firefox.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | import { execSync } from 'child_process'; 5 | import logger from '../../logger'; 6 | 7 | export class Firefox { 8 | public static profiles() { 9 | const homeFolder = os.homedir(); 10 | let profilesFolder: string | undefined; 11 | switch (os.platform()) { 12 | case 'win32': 13 | profilesFolder = path.normalize(`${homeFolder}/AppData/Roaming/Mozilla/Firefox/Profiles`); 14 | break; 15 | case 'linux': 16 | profilesFolder = path.normalize(`${homeFolder}/.mozilla/firefox`); 17 | break; 18 | case 'darwin': 19 | profilesFolder = path.normalize(`${homeFolder}/Library/Application Support/Firefox/Profiles`); 20 | break; 21 | default: 22 | throw new Error('Cannot get Firefox profile. Unsupported Operation System'); 23 | } 24 | 25 | const res: string[] = []; 26 | if (fs.existsSync(profilesFolder)) { 27 | const profiles = fs.readdirSync(profilesFolder); 28 | // eslint-disable-next-line no-restricted-syntax 29 | for (const profile of profiles) { 30 | const profileFolder = path.normalize(path.join(profilesFolder, profile)); 31 | res.push(profileFolder); 32 | } 33 | } else { 34 | logger.info('firefox', 'Profiles folder does not exist', { path: profilesFolder }); 35 | } 36 | 37 | return res; 38 | } 39 | 40 | public static restart() { 41 | try { 42 | switch (os.platform()) { 43 | case 'win32': 44 | execSync('taskkill /F /IM firefox.exe'); 45 | execSync('start firefox'); 46 | break; 47 | case 'linux': 48 | execSync('pkill firefox'); 49 | execSync('firefox&'); 50 | break; 51 | case 'darwin': 52 | execSync('pkill firefox'); 53 | execSync('open /Applications/Firefox.app'); 54 | break; 55 | default: 56 | // nothing 57 | } 58 | } catch (e) { 59 | logger.warn('firefox', 'The error has occurred during the Firefox browser restarting', { error: `${e}` }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/services/ssl/generator.ts: -------------------------------------------------------------------------------- 1 | import * as x509 from '@peculiar/asn1-x509'; 2 | import * as asn from '@peculiar/asn1-schema'; 3 | import { Convert } from 'pvtsutils'; 4 | import * as core from 'webcrypto-core'; 5 | import * as pkijs from 'pkijs'; 6 | 7 | export interface IName { 8 | commonName: string, 9 | organization?: string; 10 | } 11 | 12 | export type ValidityType = 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second'; 13 | 14 | export interface IValidity { 15 | value: number; 16 | type: ValidityType; 17 | } 18 | 19 | export interface ISubjectAlternativeName { 20 | dns?: string[], 21 | } 22 | 23 | export interface ICertificate { 24 | cert: string, 25 | publicKey: CryptoKey; 26 | privateKey?: CryptoKey; 27 | } 28 | 29 | export interface ICertificateGeneratorCreateParams { 30 | /** 31 | * Serial number. Default is 1 32 | */ 33 | serialNumber?: number, 34 | publicKey: CryptoKey; 35 | signingKey: CryptoKey; 36 | hashAlg?: string; 37 | subject: IName; 38 | issuer?: IName; 39 | validity: IValidity; 40 | extensions?: x509.Extension[]; 41 | } 42 | 43 | export class CertificateGenerator { 44 | public static readonly SECOND = 1; 45 | 46 | public static readonly MINUTE = CertificateGenerator.SECOND * 60; 47 | 48 | public static readonly HOUR = CertificateGenerator.MINUTE * 60; 49 | 50 | public static readonly DAY = CertificateGenerator.HOUR * 24; 51 | 52 | public static readonly WEEK = CertificateGenerator.DAY * 7; 53 | 54 | public static readonly MONTH = CertificateGenerator.DAY * 30; 55 | 56 | public static readonly YEAR = CertificateGenerator.MONTH * 12; 57 | 58 | public static readonly HASH = 'SHA-256'; 59 | 60 | /** 61 | * Returns RDN object 62 | * @param name Name params 63 | */ 64 | private static createName(name: IName) { 65 | const res = new x509.Name(); 66 | 67 | res.push(new x509.RelativeDistinguishedName([ 68 | new x509.AttributeTypeAndValue({ 69 | type: '2.5.4.3', // Common name 70 | value: new x509.AttributeValue({ printableString: name.commonName }), 71 | }), 72 | ])); 73 | if (name.organization) { 74 | res.push(new x509.RelativeDistinguishedName([ 75 | new x509.AttributeTypeAndValue({ 76 | type: '2.5.4.10', // Organization 77 | value: new x509.AttributeValue({ printableString: name.organization }), 78 | }), 79 | ])); 80 | } 81 | 82 | return res; 83 | } 84 | 85 | /** 86 | * Returns a validity period in seconds 87 | */ 88 | private static getSeconds(validity: IValidity) { 89 | switch (validity.type) { 90 | case 'second': 91 | return this.SECOND * validity.value; 92 | case 'minute': 93 | return this.MINUTE * validity.value; 94 | case 'hour': 95 | return this.HOUR * validity.value; 96 | case 'day': 97 | return this.DAY * validity.value; 98 | case 'week': 99 | return this.WEEK * validity.value; 100 | case 'month': 101 | return this.MONTH * validity.value; 102 | case 'year': 103 | return this.YEAR * validity.value; 104 | default: 105 | throw new Error('Unsupported validity type in use'); 106 | } 107 | } 108 | 109 | /** 110 | * Returns a random serial number 111 | */ 112 | public static randomSerial() { 113 | return Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)); 114 | } 115 | 116 | /** 117 | * Creates X509 certificate 118 | * @param params Parameters 119 | */ 120 | public static async create(params: ICertificateGeneratorCreateParams) { 121 | pkijs.setEngine('OpenSSL', crypto, new pkijs.CryptoEngine({ name: 'OpenSSL', crypto, subtle: crypto.subtle })); 122 | 123 | const cert = new x509.Certificate(); 124 | 125 | // region Put a static values 126 | const tbs = cert.tbsCertificate; 127 | tbs.version = x509.Version.v3; 128 | tbs.serialNumber = params.serialNumber 129 | ? Convert.FromHex(params.serialNumber.toString(16)) 130 | : new Uint8Array([1]).buffer; 131 | 132 | tbs.subject = this.createName(params.subject); 133 | tbs.issuer = this.createName(params.issuer || params.subject); 134 | 135 | // Valid period 136 | tbs.validity.notBefore.utcTime = new Date(); // current date 137 | const notAfter = new Date(); 138 | notAfter.setSeconds(notAfter.getSeconds() + this.getSeconds(params.validity)); 139 | tbs.validity.notAfter.utcTime = notAfter; 140 | 141 | // Extensions are not a part of certificate by default, it's an optional array 142 | tbs.extensions = new x509.Extensions(params.extensions); 143 | 144 | const pkiCert = new pkijs.Certificate({ schema: asn.AsnSerializer.toASN(cert) }); 145 | await pkiCert.subjectPublicKeyInfo.importKey(params.publicKey); 146 | const hashName = params.hashAlg 147 | || (params.signingKey.algorithm as RsaHashedKeyAlgorithm).hash.name 148 | || this.HASH; 149 | await pkiCert.sign(params.signingKey, hashName); 150 | 151 | const pem = core.PemConverter.fromBufferSource(pkiCert.toSchema().toBER(false), 'CERTIFICATE'); 152 | 153 | return { 154 | cert: pem, 155 | publicKey: params.publicKey, 156 | } as ICertificate; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/services/ssl/installer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-continue */ 2 | /* eslint-disable class-methods-use-this */ 3 | /* eslint-disable no-restricted-syntax */ 4 | 5 | import * as fs from 'fs'; 6 | import * as os from 'os'; 7 | import * as path from 'path'; 8 | import * as childProcess from 'child_process'; 9 | import { PemConverter } from 'webcrypto-core'; 10 | import { Firefox } from './firefox'; 11 | import { NssCertUtils } from './nss'; 12 | import { SRC_DIR } from '../../constants'; 13 | import logger from '../../logger'; 14 | 15 | export interface ISslCertInstallerPolicy { 16 | /** 17 | * Path to NSS certutil application 18 | */ 19 | nssCertUtil: string; 20 | /** 21 | * NSS certificate name in data base storage 22 | */ 23 | nssCertName: string; 24 | /** 25 | * App name for sudo dialog 26 | * 27 | * Default value is 'Fortify application' 28 | */ 29 | osxAppName?: string; 30 | /** 31 | * Path to icon for sudo dialog. File extension should be icns. 32 | * 33 | * Default path is '/Applications/Fortify.app/Contents/Resources/static/icons/tray/mac/icon.icns' 34 | */ 35 | osxAppIcons?: string; 36 | } 37 | 38 | export class SslCertInstaller { 39 | public constructor(public policy: ISslCertInstallerPolicy) { } 40 | 41 | /** 42 | * Installs CA certificate to trusted storages (System, Firefox, Chrome, etc) 43 | * @param certPath Path to CA PEM file 44 | */ 45 | public async install(certPath: string) { 46 | const platform = os.platform(); 47 | 48 | switch (platform) { 49 | case 'linux': 50 | this.installLinux(certPath); 51 | break; 52 | case 'darwin': 53 | await this.installDarwin(certPath); 54 | break; 55 | case 'win32': 56 | // MSI installer adds SSL certificate to Root storage 57 | break; 58 | default: 59 | throw new Error('Unsupported Operation System'); 60 | } 61 | 62 | this.installFirefox(certPath); 63 | } 64 | 65 | private installLinux(cert: string) { 66 | // Add cert to Chrome storage 67 | const USER_HOME = os.homedir(); 68 | const CHROME_DIR = path.normalize(`${USER_HOME}/.pki/nssdb`); 69 | const certName = this.policy.nssCertName; 70 | const nss = new NssCertUtils(this.policy.nssCertUtil, `sql:${CHROME_DIR}`); 71 | 72 | if (nss.exists(certName)) { 73 | // Remove a prev SSL certificate 74 | const pem = nss.get(certName); 75 | nss.remove(certName); 76 | 77 | logger.info('ssl-installer', 'SSL certificate removed from Chrome profile', { 78 | profile: CHROME_DIR, 79 | certName, 80 | pem, 81 | }); 82 | } 83 | 84 | nss.add(cert, certName, 'CT,c,'); 85 | 86 | logger.info('ssl-installer', 'SSL certificate added to Chrome profile', { 87 | profile: CHROME_DIR, 88 | certName, 89 | }); 90 | } 91 | 92 | private async installDarwin(certPath: string) { 93 | await new Promise((resolve, reject) => { 94 | const certName = this.policy.nssCertName; 95 | const { username } = os.userInfo(); 96 | 97 | logger.info('ssl-installer', 'Adding CA certificate to System KeyChain'); 98 | 99 | childProcess.exec(`certPath="${certPath}" certName="${certName}" userDir="${os.homedir()}" USER="${username}" bash ${SRC_DIR}/resources/osx-ssl.sh`, (err) => { 100 | if (err) { 101 | reject(err); 102 | } else { 103 | resolve(); 104 | } 105 | }); 106 | 107 | logger.info('ssl-installer', 'SSL certificate added to User KeyChain', { 108 | certName, 109 | }); 110 | }); 111 | } 112 | 113 | public installFirefox(certPath: string) { 114 | const certName = this.policy.nssCertName; 115 | const certUtil = this.policy.nssCertUtil; 116 | const caPem = fs.readFileSync(certPath, { encoding: 'utf8' }); 117 | const caDer = PemConverter.toArrayBuffer(caPem); 118 | const profiles = Firefox.profiles(); 119 | let installed = false; 120 | 121 | for (const profile of profiles) { 122 | try { 123 | const nss = new NssCertUtils(certUtil, `sql:${profile}`); 124 | 125 | if (nss.exists(certName, caDer)) { 126 | continue; 127 | } 128 | 129 | if (nss.exists(certName)) { 130 | // Remove a prev SSL certificate 131 | const pem = nss.get(certName); 132 | 133 | nss.remove(certName); 134 | 135 | logger.info('ssl-installer', 'SSL certificate removed from Mozilla Firefox profile', { 136 | profile, 137 | certName, 138 | pem, 139 | }); 140 | } 141 | // Add cert to NSS 142 | nss.add(certPath, certName, 'CT,c,'); 143 | 144 | logger.info('ssl-installer', 'SSL certificate added to Mozilla Firefox profile', { 145 | profile, 146 | certName, 147 | }); 148 | installed = true; 149 | } catch (error) { 150 | const err = error instanceof Error ? error : new Error('Unknown error'); 151 | logger.error('ssl-installer', 'SSL install error', { 152 | error: err.message, 153 | stack: err.stack, 154 | }); 155 | } 156 | } 157 | 158 | if (profiles.length && installed) { 159 | Firefox.restart(); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/services/ssl/pem_converter.ts: -------------------------------------------------------------------------------- 1 | import { Convert } from 'pvtsutils'; 2 | 3 | /** 4 | * Represents PEM Converter. 5 | */ 6 | export class PemConverter { 7 | public CertificateTag = 'CERTIFICATE'; 8 | 9 | public CertificateRequestTag = 'CERTIFICATE REQUEST'; 10 | 11 | public PublicKeyTag = 'PUBLIC KEY'; 12 | 13 | public PrivateKeyTag = 'PRIVATE KEY'; 14 | 15 | static isPem(data: any): data is string { 16 | return typeof data === 'string' 17 | && /-{5}BEGIN [A-Z0-9 ]+-{5}([a-zA-Z0-9=+/\n\r]+)-{5}END [A-Z0-9 ]+-{5}/g.test(data); 18 | } 19 | 20 | /** 21 | * Decodes PEM to a list of raws 22 | * @param pem message in PEM format 23 | */ 24 | public static decode(pem: string) { 25 | const pattern = /-{5}BEGIN [A-Z0-9 ]+-{5}([a-zA-Z0-9=+/\n\r]+)-{5}END [A-Z0-9 ]+-{5}/g; 26 | 27 | const res: ArrayBuffer[] = []; 28 | let matches: RegExpExecArray | null = null; 29 | // eslint-disable-next-line no-cond-assign 30 | while (matches = pattern.exec(pem)) { 31 | const base64 = matches[1] 32 | .replace(/\r/g, '') 33 | .replace(/\n/g, ''); 34 | res.push(Convert.FromBase64(base64)); 35 | } 36 | 37 | return res; 38 | } 39 | 40 | /** 41 | * Encodes a raw data to PEM format 42 | * @param rawData Raw data 43 | * @param tag PEM tag 44 | */ 45 | public static encode(rawData: BufferSource, tag: string): string; 46 | 47 | /** 48 | * Encodes a list of raws to PEM format 49 | * @param raws A list of raws 50 | * @param tag PEM tag 51 | */ 52 | public static encode(rawData: BufferSource[], tag: string): string; 53 | 54 | public static encode(rawData: BufferSource | BufferSource[], tag: string) { 55 | if (Array.isArray(rawData)) { 56 | const raws = new Array(); 57 | rawData.forEach((element) => { 58 | raws.push(this.encodeBuffer(element, tag)); 59 | }); 60 | 61 | return raws.join('\n'); 62 | } 63 | 64 | return this.encodeBuffer(rawData, tag); 65 | } 66 | 67 | /** 68 | * Encodes a raw data to PEM format 69 | * @param rawData Raw data 70 | * @param tag PEM tag 71 | */ 72 | private static encodeBuffer(rawData: BufferSource, tag: string) { 73 | const base64 = Convert.ToBase64(rawData); 74 | let sliced: string; 75 | let offset = 0; 76 | const rows = Array(); 77 | while (offset < base64.length) { 78 | if (base64.length - offset < 64) { 79 | sliced = base64.substring(offset); 80 | } else { 81 | sliced = base64.substring(offset, offset + 64); 82 | offset += 64; 83 | } 84 | if (sliced.length !== 0) { 85 | rows.push(sliced); 86 | if (sliced.length < 64) { 87 | break; 88 | } 89 | } else { 90 | break; 91 | } 92 | } 93 | const upperCaseTag = tag.toLocaleUpperCase(); 94 | 95 | return `-----BEGIN ${upperCaseTag}-----\n${rows.join('\n')}\n-----END ${upperCaseTag}-----`; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/tray/base_template.ts: -------------------------------------------------------------------------------- 1 | import { 2 | shell, 3 | MenuItemConstructorOptions, 4 | } from 'electron'; 5 | import { WindowPreferencesName } from '../../shared'; 6 | import { TOOLS_LINK } from '../constants'; 7 | import { windowsController } from '../windows'; 8 | import { autoUpdater } from '../updater'; 9 | import { l10n } from '../l10n'; 10 | 11 | export const baseTemplate = (): MenuItemConstructorOptions[] => ([ 12 | { 13 | label: l10n.get('about.app'), 14 | click: () => { 15 | windowsController.showPreferencesWindow(WindowPreferencesName.About); 16 | }, 17 | }, 18 | { 19 | label: l10n.get('updates.check'), 20 | click: () => { 21 | windowsController.showPreferencesWindow(WindowPreferencesName.Updates); 22 | autoUpdater.checkForUpdates(); 23 | }, 24 | }, 25 | { 26 | type: 'separator', 27 | }, 28 | { 29 | label: l10n.get('preferences'), 30 | click: () => { 31 | windowsController.showPreferencesWindow(WindowPreferencesName.Settings); 32 | }, 33 | }, 34 | { 35 | label: l10n.get('tools'), 36 | click: () => { 37 | shell.openExternal(TOOLS_LINK); 38 | }, 39 | }, 40 | { 41 | type: 'separator', 42 | }, 43 | { 44 | label: l10n.get('quit'), 45 | role: 'quit', 46 | }, 47 | ]); 48 | -------------------------------------------------------------------------------- /src/main/tray/development_template.ts: -------------------------------------------------------------------------------- 1 | import { MenuItemConstructorOptions } from 'electron'; 2 | import { l10n } from '../l10n'; 3 | import { windowsController } from '../windows'; 4 | 5 | export const developmentTemplate = (): MenuItemConstructorOptions[] => ([ 6 | { 7 | type: 'separator', 8 | }, 9 | { 10 | label: 'Develop', 11 | submenu: [ 12 | { 13 | label: 'Token new', 14 | click: () => { 15 | windowsController.showTokenWindow(); 16 | }, 17 | }, 18 | { 19 | type: 'separator', 20 | }, 21 | { 22 | label: 'Error SSL install', 23 | click: () => { 24 | windowsController.showErrorWindow( 25 | { 26 | text: l10n.get('error.ssl.install'), 27 | }, 28 | ); 29 | }, 30 | }, 31 | { 32 | type: 'separator', 33 | }, 34 | { 35 | label: 'Question 2key remove', 36 | click: () => { 37 | windowsController.showQuestionWindow( 38 | { 39 | text: l10n.get('question.2key.remove', 'TEST'), 40 | id: 'question.2key.remove', 41 | result: 0, 42 | }, 43 | ); 44 | }, 45 | }, 46 | { 47 | type: 'separator', 48 | }, 49 | { 50 | label: 'Warning SSL install', 51 | click: () => { 52 | windowsController.showWarningWindow( 53 | { 54 | text: l10n.get('warn.ssl.install'), 55 | buttonRejectLabel: 'i_understand', 56 | id: 'ssl.install', 57 | }, 58 | ); 59 | }, 60 | }, 61 | { 62 | label: 'Warning cannot start', 63 | click: () => { 64 | windowsController.showWarningWindow( 65 | { 66 | text: l10n.get('warn.pcsc.cannot_start'), 67 | title: 'warning.title.oh_no', 68 | buttonRejectLabel: 'i_understand', 69 | id: 'warn.pcsc.cannot_start', 70 | showAgain: true, 71 | showAgainValue: false, 72 | }, 73 | ); 74 | }, 75 | }, 76 | { 77 | label: 'Warning crypto not found', 78 | click: () => { 79 | windowsController.showWarningWindow( 80 | { 81 | text: l10n.get('warn.token.crypto_not_found', 'TEST'), 82 | title: 'warning.title.oh_no', 83 | buttonRejectLabel: 'close', 84 | id: 'warn.token.crypto_not_found', 85 | showAgain: true, 86 | showAgainValue: false, 87 | }, 88 | ); 89 | }, 90 | }, 91 | { 92 | label: 'Warning crypto wrong', 93 | click: () => { 94 | windowsController.showWarningWindow( 95 | { 96 | text: l10n.get('warn.token.crypto_wrong', 'TEST'), 97 | title: 'warning.title.oh_no', 98 | buttonRejectLabel: 'close', 99 | id: 'warn.token.crypto_wrong', 100 | showAgain: true, 101 | showAgainValue: false, 102 | }, 103 | ); 104 | }, 105 | }, 106 | { 107 | type: 'separator', 108 | }, 109 | { 110 | label: 'Key PIN', 111 | click: () => { 112 | windowsController.showKeyPinWindow( 113 | { 114 | pin: '123456', 115 | origin: 'https://TEST.com/', 116 | accept: true, 117 | }, 118 | 'test' 119 | ); 120 | }, 121 | }, 122 | { 123 | label: 'P11 PIN', 124 | click: () => { 125 | windowsController.showP11PinWindow( 126 | { 127 | pin: '', 128 | origin: 'https://TEST.com/', 129 | }, 130 | 'test' 131 | ); 132 | }, 133 | }, 134 | ], 135 | }, 136 | ]); 137 | -------------------------------------------------------------------------------- /src/main/tray/index.ts: -------------------------------------------------------------------------------- 1 | import { Menu, Tray } from 'electron'; 2 | import { isDevelopment, icons } from '../constants'; 3 | import { baseTemplate } from './base_template'; 4 | import { developmentTemplate } from './development_template'; 5 | 6 | let trayElectron: Electron.Tray; 7 | 8 | const getTemplate = () => ( 9 | baseTemplate().concat(isDevelopment ? developmentTemplate() : []) 10 | ); 11 | 12 | const setIcon = (hasNotifications?: boolean) => { 13 | const icon = hasNotifications ? icons.trayNotification : icons.tray; 14 | 15 | trayElectron.setImage(icon); 16 | }; 17 | 18 | const create = () => { 19 | if (!trayElectron) { 20 | trayElectron = new Tray(icons.tray); 21 | } 22 | 23 | const menu = Menu.buildFromTemplate(getTemplate()); 24 | 25 | trayElectron.setToolTip('Fortify'); 26 | trayElectron.setContextMenu(menu); 27 | }; 28 | 29 | const refresh = () => { 30 | create(); 31 | }; 32 | 33 | export const tray = { 34 | create, 35 | refresh, 36 | setIcon, 37 | }; 38 | -------------------------------------------------------------------------------- /src/main/types.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@webcrypto-local/cards'; 2 | 3 | export type Assoc = Record; 4 | 5 | export interface IConfigureProvider { 6 | lib: string; 7 | slots?: number[]; 8 | libraryParameters?: string; 9 | readWrite?: boolean; 10 | /** 11 | * Name of the provider 12 | */ 13 | name?: string; 14 | } 15 | 16 | export interface ICard { 17 | reader?: string; 18 | name: string; 19 | atr: Buffer; 20 | mask?: Buffer; 21 | readOnly: boolean; 22 | libraries: string[]; 23 | config?: Config; 24 | } 25 | 26 | export interface IConfigure { 27 | logging?: boolean; 28 | locale?: string; 29 | disableCardUpdate?: boolean; 30 | cardConfigPath?: string; 31 | providers?: IConfigureProvider[]; 32 | cards: ICard[]; 33 | userId: string; 34 | telemetry?: boolean; 35 | theme: ('system' | 'dark' | 'light'); 36 | } 37 | 38 | export interface ICardJson { 39 | reader?: string; 40 | name: string; 41 | atr: string; 42 | mask?: string; 43 | readOnly?: boolean; 44 | libraries: string[]; 45 | config?: Config; 46 | } 47 | 48 | export interface IConfigureJson { 49 | logging?: boolean; 50 | locale?: string; 51 | disableCardUpdate?: boolean; 52 | cardConfigPath?: string; 53 | providers?: IConfigureProvider[]; 54 | cards: ICardJson[]; 55 | userId: string; 56 | telemetry?: boolean; 57 | theme: ('system' | 'dark' | 'light'); 58 | } 59 | -------------------------------------------------------------------------------- /src/main/updater.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import * as path from 'path'; 3 | import * as semver from 'semver'; 4 | import * as fs from 'fs'; 5 | import { request } from './utils'; 6 | import { JWS_LINK, APP_DIR } from './constants'; 7 | import logger from './logger'; 8 | import * as jws from './jws'; 9 | import { UpdateError } from './errors'; 10 | 11 | type UpdateInfo = { 12 | version: string; 13 | createdAt: number; 14 | min?: string; 15 | }; 16 | 17 | class Updater extends EventEmitter { 18 | on(event: 'update-available', cb: (info: UpdateInfo) => void): this; 19 | 20 | on(event: 'update-not-available', cb: () => void): this; 21 | 22 | on(event: 'checking-for-update', cb: () => void): this; 23 | 24 | on(event: 'error', cb: (error: UpdateError) => void): this; 25 | 26 | on(event: string, cb: (...args: any[]) => void) { 27 | return super.on(event, cb); 28 | } 29 | 30 | emit(event: 'update-available', info: UpdateInfo): boolean; 31 | 32 | emit(event: 'update-not-available'): boolean; 33 | 34 | emit(event: 'checking-for-update'): boolean; 35 | 36 | emit(event: 'error', error: UpdateError): boolean; 37 | 38 | emit(event: string, ...args: any[]) { 39 | return super.emit(event, ...args); 40 | } 41 | 42 | // eslint-disable-next-line class-methods-use-this 43 | private async getJWS() { 44 | try { 45 | const response = await request(JWS_LINK); 46 | 47 | return response.replace(/[\n\r]/g, ''); 48 | } catch (error) { 49 | const err = error instanceof Error ? error : new Error('Unknown error'); 50 | logger.error('update', 'JWS GET error', { 51 | jwsLink: JWS_LINK, 52 | error: err.message, 53 | stack: err.stack, 54 | }); 55 | 56 | throw new UpdateError('Unable to connect to update server'); 57 | } 58 | } 59 | 60 | /** 61 | * Get info from trusted update.jws 62 | */ 63 | private async getUpdateInfo(): Promise { 64 | try { 65 | const jwsString = await this.getJWS(); 66 | 67 | return await jws.getContent(jwsString); 68 | } catch (error) { 69 | const err = error instanceof Error ? error : new Error('Unknown error'); 70 | logger.error('update', 'Get info error', { 71 | error: err.message, 72 | stack: err.stack, 73 | }); 74 | 75 | if (error instanceof UpdateError) { 76 | throw error; 77 | } 78 | 79 | throw new UpdateError('Unable to check updated version'); 80 | } 81 | } 82 | 83 | async checkForUpdates() { 84 | this.emit('checking-for-update'); 85 | logger.info('update', 'Check for new update'); 86 | 87 | try { 88 | const info = await this.getUpdateInfo(); 89 | // Get current version 90 | const packageJson = fs.readFileSync(path.join(APP_DIR, 'package.json')).toString(); 91 | const curVersion = JSON.parse(packageJson).version; 92 | 93 | // Compare versions 94 | if (semver.lt(curVersion, info.version)) { 95 | logger.info('update', 'New version was found'); 96 | 97 | this.emit('update-available', info); 98 | } else { 99 | logger.info('update', 'New version wasn\'t found'); 100 | 101 | this.emit('update-not-available'); 102 | } 103 | } catch (error) { 104 | const err = error instanceof Error ? error : new Error('Unknown error'); 105 | logger.error('update', 'Update error', { 106 | error: err.message, 107 | stack: err.stack, 108 | }); 109 | 110 | if (error instanceof UpdateError) { 111 | this.emit('error', error); 112 | } 113 | } 114 | } 115 | } 116 | 117 | export const autoUpdater = new Updater(); 118 | -------------------------------------------------------------------------------- /src/main/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request'; 2 | export * from './printf'; 3 | -------------------------------------------------------------------------------- /src/main/utils/printf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Print formatted data 3 | * 4 | * Example: 5 | * printf("Some text %1 must be %2", 1, "here") 6 | * @param text string template 7 | * @param args arguments 8 | */ 9 | export const printf = (text: string, ...args: any[]) => { 10 | let msg: string = text; 11 | let match: RegExpExecArray | null; 12 | const regFind = /(%\d+)/g; 13 | const matches: Array<{ arg: string, index: number }> = []; 14 | 15 | // eslint-disable-next-line no-cond-assign 16 | while (match = regFind.exec(msg)) { 17 | matches.push({ arg: match[1], index: match.index }); 18 | } 19 | 20 | // replace matches 21 | for (let i = matches.length - 1; i >= 0; i -= 1) { 22 | const item = matches[i]; 23 | const arg = item.arg.substring(1); 24 | const { index } = item; 25 | 26 | msg = msg.substring(0, index) + args[i] + msg.substring(index + 1 + arg.length); 27 | } 28 | 29 | // convert %% -> % 30 | // msg = msg.replace("%%", "%"); 31 | 32 | return msg; 33 | }; 34 | -------------------------------------------------------------------------------- /src/main/utils/request.ts: -------------------------------------------------------------------------------- 1 | import { getProxySettings } from 'get-proxy-settings'; 2 | import * as requestInt from 'request'; 3 | 4 | /** 5 | * Sends GET request 6 | * @param url URL 7 | */ 8 | export async function request(url: string, encoding = 'utf8') { 9 | const options: requestInt.CoreOptions = { 10 | encoding, 11 | }; 12 | const proxySettings = await getProxySettings(); 13 | if (proxySettings && proxySettings.https) { 14 | options.proxy = proxySettings.https.toString(); 15 | } 16 | 17 | return new Promise((resolve, reject) => { 18 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'; 19 | 20 | requestInt.get(url, options, (error: Error, response, body) => { 21 | if (error) { 22 | reject(error); 23 | } else { 24 | resolve(body); 25 | } 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/windows/browser_window.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | 3 | import { 4 | BrowserWindow as ElectronWindow, 5 | shell, 6 | globalShortcut, 7 | } from 'electron'; 8 | import logger from '../logger'; 9 | import { WindowsName } from '../../shared'; 10 | import * as constants from '../constants'; 11 | import { l10n } from '../l10n'; 12 | import { Assoc } from '../types'; 13 | 14 | type WindowAppType = WindowsName | 'index'; 15 | 16 | export interface IBrowserWindow extends ElectronWindow { 17 | app: WindowAppType; 18 | lang: string; 19 | params: Assoc; 20 | } 21 | 22 | export interface IWindowOptions { 23 | app: WindowAppType; 24 | title: string; 25 | size?: keyof typeof constants.windowSizes; 26 | params?: Assoc; 27 | onClosed: (...args: any[]) => void; 28 | windowOptions?: { 29 | modal?: boolean; 30 | alwaysOnTop?: boolean; 31 | x?: number; 32 | y?: number; 33 | center?: boolean; 34 | parent?: ElectronWindow ; 35 | show?: boolean; 36 | }; 37 | } 38 | 39 | /** 40 | * Base class for create browser windows and interact with them. 41 | */ 42 | export class BrowserWindow { 43 | window: IBrowserWindow; 44 | 45 | constructor(options: IWindowOptions) { 46 | this.window = new ElectronWindow({ 47 | title: options.title ? `Fortify - ${options.title}` : 'Fortify', 48 | ...this.getWindowDefaultOptions(), 49 | ...this.getWindowSize(options.size), 50 | ...options.windowOptions, 51 | }) as IBrowserWindow; 52 | 53 | this.onInit(options); 54 | } 55 | 56 | private onInit(options: IWindowOptions) { 57 | logger.info('windows', 'Create window', { 58 | name: options.app, 59 | id: options.params?.id, 60 | }); 61 | 62 | this.window.loadFile(constants.HTML_PATH, { 63 | hash: `/${options.app}`, 64 | query: options.params || {}, 65 | }); 66 | 67 | this.window.lang = l10n.lang; 68 | this.window.app = options.app; 69 | this.window.params = options.params || {}; 70 | 71 | this.onInitListeners(options); 72 | } 73 | 74 | private onInitListeners(options: IWindowOptions) { 75 | // Open a url from on default OS browser 76 | this.window.webContents.on('will-navigate', (e: Event, href: string) => { 77 | if (href !== this.window.webContents.getURL()) { 78 | e.preventDefault(); 79 | shell.openExternal(href); 80 | } 81 | }); 82 | 83 | // Show page only after `lfinish-load` event and prevent show index page 84 | if (options.app !== 'index') { 85 | this.window.webContents.once('did-finish-load', () => { 86 | this.window.show(); 87 | }); 88 | } 89 | 90 | // Prevent BrowserWindow refreshes 91 | this.window.on('focus', () => { 92 | globalShortcut.registerAll(['CommandOrControl+R', 'F5'], () => {}); 93 | }); 94 | 95 | this.window.on('blur', () => { 96 | globalShortcut.unregisterAll(); 97 | }); 98 | 99 | this.window.on('close', () => { 100 | logger.info('windows', 'Close window', { 101 | name: options.app, 102 | id: options.params?.id, 103 | }); 104 | 105 | globalShortcut.unregisterAll(); 106 | }); 107 | 108 | this.window.on('closed', options.onClosed); 109 | } 110 | 111 | private getWindowDefaultOptions(): Electron.BrowserWindowConstructorOptions { 112 | return { 113 | icon: constants.icons.favicon, 114 | autoHideMenuBar: true, 115 | minimizable: false, 116 | maximizable: false, 117 | fullscreen: false, 118 | fullscreenable: false, 119 | // Prevent resize window on production 120 | resizable: constants.isDevelopment, 121 | show: false, 122 | ...this.getWindowSize(), 123 | webPreferences: { 124 | nodeIntegration: true, 125 | // Prevent open DevTools on production 126 | devTools: constants.isDevelopment, 127 | enableRemoteModule: true, 128 | // https://github.com/PeculiarVentures/fortify/issues/453 129 | backgroundThrottling: false, 130 | contextIsolation: false, 131 | }, 132 | }; 133 | } 134 | 135 | private getWindowSize(size: keyof typeof constants.windowSizes = 'default') { 136 | if (size === 'small') { 137 | return constants.windowSizes.small; 138 | } 139 | 140 | return constants.windowSizes.default; 141 | } 142 | 143 | public setParams(params: Assoc) { 144 | this.window.params = params || {}; 145 | 146 | this.window.webContents.send('window-params-changed', params); 147 | } 148 | 149 | public getParams() { 150 | try { 151 | return this.window.params || {}; 152 | } catch { 153 | return {}; 154 | } 155 | } 156 | 157 | public focus() { 158 | this.window.focus(); 159 | } 160 | 161 | public show() { 162 | this.window.show(); 163 | } 164 | 165 | public hide() { 166 | this.window.hide(); 167 | } 168 | 169 | public reload() { 170 | this.window.reload(); 171 | } 172 | 173 | public close() { 174 | this.window.close(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/windows/dialogs_storage.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import logger from '../logger'; 3 | import * as constants from '../constants'; 4 | import type { IBrowserWindow } from './browser_window'; 5 | 6 | export class DialogsStorage { 7 | static saveDialogs(dialogs: string[]) { 8 | fs.writeFileSync(constants.APP_DIALOG_FILE, JSON.stringify(dialogs, null, ' '), { flag: 'w+' }); 9 | } 10 | 11 | static getDialogs() { 12 | let dialogs: string[] = []; 13 | 14 | if (fs.existsSync(constants.APP_DIALOG_FILE)) { 15 | try { 16 | const json = fs.readFileSync(constants.APP_DIALOG_FILE).toString(); 17 | 18 | dialogs = JSON.parse(json); 19 | 20 | if (!Array.isArray(dialogs)) { 21 | throw new TypeError('Bad JSON format. Must be Array of strings'); 22 | } 23 | } catch (error) { 24 | const err = error instanceof Error ? error : new Error('Unknown error'); 25 | logger.error('dialog-storage', 'Cannot parse JSON file', { 26 | file: constants.APP_DIALOG_FILE, 27 | error: err.message, 28 | stack: err.stack, 29 | }); 30 | } 31 | } 32 | 33 | return dialogs; 34 | } 35 | 36 | static hasDialog(name: string) { 37 | return DialogsStorage.getDialogs().includes(name); 38 | } 39 | 40 | static onDialogClose(window: IBrowserWindow) { 41 | if (window.params && window.params.id && window.params.showAgainValue) { 42 | const dialogs = DialogsStorage.getDialogs(); 43 | 44 | dialogs.push(window.params.id); 45 | DialogsStorage.saveDialogs(dialogs); 46 | 47 | logger.info('dialog-storage', 'Disable dialog', { 48 | id: window.params.id, 49 | }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/windows/index.ts: -------------------------------------------------------------------------------- 1 | export * from './windows_controller'; 2 | -------------------------------------------------------------------------------- /src/renderer/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | HashRouter, 4 | Routes, 5 | Route, 6 | } from 'react-router-dom'; 7 | import { 8 | Preferences, 9 | Message, 10 | KeyPin, 11 | P11Pin, 12 | } from './containers'; 13 | import { WindowsName } from '../shared'; 14 | 15 | export const App: React.FC = () => ( 16 | 17 | 18 | } 21 | /> 22 | } 25 | /> 26 | } 29 | /> 30 | } 33 | /> 34 | 35 | 36 | ); 37 | -------------------------------------------------------------------------------- /src/renderer/components/document_title.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { IntlContext } from './intl'; 3 | 4 | interface IDocumentTitleProps { 5 | titleKey: string | any[]; 6 | } 7 | 8 | export default class DocumentTitle extends React.Component { 9 | static contextType = IntlContext; 10 | 11 | context!: React.ContextType; 12 | 13 | render() { 14 | const { titleKey } = this.props; 15 | const { intl } = this.context; 16 | 17 | if (titleKey) { 18 | let title: string; 19 | 20 | if (Array.isArray(titleKey)) { 21 | const [key, ...other] = titleKey; 22 | 23 | title = intl(key, ...other); 24 | } else { 25 | title = intl(titleKey); 26 | } 27 | 28 | if (title) { 29 | document.title = `Fortify - ${title}`; 30 | } else { 31 | document.title = 'Fortify'; 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/renderer/components/intl/index.ts: -------------------------------------------------------------------------------- 1 | export { IntlContext } from './intl_context'; 2 | export { default as IntlProvider } from './intl_provider'; 3 | -------------------------------------------------------------------------------- /src/renderer/components/intl/intl_context.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ISO_LANGS } from '../../constants'; 3 | 4 | export interface IIntlContext { 5 | lang: string; 6 | intl: (key: string, ...args: any[]) => string; 7 | list: (keyof typeof ISO_LANGS)[]; 8 | } 9 | 10 | export const IntlContext = React.createContext({ 11 | lang: 'en', 12 | intl: () => String(), 13 | list: [], 14 | }); 15 | -------------------------------------------------------------------------------- /src/renderer/components/intl/intl_provider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ipcRenderer } from 'electron'; 3 | import { IntlContext, IIntlContext } from './intl_context'; 4 | import { printf } from '../../../main/utils'; 5 | 6 | interface IIntlProviderProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export default class IntlProvider extends React.Component { 11 | UNSAFE_componentWillMount() { 12 | ipcRenderer.on('ipc-language-changed', this.onLanguageListener); 13 | 14 | this.onLanguageListener(); 15 | } 16 | 17 | onLanguageListener = () => { 18 | const l10n = ipcRenderer.sendSync('ipc-language-get'); 19 | 20 | this.setState({ 21 | lang: l10n.lang, 22 | list: l10n.list, 23 | intl: this.intl(l10n.data), 24 | }); 25 | }; 26 | 27 | intl = (data: Record) => (key: string, ...args: any[]): string => { 28 | const text = data[key]; 29 | 30 | return text ? printf(text, args) : `{${key}}`; 31 | }; 32 | 33 | render() { 34 | const { children } = this.props; 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/dialog_layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button } from 'lib-react-components'; 3 | import { IntlContext } from '../intl'; 4 | 5 | const s = require('./styles/dialog_layout.sass'); 6 | 7 | export interface IDialogLayoutProps { 8 | children: React.ReactNode; 9 | icon: React.ReactNode; 10 | footer?: React.ReactNode; 11 | onReject?: () => void; 12 | onApprove?: () => void; 13 | textReject?: string; 14 | textApprove?: string; 15 | } 16 | 17 | export default class DialogLayout extends React.Component { 18 | static contextType = IntlContext; 19 | 20 | renderButtons() { 21 | const { 22 | onReject, 23 | onApprove, 24 | textReject, 25 | textApprove, 26 | } = this.props; 27 | const { intl } = this.context; 28 | const buttons = []; 29 | 30 | if (onReject) { 31 | buttons.push(( 32 | 43 | )); 44 | } 45 | 46 | if (onApprove) { 47 | buttons.push(( 48 | 57 | )); 58 | } 59 | 60 | return buttons; 61 | } 62 | 63 | render() { 64 | const { children, icon, footer } = this.props; 65 | 66 | return ( 67 |
68 |
69 |
70 | {children} 71 |
72 |
73 | {icon} 74 |
75 |
76 |
77 |
78 | {footer} 79 |
80 |
81 | {this.renderButtons()} 82 |
83 |
84 |
85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DialogLayout, IDialogLayoutProps } from './dialog_layout'; 2 | export { default as ModalLayout } from './modal_layout'; 3 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/modal_layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Typography } from 'lib-react-components'; 3 | import { IntlContext } from '../intl'; 4 | 5 | const s = require('./styles/modal_layout.sass'); 6 | 7 | export interface IModalLayoutProps { 8 | children: React.ReactNode; 9 | title: (string | React.ReactNode)[]; 10 | onReject: () => void; 11 | onApprove: () => void; 12 | textReject?: string; 13 | textApprove?: string; 14 | } 15 | 16 | export default class ModalLayout extends React.Component { 17 | static contextType = IntlContext; 18 | 19 | renderButtons() { 20 | const { 21 | onReject, 22 | onApprove, 23 | textReject, 24 | textApprove, 25 | } = this.props; 26 | const { intl } = this.context; 27 | 28 | return ( 29 | <> 30 | 41 | 50 | 51 | ); 52 | } 53 | 54 | render() { 55 | const { children, title } = this.props; 56 | 57 | return ( 58 |
59 |
60 |
61 | {title.map((value, index) => ( 62 | 68 | {value} 69 | 70 | ))} 71 |
72 |
73 | {children} 74 |
75 |
76 |
77 | {this.renderButtons()} 78 |
79 |
80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/styles/dialog_layout.sass: -------------------------------------------------------------------------------- 1 | .host 2 | height: 100% 3 | padding: 35px 40px 40px 4 | width: 100% 5 | display: block 6 | 7 | .container_body 8 | height: calc(100% - 40px - 10px) 9 | margin-bottom: 10px 10 | 11 | .container_content 12 | display: inline-block 13 | vertical-align: top 14 | width: calc(100% - 70px) 15 | padding-right: 10px 16 | 17 | .container_icon 18 | display: inline-block 19 | vertical-align: top 20 | width: 70px 21 | text-align: right 22 | 23 | .footer 24 | width: 100% 25 | display: flex 26 | justify-content: space-between 27 | align-items: center 28 | height: 40px 29 | 30 | .button 31 | & + .button 32 | margin-left: 10px 33 | 34 | .buttons_container 35 | white-space: nowrap 36 | 37 | .footer_content_container 38 | padding-right: 10px 39 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/styles/modal_layout.sass: -------------------------------------------------------------------------------- 1 | .host 2 | height: 100% 3 | padding: 50px 100px 60px 4 | width: 100% 5 | display: block 6 | text-align: center 7 | 8 | .container_body 9 | height: calc(100% - 40px - 10px) 10 | margin-bottom: 10px 11 | 12 | .footer 13 | width: 100% 14 | height: 40px 15 | 16 | .button 17 | & + .button 18 | margin-left: 10px 19 | 20 | .container_title 21 | min-height: 116px 22 | margin-bottom: 20px 23 | max-height: 200px 24 | overflow: auto 25 | 26 | .title 27 | & + .title 28 | margin-top: 8px 29 | -------------------------------------------------------------------------------- /src/renderer/components/window_event.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export interface IWindowEventProps { 4 | event: string; 5 | onCall: (e: any) => void; 6 | } 7 | export interface IWindowEventState { } 8 | 9 | export class WindowEvent extends React.Component { 10 | constructor(props: IWindowEventProps) { 11 | super(props); 12 | 13 | this.state = {}; 14 | } 15 | 16 | public componentDidMount() { 17 | const { event, onCall } = this.props; 18 | 19 | window.addEventListener(event, onCall); 20 | } 21 | 22 | public componentWillUnmount() { 23 | const { event, onCall } = this.props; 24 | 25 | window.removeEventListener(event, onCall); 26 | } 27 | 28 | public render() { 29 | return null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/components/window_provider.tsx: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | import * as React from 'react'; 3 | import * as winston from 'winston'; 4 | import { IntlProvider } from './intl'; 5 | import DocumentTitle from './document_title'; 6 | 7 | winston.add(new winston.transports.Console()); 8 | 9 | export default abstract class WindowProvider extends React.Component { 10 | public params: Record; 11 | 12 | constructor(props: P) { 13 | super(props); 14 | 15 | this.params = (remote.getCurrentWindow() as any).params || {}; 16 | } 17 | 18 | // eslint-disable-next-line class-methods-use-this 19 | protected onClose(...args: any[]) { 20 | console.log(args); 21 | } 22 | 23 | close = (...args: any[]) => { 24 | this.onClose(...args); 25 | 26 | remote.getCurrentWindow().close(); 27 | }; 28 | 29 | abstract renderChildrens(): JSX.Element; 30 | 31 | render() { 32 | return ( 33 | 34 | 37 | {this.renderChildrens()} 38 | 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './iso_langs'; 2 | -------------------------------------------------------------------------------- /src/renderer/containers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './key_pin'; 2 | export * from './message'; 3 | export * from './p11_pin'; 4 | export * from './preferences'; 5 | -------------------------------------------------------------------------------- /src/renderer/containers/key_pin/container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Typography, Box } from 'lib-react-components'; 3 | import { ModalLayout } from '../../components/layouts'; 4 | import { WindowEvent } from '../../components/window_event'; 5 | import { IntlContext } from '../../components/intl'; 6 | 7 | const s = require('./styles/container.sass'); 8 | 9 | export interface IContainerProps { 10 | onApprove: () => void; 11 | onReject: () => void; 12 | origin: string; 13 | pin: string; 14 | } 15 | 16 | export default class Container extends React.Component { 17 | static contextType = IntlContext; 18 | 19 | onKeyDown = (e: KeyboardEvent) => { 20 | const { onApprove, onReject } = this.props; 21 | 22 | switch (e.keyCode) { 23 | case 13: // enter 24 | onApprove(); 25 | break; 26 | 27 | case 27: // esc 28 | onReject(); 29 | break; 30 | 31 | default: 32 | // nothing 33 | } 34 | }; 35 | 36 | render() { 37 | const { 38 | onApprove, 39 | onReject, 40 | origin, 41 | pin, 42 | } = this.props; 43 | const { intl } = this.context; 44 | 45 | return ( 46 | <> 47 | 51 | {origin}
,
, intl('key-pin.1', '')], 54 | intl('key-pin.2', intl('approve')), 55 | ]} 56 | onApprove={onApprove} 57 | onReject={onReject} 58 | textApprove={intl('approve')} 59 | > 60 | {pin.split('').map((char, index) => ( 61 | 66 | 71 | {char} 72 | 73 | 74 | ))} 75 | 76 | 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/renderer/containers/key_pin/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import WindowProvider from '../../components/window_provider'; 3 | import Container from './container'; 4 | 5 | export class KeyPin extends WindowProvider<{}, {}> { 6 | onReject = () => { 7 | this.params.accept = false; 8 | this.close(); 9 | }; 10 | 11 | onApprove = () => { 12 | this.params.accept = true; 13 | this.close(); 14 | }; 15 | 16 | renderChildrens() { 17 | return ( 18 | 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/containers/key_pin/styles/container.sass: -------------------------------------------------------------------------------- 1 | .pin_item 2 | display: inline-block 3 | vertical-align: top 4 | width: 30px 5 | border-radius: 3px 6 | & + .pin_item 7 | margin-left: 5px 8 | 9 | .pin_value 10 | line-height: 38px 11 | -------------------------------------------------------------------------------- /src/renderer/containers/message/container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Typography, Checkbox } from 'lib-react-components'; 3 | import { DialogLayout, IDialogLayoutProps } from '../../components/layouts'; 4 | import { WindowEvent } from '../../components/window_event'; 5 | import { IntlContext } from '../../components/intl'; 6 | 7 | const s = require('./styles/container.sass'); 8 | 9 | export interface IContainerProps { 10 | type: 'error' | 'warning' | 'question' | 'token'; 11 | text: string; 12 | onClose: (showAgain?: boolean) => void; 13 | onApprove?: (showAgain?: boolean) => void; 14 | hasShowAgain?: boolean; 15 | defaultShowAgainValue?: boolean; 16 | buttonRejectLabel?: string; 17 | buttonApproveLabel?: string; 18 | } 19 | 20 | export default class Container extends React.Component { 21 | static contextType = IntlContext; 22 | 23 | checkboxRef = React.createRef(); 24 | 25 | onKeyDown = (e: KeyboardEvent) => { 26 | const { type } = this.props; 27 | 28 | switch (e.keyCode) { 29 | case 13: // enter 30 | if (['question', 'token'].includes(type)) { 31 | this.onApprove(); 32 | } 33 | 34 | break; 35 | 36 | case 27: // esc 37 | this.onClose(); 38 | break; 39 | 40 | default: 41 | // nothing 42 | } 43 | }; 44 | 45 | onClose = () => { 46 | const { onClose } = this.props; 47 | const { checkboxRef } = this; 48 | 49 | onClose(checkboxRef?.current?.isChecked()); 50 | }; 51 | 52 | onApprove = () => { 53 | const { onApprove } = this.props; 54 | const { checkboxRef } = this; 55 | 56 | if (onApprove) { 57 | onApprove(checkboxRef?.current?.isChecked()); 58 | } 59 | }; 60 | 61 | getDialogProps() { 62 | const { 63 | type, 64 | buttonRejectLabel, 65 | buttonApproveLabel, 66 | hasShowAgain, 67 | defaultShowAgainValue, 68 | } = this.props; 69 | const { intl } = this.context; 70 | const props: Omit = { 71 | icon: ( 72 | Attention icon 77 | ), 78 | onReject: this.onClose, 79 | textReject: buttonRejectLabel && intl(buttonRejectLabel), 80 | }; 81 | 82 | if (hasShowAgain) { 83 | props.footer = ( 84 | 98 | ); 99 | } 100 | 101 | switch (type) { 102 | case 'error': 103 | props.icon = ( 104 | Error icon 109 | ); 110 | break; 111 | 112 | case 'question': 113 | props.icon = ( 114 | Question icon 119 | ); 120 | 121 | props.onApprove = this.onApprove; 122 | props.textReject = intl(buttonRejectLabel || 'no'); 123 | props.textApprove = intl(buttonApproveLabel || 'yes'); 124 | break; 125 | 126 | case 'token': 127 | props.icon = ( 128 | Token icon 133 | ); 134 | 135 | props.onApprove = this.onApprove; 136 | props.textReject = intl(buttonRejectLabel || 'no'); 137 | props.textApprove = intl(buttonApproveLabel || 'yes'); 138 | break; 139 | 140 | default: 141 | } 142 | 143 | return props; 144 | } 145 | 146 | render() { 147 | const { text } = this.props; 148 | 149 | return ( 150 | <> 151 | 155 | 158 | {text.split('\n').map((part: string, index) => ( 159 | 164 | {part} 165 | 166 | ))} 167 | 168 | 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/renderer/containers/message/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import WindowProvider from '../../components/window_provider'; 3 | import Container from './container'; 4 | 5 | export class Message extends WindowProvider<{}, {}> { 6 | onApprove = (showAgain?: boolean) => { 7 | this.params.result = 1; 8 | this.close(showAgain); 9 | }; 10 | 11 | onClose = (showAgain?: boolean) => { 12 | if (this.params.id && this.params.showAgain) { 13 | this.params.showAgainValue = showAgain; 14 | } 15 | }; 16 | 17 | renderChildrens() { 18 | return ( 19 | 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/containers/message/styles/container.sass: -------------------------------------------------------------------------------- 1 | .text 2 | margin-bottom: 8px 3 | -------------------------------------------------------------------------------- /src/renderer/containers/p11_pin/container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { TextField } from 'lib-react-components'; 3 | import { ModalLayout } from '../../components/layouts'; 4 | import { WindowEvent } from '../../components/window_event'; 5 | import { IntlContext } from '../../components/intl'; 6 | 7 | const s = require('./styles/container.sass'); 8 | 9 | export interface IContainerProps { 10 | onApprove: (password: string) => void; 11 | onReject: () => void; 12 | origin: string; 13 | } 14 | 15 | export default class Container extends React.Component { 16 | static contextType = IntlContext; 17 | 18 | context!: React.ContextType; 19 | 20 | textFieldRef = React.createRef(); 21 | 22 | onKeyDown = (e: KeyboardEvent) => { 23 | const { onReject } = this.props; 24 | 25 | switch (e.keyCode) { 26 | case 13: // enter 27 | this.onApprove(); 28 | break; 29 | 30 | case 27: // esc 31 | onReject(); 32 | break; 33 | 34 | default: 35 | // nothing 36 | } 37 | }; 38 | 39 | onApprove = () => { 40 | const { onApprove } = this.props; 41 | const password = this.textFieldRef.current.inputNode.getValue(); 42 | 43 | onApprove(password); 44 | }; 45 | 46 | render() { 47 | const { 48 | onReject, 49 | origin, 50 | } = this.props; 51 | const { intl } = this.context; 52 | 53 | return ( 54 | <> 55 | 59 | {origin},
, intl('p11-pin.1')], 62 | intl('p11-pin.2'), 63 | ]} 64 | onApprove={this.onApprove} 65 | onReject={onReject} 66 | > 67 | 77 |
78 | 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/renderer/containers/p11_pin/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import WindowProvider from '../../components/window_provider'; 3 | import Container from './container'; 4 | 5 | export class P11Pin extends WindowProvider<{}, {}> { 6 | onApprove = (password: string) => { 7 | this.params.pin = password; 8 | this.close(); 9 | }; 10 | 11 | onReject = () => { 12 | this.params.pin = ''; 13 | this.close(); 14 | }; 15 | 16 | renderChildrens() { 17 | return ( 18 | 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/containers/p11_pin/styles/container.sass: -------------------------------------------------------------------------------- 1 | .field 2 | max-width: 205px 3 | margin: 0 auto 4 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/about.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Typography } from 'lib-react-components'; 3 | import { IntlContext } from '../../components/intl'; 4 | 5 | const s = require('./styles/about.sass'); 6 | 7 | interface IAboutProps { 8 | name: any; 9 | version: string; 10 | } 11 | 12 | // eslint-disable-next-line react/prefer-stateless-function 13 | export class About extends React.Component { 14 | static contextType = IntlContext; 15 | 16 | render() { 17 | const { version } = this.props; 18 | const { intl } = this.context; 19 | 20 | return ( 21 |
22 |
23 | 26 | Fortify {intl('by')} Peculiar Ventures 27 | 28 | 31 | {intl('version')} {version} 32 | 33 |
34 | 37 | {intl('made.with')} 38 | 39 | 42 | {intl('copyright')}. {intl('all.rights')}. 43 | 44 |
45 | 67 |
68 |
69 | Fortify logo 74 |
75 |
76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Tabs, 4 | Tab, 5 | Box, 6 | SegueHandler, 7 | } from 'lib-react-components'; 8 | import classnames from 'classnames'; 9 | import { WindowPreferencesName } from '../../../shared'; 10 | import { IntlContext } from '../../components/intl'; 11 | import { Sites } from './sites'; 12 | import { About } from './about'; 13 | import { Settings } from './settings'; 14 | import { Updates } from './updates'; 15 | 16 | const s = require('./styles/container.sass'); 17 | 18 | export interface IContainerProps { 19 | logging: { 20 | onLoggingOpen: () => void; 21 | onLoggingStatusChange: () => void; 22 | status: boolean; 23 | }; 24 | telemetry: { 25 | onTelemetryStatusChange: () => void; 26 | status: boolean; 27 | }; 28 | language: { 29 | onLanguageChange: (lang: string) => void; 30 | }; 31 | keys: { 32 | list: IKey[]; 33 | isFetching: IsFetchingType; 34 | onKeyRemove: (origin: string) => void; 35 | }; 36 | theme: { 37 | value: ThemeType; 38 | onThemeChange: (theme: ThemeType) => void; 39 | }; 40 | update: { 41 | isFetching: IsFetchingType; 42 | info?: UpdateInfoType; 43 | }; 44 | version: string; 45 | tab: { 46 | value: WindowPreferencesName; 47 | onChange: (value: WindowPreferencesName) => void; 48 | }; 49 | } 50 | 51 | export interface IContainerState { 52 | tab: WindowPreferencesName; 53 | } 54 | 55 | export default class Container extends React.Component { 56 | static contextType = IntlContext; 57 | 58 | handleChangeTab = (_: Event, value: string | number) => { 59 | const { tab } = this.props; 60 | 61 | tab.onChange(value as WindowPreferencesName); 62 | }; 63 | 64 | // eslint-disable-next-line class-methods-use-this 65 | renderNotificationBadge() { 66 | return ( 67 | 71 | ); 72 | } 73 | 74 | render() { 75 | const { 76 | language, 77 | keys, 78 | logging, 79 | telemetry, 80 | version, 81 | theme, 82 | update, 83 | tab, 84 | } = this.props; 85 | const { intl } = this.context; 86 | 87 | return ( 88 | 92 | 96 | 104 | 108 | {intl('sites')} 109 | 110 | 114 | {intl('settings')} 115 | 116 | 120 | {intl('updates')} 121 | {update.info ? this.renderNotificationBadge() : null} 122 | 123 | 127 | {intl('about')} 128 | 129 | 130 | 131 |
132 | 133 | 137 | 144 | 148 | 152 | 153 |
154 |
155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Box, 4 | Typography, 5 | Select, 6 | Switch, 7 | Button, 8 | SelectChangeEvent, 9 | } from 'lib-react-components'; 10 | import classnames from 'classnames'; 11 | import { IntlContext } from '../../components/intl'; 12 | import { ISO_LANGS } from '../../constants'; 13 | 14 | const s = require('./styles/settings.sass'); 15 | 16 | interface ISettingsProps { 17 | name: any; 18 | language: { 19 | onLanguageChange: (lang: string) => void; 20 | }; 21 | logging: { 22 | onLoggingOpen: () => void; 23 | onLoggingStatusChange: () => void; 24 | status: boolean; 25 | }; 26 | telemetry: { 27 | onTelemetryStatusChange: () => void; 28 | status: boolean; 29 | }; 30 | theme: { 31 | value: ThemeType; 32 | onThemeChange: (theme: ThemeType) => void; 33 | }; 34 | } 35 | 36 | export class Settings extends React.Component { 37 | static contextType = IntlContext; 38 | 39 | context!: React.ContextType; 40 | 41 | handleChangeLanguage = (event: SelectChangeEvent) => { 42 | const { language } = this.props; 43 | const { value } = event.target; 44 | 45 | language.onLanguageChange(value as string); 46 | }; 47 | 48 | handleChangeTheme = (event: SelectChangeEvent) => { 49 | const { theme } = this.props; 50 | const { value } = event.target; 51 | 52 | theme.onThemeChange(value as ThemeType); 53 | }; 54 | 55 | render() { 56 | const { telemetry, logging, theme } = this.props; 57 | const { list, lang, intl } = this.context; 58 | 59 | return ( 60 | <> 61 | 66 | 69 | {intl('language')} 70 | 71 |
72 | ({ 108 | value, 109 | label: intl(`theme.${value}`), 110 | }))} 111 | /> 112 |
113 |
114 | 115 | 120 | 123 | {intl('telemetry')} 124 | 125 |
126 | 141 |
142 |
143 | 144 | 149 | 152 | {intl('logging')} 153 | 154 |
155 | 167 | 174 |
175 |
176 | 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/sites.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Typography, 4 | Box, 5 | TextField, 6 | Button, 7 | CircularProgress, 8 | } from 'lib-react-components'; 9 | import classnames from 'classnames'; 10 | import { IntlContext } from '../../components/intl'; 11 | 12 | const s = require('./styles/sites.sass'); 13 | 14 | interface ISitesProps { 15 | name: any; 16 | keys: { 17 | list: IKey[]; 18 | isFetching: IsFetchingType; 19 | onKeyRemove: (origin: string) => void; 20 | }; 21 | } 22 | 23 | interface ISitesState { 24 | search: string; 25 | } 26 | 27 | export class Sites extends React.Component { 28 | static browsersList = [ 29 | { 30 | title: 'Firefox', 31 | name: 'firefox', 32 | src: '../static/icons/firefox.png', 33 | }, 34 | { 35 | title: 'Chrome', 36 | name: 'chrome', 37 | src: '../static/icons/chrome.png', 38 | }, 39 | { 40 | title: 'Safari', 41 | name: 'safari', 42 | src: '../static/icons/safari.png', 43 | }, 44 | { 45 | title: 'Edge', 46 | name: 'edge', 47 | src: '../static/icons/edge.png', 48 | }, 49 | { 50 | title: 'Internet Explorer', 51 | name: 'ie', 52 | src: '../static/icons/ie.png', 53 | }, 54 | ]; 55 | 56 | static contextType = IntlContext; 57 | 58 | constructor(props: ISitesProps) { 59 | super(props); 60 | 61 | this.state = { 62 | search: '', 63 | }; 64 | } 65 | 66 | onChangeSearch = (e: any) => { 67 | this.setState({ 68 | search: e.target.value.toLowerCase(), 69 | }); 70 | }; 71 | 72 | renderKeyItem(key: IKey) { 73 | const { keys } = this.props; 74 | const { intl } = this.context; 75 | 76 | return ( 77 | 84 |
85 | 88 | {key.origin} 89 | 90 | 95 | {new Date(key.created).toLocaleDateString()} 96 | 97 |
98 | {Sites.browsersList.map((browser) => ( 99 | {browser.title} 107 | ))} 108 |
109 |
110 | 119 |
120 | ); 121 | } 122 | 123 | renderContent() { 124 | const { keys } = this.props; 125 | const { search } = this.state; 126 | const { intl } = this.context; 127 | 128 | if (keys.isFetching === 'pending') { 129 | return ( 130 | 131 | ); 132 | } 133 | 134 | if (keys.list.length === 0) { 135 | return ( 136 |
137 | Globe icon 143 | 147 | {intl('keys.empty')} 148 | 149 |
150 | ); 151 | } 152 | 153 | const newList: React.ReactNode[] = []; 154 | 155 | keys.list.forEach((key) => { 156 | if (search) { 157 | const origin = key.origin.toLowerCase(); 158 | const hasMatch = origin.includes(search); 159 | 160 | if (!hasMatch) { 161 | return; 162 | } 163 | } 164 | 165 | newList.push(this.renderKeyItem(key)); 166 | }); 167 | 168 | if (newList.length === 0) { 169 | return ( 170 |
171 | Search icon 177 | 181 | {intl('keys.empty.search')} 182 | 183 |
184 | ); 185 | } 186 | 187 | return ( 188 |
    189 | {newList} 190 |
191 | ); 192 | } 193 | 194 | render() { 195 | const { keys } = this.props; 196 | const { intl } = this.context; 197 | 198 | return ( 199 | <> 200 | 209 |
217 | {this.renderContent()} 218 |
219 | 220 | ); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/styles/about.sass: -------------------------------------------------------------------------------- 1 | .root 2 | display: flex 3 | justify-content: space-between 4 | 5 | .links 6 | font-size: 0 7 | 8 | .link 9 | display: inline-block 10 | vertical-align: middle 11 | & + .link 12 | margin-left: 20px 13 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/styles/container.sass: -------------------------------------------------------------------------------- 1 | .host 2 | height: 100% 3 | padding: 10px 0 4 | 5 | .tabs 6 | padding: 0 20px 7 | 8 | .tab 9 | padding: 0 10 | text-transform: uppercase 11 | font-weight: 600 !important 12 | line-height: 48px 13 | position: relative 14 | & + .tab 15 | margin-left: 20px 16 | 17 | .content 18 | padding: 20px 19 | height: calc(100% - 41px) 20 | overflow: auto 21 | 22 | .badge 23 | position: absolute 24 | top: 14px 25 | left: 100% 26 | width: 8px 27 | height: 8px 28 | border-radius: 50% 29 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/styles/settings.sass: -------------------------------------------------------------------------------- 1 | .m_alight 2 | display: flex 3 | justify-content: space-between 4 | align-items: center 5 | 6 | .item 7 | @extend .m_alight 8 | padding: 20px 9 | min-height: 80px 10 | &:not(:first-child) 11 | margin-top: 10px 12 | 13 | .actions 14 | width: 78% 15 | margin-left: 10px 16 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/styles/sites.sass: -------------------------------------------------------------------------------- 1 | .item_key 2 | border-radius: 3px 3 | display: block 4 | width: 100% 5 | padding: 14px 30px 17px 20px 6 | display: flex 7 | justify-content: space-between 8 | align-items: center 9 | & + .item_key 10 | margin-top: 10px 11 | &:hover 12 | background: rgba(var(--grey_2), 0.2) !important 13 | .button_remove 14 | visibility: visible 15 | 16 | .date 17 | margin-bottom: 5px 18 | 19 | .image_browser 20 | float: left 21 | height: 16px 22 | filter: grayscale(100%) 23 | opacity: .7 24 | &[data-active="true"] 25 | filter: none 26 | opacity: 1 27 | & + .image_browser 28 | margin-left: 5px 29 | 30 | .button_remove 31 | padding: 0 15px 32 | margin-left: 15px 33 | flex-shrink: 0 34 | visibility: hidden 35 | 36 | .search 37 | margin-bottom: 10px 38 | 39 | .content 40 | height: calc(100% - 40px - 10px) 41 | overflow: auto 42 | &.m_center 43 | display: flex 44 | align-items: center 45 | justify-content: center 46 | 47 | .container_list_state 48 | padding: 15px 10px 49 | font-size: 0 50 | display: flex 51 | width: 100% 52 | align-items: start 53 | 54 | .icon_list_state 55 | margin-right: 12px 56 | 57 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/styles/updates.sass: -------------------------------------------------------------------------------- 1 | .container_update_available 2 | padding: 12px 20px 20px 3 | 4 | .description_update_available 5 | margin-top: 4px 6 | 7 | .footer_update_available 8 | margin-top: 24px 9 | display: flex 10 | align-items: center 11 | justify-content: space-between 12 | 13 | .container_cheking 14 | display: flex 15 | align-items: center 16 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/types.d.ts: -------------------------------------------------------------------------------- 1 | interface IKey { 2 | origin: string; 3 | created: Date; 4 | browsers: string[] 5 | } 6 | 7 | type IsFetchingType = 'pending' | 'resolved' | 'rejected'; 8 | 9 | type ThemeType = ('system' | 'dark' | 'light'); 10 | 11 | type UpdateInfoType = { 12 | version: string; 13 | createdAt: number; 14 | }; 15 | -------------------------------------------------------------------------------- /src/renderer/containers/preferences/updates.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Typography, 4 | Button, 5 | CircularProgress, 6 | Box, 7 | } from 'lib-react-components'; 8 | import { GITHUB_REPO_LINK, DOWNLOAD_LINK } from '../../../main/constants'; 9 | import { IntlContext } from '../../components/intl'; 10 | 11 | const s = require('./styles/updates.sass'); 12 | 13 | interface IUpdatesProps { 14 | name: any; 15 | update: { 16 | isFetching: IsFetchingType; 17 | info?: UpdateInfoType; 18 | }; 19 | } 20 | 21 | export class Updates extends React.Component { 22 | static contextType = IntlContext; 23 | 24 | context!: React.ContextType; 25 | 26 | renderChekingState() { 27 | const { intl } = this.context; 28 | 29 | return ( 30 |
31 | 36 | 40 | {intl('updates.checking')} 41 | 42 |
43 | ); 44 | } 45 | 46 | renderLatestVersionState() { 47 | const { intl } = this.context; 48 | 49 | return ( 50 | 54 | {intl('updates.latest')} 55 | 56 | ); 57 | } 58 | 59 | renderUpdateVersionState() { 60 | const { update } = this.props; 61 | const { intl } = this.context; 62 | 63 | return ( 64 | 69 | 72 | {intl('updates.available')} 73 | 74 | 79 | v{update.info?.version} ({new Date(update.info?.createdAt as any).toLocaleDateString()}) 80 | 81 |
82 | 94 | 101 |
102 |
103 | ); 104 | } 105 | 106 | render() { 107 | const { update } = this.props; 108 | 109 | if (update.isFetching === 'pending') { 110 | return this.renderChekingState(); 111 | } 112 | 113 | if (update.info) { 114 | return this.renderUpdateVersionState(); 115 | } 116 | 117 | return this.renderLatestVersionState(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { App } from './app'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('root'), 8 | ); 9 | -------------------------------------------------------------------------------- /src/resources/new_card.tmpl: -------------------------------------------------------------------------------- 1 | Reader name: ${reader} 2 | ATR: ${atr} 3 | 4 | ```js 5 | { 6 | "cards": [{ 7 | "atr": "${atr}", 8 | "name": "Token name", 9 | "driver": "${driver}" 10 | }], 11 | "drivers": [{ 12 | "id": "${driver}", 13 | "name": "Driver name", 14 | "file": { 15 | "windows": "path/to/pkcs11.dll", 16 | "osx": "path/to/pkcs11.dylib" 17 | } 18 | }] 19 | } 20 | ``` 21 | 22 | [Smart card ATR parsing ${atr}](https://smartcard-atr.apdu.fr/parse?ATR=${atr}) -------------------------------------------------------------------------------- /src/resources/osx-ssl.sh: -------------------------------------------------------------------------------- 1 | # Add certificate to system key chain 2 | 3 | certPath=${certPath} 4 | certificateName=${certName} 5 | 6 | echo -e "certificateName: ${certificateName}" 7 | echo -e "certPath: ${certPath}" 8 | 9 | # keychain 10 | keychain=$(security default-keychain -d user | sed 's/"//g') 11 | security delete-certificate -c ${certificateName} ${keychain} 12 | security add-trusted-cert -r trustRoot -k ${keychain} ${certPath} 13 | -------------------------------------------------------------------------------- /src/shared/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './windows'; 2 | -------------------------------------------------------------------------------- /src/shared/constants/windows.ts: -------------------------------------------------------------------------------- 1 | export enum WindowsName { 2 | Preferences = 'preferences', 3 | Message = 'message', 4 | KeyPin = 'key-pin', 5 | P11Pin = 'p11-pin', 6 | } 7 | 8 | export enum WindowPreferencesName { 9 | Sites = 'sites', 10 | Settings = 'settings', 11 | Updates = 'updates', 12 | About = 'about', 13 | } 14 | -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | -------------------------------------------------------------------------------- /src/static/icons/attention_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/static/icons/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/chrome.png -------------------------------------------------------------------------------- /src/static/icons/edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/edge.png -------------------------------------------------------------------------------- /src/static/icons/error_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/static/icons/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/firefox.png -------------------------------------------------------------------------------- /src/static/icons/github_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/icons/globe_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/icons/ie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/ie.png -------------------------------------------------------------------------------- /src/static/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/static/icons/question_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/static/icons/safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/safari.png -------------------------------------------------------------------------------- /src/static/icons/search_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/icons/token_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/static/icons/tray/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/mac/icon.icns -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@1.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@1.5x.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@16x.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@2x.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@32x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@32x.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@3x.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@4x.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@64x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@64x.png -------------------------------------------------------------------------------- /src/static/icons/tray/png/icon@8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/png/icon@8x.png -------------------------------------------------------------------------------- /src/static/icons/tray/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray/win/icon.ico -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@1.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@1.5x.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@16x.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@2x.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@32x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@32x.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@3x.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@4x.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@64x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@64x.png -------------------------------------------------------------------------------- /src/static/icons/tray_notification/png/icon@8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PeculiarVentures/fortify/5916fac1d3e1c6d16b676f52e535ab37b7c3a822/src/static/icons/tray_notification/png/icon@8x.png -------------------------------------------------------------------------------- /src/static/icons/twitter_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 137 | 138 | 139 |
140 | 148 |
149 |
150 | 151 | 152 | 153 | 154 |
155 |
156 |
157 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /test/nss.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as os from 'os'; 3 | import * as path from 'path'; 4 | import { NssCertUtils } from '../src/main/services/ssl/nss'; 5 | import { Firefox } from '../src/main/services/ssl/firefox'; 6 | 7 | context('NssCertUtils', () => { 8 | const resourcesFolder = path.join(__dirname, 'resources'); 9 | const ca = path.join(resourcesFolder, 'ca.pem'); 10 | const platform = os.platform(); 11 | const certutil = platform === 'linux' 12 | ? 'certutil' 13 | : path.join(__dirname, '..', 'nss', 'utils', 'certutil'); 14 | const profiles = Firefox.profiles(); 15 | if (!profiles.length) { 16 | throw new Error('Cannot find any Mozilla Firefox profiles'); 17 | } 18 | const profile = profiles[0]; 19 | // eslint-disable-next-line no-console 20 | console.log('Mozilla Firefox profile:', profile); 21 | const nss = new NssCertUtils(certutil, `sql:${profile}`); 22 | 23 | it('list certificate', () => { 24 | const certs = nss.list(); 25 | certs.forEach((o) => { 26 | assert.equal(!!o.name, true, 'Certificate name is empty'); 27 | assert.equal(!!o.attrs, true, 'Certificate trusted attributes are empty'); 28 | }); 29 | }); 30 | 31 | it('get cert', () => { 32 | const cert = nss.get('Amazon'); 33 | assert.equal(/-----BEGIN CERTIFICATE/.test(cert), true, 'Certificate is not PEM'); 34 | }); 35 | 36 | it('exists', () => { 37 | assert.equal(nss.exists('Amazon'), true); 38 | assert.equal(nss.exists('Amazon!!!'), false); 39 | }); 40 | 41 | it('add/remove cert', () => { 42 | const certName = 'Fortify test'; 43 | try { 44 | nss.add(ca, certName, 'CT,c,'); 45 | } finally { 46 | nss.remove(certName); 47 | } 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/resources/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDFzCCAf+gAwIBAgIBATANBgkqhkiG9w0BAQsFADA7MR4wHAYDVQQKDBVQZWN1 3 | bGlhclZlbnR1cmVzLCBMTEMxGTAXBgNVBAMMEEZvcnRpZnkgTG9jYWwgQ0EwHhcN 4 | MjAwNjAzMTUxMjQxWhcNMjUwNTA4MTUxMjQxWjA7MR4wHAYDVQQKDBVQZWN1bGlh 5 | clZlbnR1cmVzLCBMTEMxGTAXBgNVBAMMEEZvcnRpZnkgTG9jYWwgQ0EwggEiMA0G 6 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuVAtWRS1LTiuMScQDOulx2OhFoCXQ 7 | LM00UDKcBhxde+Am02WEHf6Ey9T+Av0q/sKbAwo9zpFSNBsNJxnDiK/xJmi6Ty41 8 | JPitA/HBGawwizX7MbDfgXjzmlB3MjdyknAsdEzCnhXjGvdolEwAiNwefqApRdcJ 9 | k02Xkp1e64hKiK+wkSJihhMSl0TOU1rWYummuKCtkzFpqPl7fgRcAXl1ZFIu2rLT 10 | T45LTNP3Bxb7oQ9R+dnSc61Gn3s5q5Zy4SoazL+QH5E6bASM1jvHgwzM1USAOSH+ 11 | qEEv7znq4iSCQMoxk7JNz9RXvFBx33QwOzHbO0+g1859eZbxmh2sLWZfAgMBAAGj 12 | JjAkMBIGA1UdEwEB/wQIMAYBAf8CAQIwDgYDVR0PAQH/BAQDAgIEMA0GCSqGSIb3 13 | DQEBCwUAA4IBAQChpRdlb5YRUQWfoZFiMGA4sqB0yD/s3unciDGYGaRu88Cwojs7 14 | XK3gR6X7rmOKl0bPXtTvTndD/F0BsyZRizmhJvchS7GbFTlEGBt9qT8Rxe7g0MVO 15 | ayGzHgodM91FvSscG55GuQSsoCqGAugfXsPEMH0LqnqVhyx54qytG02bvUuk7yoX 16 | WTot9Zq/g18Py+VZ3eqheIkobSUaf9D1SUckpJhMLy6FTx93u3YbibhuD5ZAzQVP 17 | KWait0zpxrM8xljucEKzSLWlv9d2oV7Eo6ebL0UgYx/XJ+NsEWHMtUZW5tRhRMBW 18 | mPCPu6rbFFSkmCcSKhViVNYwP9y5IwVo9wiQ 19 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2019", 7 | "dom" 8 | ], 9 | "jsx": "react", 10 | "strict": true, 11 | "moduleResolution": "node", 12 | "allowSyntheticDefaultImports": true, 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "skipLibCheck": true, 16 | "resolveJsonModule": true 17 | } 18 | } 19 | --------------------------------------------------------------------------------