initial commit
|
@ -0,0 +1,3 @@
|
|||
out/
|
||||
node_modules/
|
||||
dist/
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 Ksenia Balistreri
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,28 @@
|
|||
# Keycard Desktop
|
||||
|
||||
Keycard Desktop is an application to manage your Keycard. Here are some screenshots of its functionality.
|
||||
|
||||
![alt text](https://user-images.githubusercontent.com/66014759/87177882-06774300-c2e5-11ea-90f2-806a089530dd.png "Keycard Desktop 1")
|
||||
![alt text](https://user-images.githubusercontent.com/66014759/86470208-9d6f5880-bd43-11ea-8ead-edc3cf7fa1ed.png "Keycard Desktop 2")
|
||||
![alt text](https://user-images.githubusercontent.com/66014759/86470209-9ea08580-bd43-11ea-9516-d728f4ffe709.png "Keycard Desktop 3")
|
||||
|
||||
## Installing Keycard Desktop
|
||||
|
||||
[Download the latest version](https://github.com/choppu/keycard-desktop/releases) of Keycard Desktop from the github release page. Please note, that the binaries are unsigned. Alternativelly, you can compile Keycard Desktop for your computer from the command line.
|
||||
|
||||
`npm run dist`
|
||||
|
||||
## Functionality
|
||||
|
||||
- [x] Show Card Details
|
||||
- [x] Manage Pairings
|
||||
- [x] Change Credentials
|
||||
- [x] Manage Wallet
|
||||
- [x] Get Ethereum Address with QR Code and link to Etherscan
|
||||
- [x] Reinstall Keycard Applet
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Sign Transactions, providing an RPC Interface for other programs
|
||||
- [ ] Basic Wallet Functionality
|
||||
- [ ] Autoupdate
|
|
@ -0,0 +1,387 @@
|
|||
# Do not edit. File was generated by node-gyp's "configure" step
|
||||
{
|
||||
"target_defaults": {
|
||||
"cflags": [],
|
||||
"default_configuration": "Release",
|
||||
"defines": [],
|
||||
"include_dirs": [],
|
||||
"libraries": [],
|
||||
"msbuild_toolset": "v141",
|
||||
"msvs_windows_target_platform_version": "10.0.17763.0"
|
||||
},
|
||||
"variables": {
|
||||
"asan": 0,
|
||||
"coverage": "false",
|
||||
"dcheck_always_on": 0,
|
||||
"debug_nghttp2": "false",
|
||||
"debug_node": "false",
|
||||
"enable_lto": "false",
|
||||
"enable_pgo_generate": "false",
|
||||
"enable_pgo_use": "false",
|
||||
"error_on_warn": "false",
|
||||
"force_dynamic_crt": 0,
|
||||
"host_arch": "x64",
|
||||
"icu_data_in": "..\\..\\deps\\icu-tmp\\icudt71l.dat",
|
||||
"icu_endianness": "l",
|
||||
"icu_gyp_path": "tools/icu/icu-generic.gyp",
|
||||
"icu_path": "deps/icu-small",
|
||||
"icu_small": "false",
|
||||
"icu_ver_major": "71",
|
||||
"is_debug": 0,
|
||||
"libdir": "lib",
|
||||
"llvm_version": "0.0",
|
||||
"napi_build_version": "8",
|
||||
"nasm_version": "2.15",
|
||||
"node_byteorder": "little",
|
||||
"node_debug_lib": "false",
|
||||
"node_enable_d8": "false",
|
||||
"node_fipsinstall": "false",
|
||||
"node_install_corepack": "true",
|
||||
"node_install_npm": "true",
|
||||
"node_library_files": [
|
||||
"lib/_http_agent.js",
|
||||
"lib/_http_client.js",
|
||||
"lib/_http_common.js",
|
||||
"lib/_http_incoming.js",
|
||||
"lib/_http_outgoing.js",
|
||||
"lib/_http_server.js",
|
||||
"lib/_stream_duplex.js",
|
||||
"lib/_stream_passthrough.js",
|
||||
"lib/_stream_readable.js",
|
||||
"lib/_stream_transform.js",
|
||||
"lib/_stream_wrap.js",
|
||||
"lib/_stream_writable.js",
|
||||
"lib/_tls_common.js",
|
||||
"lib/_tls_wrap.js",
|
||||
"lib/assert.js",
|
||||
"lib/assert/strict.js",
|
||||
"lib/async_hooks.js",
|
||||
"lib/buffer.js",
|
||||
"lib/child_process.js",
|
||||
"lib/cluster.js",
|
||||
"lib/console.js",
|
||||
"lib/constants.js",
|
||||
"lib/crypto.js",
|
||||
"lib/dgram.js",
|
||||
"lib/diagnostics_channel.js",
|
||||
"lib/dns.js",
|
||||
"lib/dns/promises.js",
|
||||
"lib/domain.js",
|
||||
"lib/events.js",
|
||||
"lib/fs.js",
|
||||
"lib/fs/promises.js",
|
||||
"lib/http.js",
|
||||
"lib/http2.js",
|
||||
"lib/https.js",
|
||||
"lib/inspector.js",
|
||||
"lib/internal/abort_controller.js",
|
||||
"lib/internal/assert.js",
|
||||
"lib/internal/assert/assertion_error.js",
|
||||
"lib/internal/assert/calltracker.js",
|
||||
"lib/internal/assert/snapshot.js",
|
||||
"lib/internal/async_hooks.js",
|
||||
"lib/internal/blob.js",
|
||||
"lib/internal/blocklist.js",
|
||||
"lib/internal/bootstrap/browser.js",
|
||||
"lib/internal/bootstrap/loaders.js",
|
||||
"lib/internal/bootstrap/node.js",
|
||||
"lib/internal/bootstrap/switches/does_not_own_process_state.js",
|
||||
"lib/internal/bootstrap/switches/does_own_process_state.js",
|
||||
"lib/internal/bootstrap/switches/is_main_thread.js",
|
||||
"lib/internal/bootstrap/switches/is_not_main_thread.js",
|
||||
"lib/internal/buffer.js",
|
||||
"lib/internal/child_process.js",
|
||||
"lib/internal/child_process/serialization.js",
|
||||
"lib/internal/cli_table.js",
|
||||
"lib/internal/cluster/child.js",
|
||||
"lib/internal/cluster/primary.js",
|
||||
"lib/internal/cluster/round_robin_handle.js",
|
||||
"lib/internal/cluster/shared_handle.js",
|
||||
"lib/internal/cluster/utils.js",
|
||||
"lib/internal/cluster/worker.js",
|
||||
"lib/internal/console/constructor.js",
|
||||
"lib/internal/console/global.js",
|
||||
"lib/internal/constants.js",
|
||||
"lib/internal/crypto/aes.js",
|
||||
"lib/internal/crypto/certificate.js",
|
||||
"lib/internal/crypto/cfrg.js",
|
||||
"lib/internal/crypto/cipher.js",
|
||||
"lib/internal/crypto/diffiehellman.js",
|
||||
"lib/internal/crypto/ec.js",
|
||||
"lib/internal/crypto/hash.js",
|
||||
"lib/internal/crypto/hashnames.js",
|
||||
"lib/internal/crypto/hkdf.js",
|
||||
"lib/internal/crypto/keygen.js",
|
||||
"lib/internal/crypto/keys.js",
|
||||
"lib/internal/crypto/mac.js",
|
||||
"lib/internal/crypto/pbkdf2.js",
|
||||
"lib/internal/crypto/random.js",
|
||||
"lib/internal/crypto/rsa.js",
|
||||
"lib/internal/crypto/scrypt.js",
|
||||
"lib/internal/crypto/sig.js",
|
||||
"lib/internal/crypto/util.js",
|
||||
"lib/internal/crypto/webcrypto.js",
|
||||
"lib/internal/crypto/x509.js",
|
||||
"lib/internal/debugger/inspect.js",
|
||||
"lib/internal/debugger/inspect_client.js",
|
||||
"lib/internal/debugger/inspect_repl.js",
|
||||
"lib/internal/dgram.js",
|
||||
"lib/internal/dns/callback_resolver.js",
|
||||
"lib/internal/dns/promises.js",
|
||||
"lib/internal/dns/utils.js",
|
||||
"lib/internal/dtrace.js",
|
||||
"lib/internal/encoding.js",
|
||||
"lib/internal/error_serdes.js",
|
||||
"lib/internal/errors.js",
|
||||
"lib/internal/event_target.js",
|
||||
"lib/internal/fixed_queue.js",
|
||||
"lib/internal/freelist.js",
|
||||
"lib/internal/freeze_intrinsics.js",
|
||||
"lib/internal/fs/cp/cp-sync.js",
|
||||
"lib/internal/fs/cp/cp.js",
|
||||
"lib/internal/fs/dir.js",
|
||||
"lib/internal/fs/promises.js",
|
||||
"lib/internal/fs/read_file_context.js",
|
||||
"lib/internal/fs/rimraf.js",
|
||||
"lib/internal/fs/streams.js",
|
||||
"lib/internal/fs/sync_write_stream.js",
|
||||
"lib/internal/fs/utils.js",
|
||||
"lib/internal/fs/watchers.js",
|
||||
"lib/internal/heap_utils.js",
|
||||
"lib/internal/histogram.js",
|
||||
"lib/internal/http.js",
|
||||
"lib/internal/http2/compat.js",
|
||||
"lib/internal/http2/core.js",
|
||||
"lib/internal/http2/util.js",
|
||||
"lib/internal/idna.js",
|
||||
"lib/internal/inspector_async_hook.js",
|
||||
"lib/internal/js_stream_socket.js",
|
||||
"lib/internal/legacy/processbinding.js",
|
||||
"lib/internal/linkedlist.js",
|
||||
"lib/internal/main/check_syntax.js",
|
||||
"lib/internal/main/environment.js",
|
||||
"lib/internal/main/eval_stdin.js",
|
||||
"lib/internal/main/eval_string.js",
|
||||
"lib/internal/main/inspect.js",
|
||||
"lib/internal/main/mksnapshot.js",
|
||||
"lib/internal/main/print_help.js",
|
||||
"lib/internal/main/prof_process.js",
|
||||
"lib/internal/main/repl.js",
|
||||
"lib/internal/main/run_main_module.js",
|
||||
"lib/internal/main/test_runner.js",
|
||||
"lib/internal/main/watch_mode.js",
|
||||
"lib/internal/main/worker_thread.js",
|
||||
"lib/internal/modules/cjs/helpers.js",
|
||||
"lib/internal/modules/cjs/loader.js",
|
||||
"lib/internal/modules/esm/assert.js",
|
||||
"lib/internal/modules/esm/create_dynamic_module.js",
|
||||
"lib/internal/modules/esm/fetch_module.js",
|
||||
"lib/internal/modules/esm/formats.js",
|
||||
"lib/internal/modules/esm/get_format.js",
|
||||
"lib/internal/modules/esm/handle_process_exit.js",
|
||||
"lib/internal/modules/esm/initialize_import_meta.js",
|
||||
"lib/internal/modules/esm/load.js",
|
||||
"lib/internal/modules/esm/loader.js",
|
||||
"lib/internal/modules/esm/module_job.js",
|
||||
"lib/internal/modules/esm/module_map.js",
|
||||
"lib/internal/modules/esm/package_config.js",
|
||||
"lib/internal/modules/esm/resolve.js",
|
||||
"lib/internal/modules/esm/translators.js",
|
||||
"lib/internal/modules/package_json_reader.js",
|
||||
"lib/internal/modules/run_main.js",
|
||||
"lib/internal/net.js",
|
||||
"lib/internal/options.js",
|
||||
"lib/internal/per_context/domexception.js",
|
||||
"lib/internal/per_context/messageport.js",
|
||||
"lib/internal/per_context/primordials.js",
|
||||
"lib/internal/perf/event_loop_delay.js",
|
||||
"lib/internal/perf/event_loop_utilization.js",
|
||||
"lib/internal/perf/nodetiming.js",
|
||||
"lib/internal/perf/observe.js",
|
||||
"lib/internal/perf/performance.js",
|
||||
"lib/internal/perf/performance_entry.js",
|
||||
"lib/internal/perf/resource_timing.js",
|
||||
"lib/internal/perf/timerify.js",
|
||||
"lib/internal/perf/usertiming.js",
|
||||
"lib/internal/perf/utils.js",
|
||||
"lib/internal/policy/manifest.js",
|
||||
"lib/internal/policy/sri.js",
|
||||
"lib/internal/priority_queue.js",
|
||||
"lib/internal/process/esm_loader.js",
|
||||
"lib/internal/process/execution.js",
|
||||
"lib/internal/process/per_thread.js",
|
||||
"lib/internal/process/policy.js",
|
||||
"lib/internal/process/pre_execution.js",
|
||||
"lib/internal/process/promises.js",
|
||||
"lib/internal/process/report.js",
|
||||
"lib/internal/process/signal.js",
|
||||
"lib/internal/process/task_queues.js",
|
||||
"lib/internal/process/warning.js",
|
||||
"lib/internal/process/worker_thread_only.js",
|
||||
"lib/internal/promise_hooks.js",
|
||||
"lib/internal/querystring.js",
|
||||
"lib/internal/readline/callbacks.js",
|
||||
"lib/internal/readline/emitKeypressEvents.js",
|
||||
"lib/internal/readline/interface.js",
|
||||
"lib/internal/readline/promises.js",
|
||||
"lib/internal/readline/utils.js",
|
||||
"lib/internal/repl.js",
|
||||
"lib/internal/repl/await.js",
|
||||
"lib/internal/repl/history.js",
|
||||
"lib/internal/repl/utils.js",
|
||||
"lib/internal/socket_list.js",
|
||||
"lib/internal/socketaddress.js",
|
||||
"lib/internal/source_map/prepare_stack_trace.js",
|
||||
"lib/internal/source_map/source_map.js",
|
||||
"lib/internal/source_map/source_map_cache.js",
|
||||
"lib/internal/stream_base_commons.js",
|
||||
"lib/internal/streams/add-abort-signal.js",
|
||||
"lib/internal/streams/buffer_list.js",
|
||||
"lib/internal/streams/compose.js",
|
||||
"lib/internal/streams/destroy.js",
|
||||
"lib/internal/streams/duplex.js",
|
||||
"lib/internal/streams/duplexify.js",
|
||||
"lib/internal/streams/end-of-stream.js",
|
||||
"lib/internal/streams/from.js",
|
||||
"lib/internal/streams/lazy_transform.js",
|
||||
"lib/internal/streams/legacy.js",
|
||||
"lib/internal/streams/operators.js",
|
||||
"lib/internal/streams/passthrough.js",
|
||||
"lib/internal/streams/pipeline.js",
|
||||
"lib/internal/streams/readable.js",
|
||||
"lib/internal/streams/state.js",
|
||||
"lib/internal/streams/transform.js",
|
||||
"lib/internal/streams/utils.js",
|
||||
"lib/internal/streams/writable.js",
|
||||
"lib/internal/structured_clone.js",
|
||||
"lib/internal/test/binding.js",
|
||||
"lib/internal/test/transfer.js",
|
||||
"lib/internal/test_runner/harness.js",
|
||||
"lib/internal/test_runner/runner.js",
|
||||
"lib/internal/test_runner/tap_stream.js",
|
||||
"lib/internal/test_runner/test.js",
|
||||
"lib/internal/test_runner/utils.js",
|
||||
"lib/internal/timers.js",
|
||||
"lib/internal/tls/secure-context.js",
|
||||
"lib/internal/tls/secure-pair.js",
|
||||
"lib/internal/trace_events_async_hooks.js",
|
||||
"lib/internal/tty.js",
|
||||
"lib/internal/url.js",
|
||||
"lib/internal/util.js",
|
||||
"lib/internal/util/colors.js",
|
||||
"lib/internal/util/comparisons.js",
|
||||
"lib/internal/util/debuglog.js",
|
||||
"lib/internal/util/inspect.js",
|
||||
"lib/internal/util/inspector.js",
|
||||
"lib/internal/util/iterable_weak_map.js",
|
||||
"lib/internal/util/parse_args/parse_args.js",
|
||||
"lib/internal/util/parse_args/utils.js",
|
||||
"lib/internal/util/types.js",
|
||||
"lib/internal/v8/startup_snapshot.js",
|
||||
"lib/internal/v8_prof_polyfill.js",
|
||||
"lib/internal/v8_prof_processor.js",
|
||||
"lib/internal/validators.js",
|
||||
"lib/internal/vm/module.js",
|
||||
"lib/internal/wasm_web_api.js",
|
||||
"lib/internal/watch_mode/files_watcher.js",
|
||||
"lib/internal/watchdog.js",
|
||||
"lib/internal/webstreams/adapters.js",
|
||||
"lib/internal/webstreams/compression.js",
|
||||
"lib/internal/webstreams/encoding.js",
|
||||
"lib/internal/webstreams/queuingstrategies.js",
|
||||
"lib/internal/webstreams/readablestream.js",
|
||||
"lib/internal/webstreams/transfer.js",
|
||||
"lib/internal/webstreams/transformstream.js",
|
||||
"lib/internal/webstreams/util.js",
|
||||
"lib/internal/webstreams/writablestream.js",
|
||||
"lib/internal/worker.js",
|
||||
"lib/internal/worker/io.js",
|
||||
"lib/internal/worker/js_transferable.js",
|
||||
"lib/module.js",
|
||||
"lib/net.js",
|
||||
"lib/os.js",
|
||||
"lib/path.js",
|
||||
"lib/path/posix.js",
|
||||
"lib/path/win32.js",
|
||||
"lib/perf_hooks.js",
|
||||
"lib/process.js",
|
||||
"lib/punycode.js",
|
||||
"lib/querystring.js",
|
||||
"lib/readline.js",
|
||||
"lib/readline/promises.js",
|
||||
"lib/repl.js",
|
||||
"lib/stream.js",
|
||||
"lib/stream/consumers.js",
|
||||
"lib/stream/promises.js",
|
||||
"lib/stream/web.js",
|
||||
"lib/string_decoder.js",
|
||||
"lib/sys.js",
|
||||
"lib/test.js",
|
||||
"lib/timers.js",
|
||||
"lib/timers/promises.js",
|
||||
"lib/tls.js",
|
||||
"lib/trace_events.js",
|
||||
"lib/tty.js",
|
||||
"lib/url.js",
|
||||
"lib/util.js",
|
||||
"lib/util/types.js",
|
||||
"lib/v8.js",
|
||||
"lib/vm.js",
|
||||
"lib/wasi.js",
|
||||
"lib/worker_threads.js",
|
||||
"lib/zlib.js"
|
||||
],
|
||||
"node_module_version": 108,
|
||||
"node_no_browser_globals": "false",
|
||||
"node_prefix": "/usr/local",
|
||||
"node_release_urlbase": "https://nodejs.org/download/release/",
|
||||
"node_shared": "false",
|
||||
"node_shared_brotli": "false",
|
||||
"node_shared_cares": "false",
|
||||
"node_shared_http_parser": "false",
|
||||
"node_shared_libuv": "false",
|
||||
"node_shared_nghttp2": "false",
|
||||
"node_shared_nghttp3": "false",
|
||||
"node_shared_ngtcp2": "false",
|
||||
"node_shared_openssl": "false",
|
||||
"node_shared_zlib": "false",
|
||||
"node_tag": "",
|
||||
"node_target_type": "executable",
|
||||
"node_use_bundled_v8": "true",
|
||||
"node_use_dtrace": "false",
|
||||
"node_use_etw": "true",
|
||||
"node_use_node_code_cache": "true",
|
||||
"node_use_node_snapshot": "true",
|
||||
"node_use_openssl": "true",
|
||||
"node_use_v8_platform": "true",
|
||||
"node_with_ltcg": "true",
|
||||
"node_without_node_options": "false",
|
||||
"openssl_is_fips": "false",
|
||||
"openssl_quic": "true",
|
||||
"ossfuzz": "false",
|
||||
"shlib_suffix": "so.108",
|
||||
"target_arch": "x64",
|
||||
"v8_enable_31bit_smis_on_64bit_arch": 0,
|
||||
"v8_enable_gdbjit": 0,
|
||||
"v8_enable_hugepage": 0,
|
||||
"v8_enable_i18n_support": 1,
|
||||
"v8_enable_inspector": 1,
|
||||
"v8_enable_javascript_promise_hooks": 1,
|
||||
"v8_enable_lite_mode": 0,
|
||||
"v8_enable_object_print": 1,
|
||||
"v8_enable_pointer_compression": 0,
|
||||
"v8_enable_shared_ro_heap": 1,
|
||||
"v8_enable_short_builtin_calls": 1,
|
||||
"v8_enable_webassembly": 1,
|
||||
"v8_no_strict_aliasing": 1,
|
||||
"v8_optimized_debug": 1,
|
||||
"v8_promise_internal_field_count": 1,
|
||||
"v8_random_seed": 0,
|
||||
"v8_trace_maps": 0,
|
||||
"v8_use_siphash": 1,
|
||||
"want_separate_host_toolset": 0,
|
||||
"nodedir": "C:\\Users\\SigMilles\\AppData\\Local\\node-gyp\\Cache\\18.12.1",
|
||||
"standalone_static_library": 1,
|
||||
"msbuild_path": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2017\\BuildTools\\MSBuild\\15.0\\Bin\\MSBuild.exe"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 88 KiB |
|
@ -0,0 +1,426 @@
|
|||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 62.5%;
|
||||
font-size: 1em;
|
||||
color: rgb(55, 55, 55);
|
||||
line-height: 1.3em;
|
||||
font-family: 'Cutive Mono', monospace;
|
||||
font-weight: 300;
|
||||
background: rgb(15, 17, 22);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input[disabled="disabled"], input[disabled="disabled"]:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.group:before, .group:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
.group:after {
|
||||
clear: both;
|
||||
}
|
||||
.group {
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background-color: rgb(15, 17, 22);
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:window-inactive,
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgb(60, 77, 75);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:horizontal {
|
||||
background-color: rgb(15, 17, 22);
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.keycard__main-container {
|
||||
background: rgb(15, 17, 22);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: white;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
display: inherit;
|
||||
animation: fade 300ms;
|
||||
}
|
||||
|
||||
.keycard__card-info-container-hidden {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
-webkit-transition: opacity 600ms, visibility 600ms;
|
||||
transition: opacity 200ms, visibility 200ms;
|
||||
}
|
||||
|
||||
.keycard__pairing-container {
|
||||
color: white;
|
||||
text-align: center;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
display: inherit;
|
||||
animation: fade 300ms;
|
||||
}
|
||||
|
||||
.keycard__card-info-container-message {
|
||||
text-align: center;
|
||||
padding: 8em 1em;
|
||||
}
|
||||
|
||||
.keycard__card-messages {
|
||||
padding: 1.5em;
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
font-size: .9em;
|
||||
color:#6f85e4;
|
||||
overflow: auto;
|
||||
max-height: 47vh;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.keycard__card-info-container-element {
|
||||
display: block;
|
||||
padding: .2em 2em;
|
||||
}
|
||||
|
||||
.keycard__ident-container {
|
||||
height: 60%;
|
||||
box-sizing: border-box;
|
||||
padding: 2em 0;
|
||||
}
|
||||
|
||||
.keycard__card-info-container {
|
||||
height: 40%;
|
||||
box-sizing: border-box;
|
||||
background: #1c2030;
|
||||
overflow-y: scroll;
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.keycard__ident-form-container {
|
||||
width: 80%;
|
||||
box-sizing: border-box;
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.keycard__ident-form-field-container {
|
||||
padding: .8em 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.keycard__ident-form-field-label {
|
||||
float: left;
|
||||
width: 37%;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
padding-top: .5em;
|
||||
}
|
||||
|
||||
.keycard-ident__enc-file-label {
|
||||
padding-top: 2em;
|
||||
}
|
||||
|
||||
.keycard__inp-right {
|
||||
float: right;
|
||||
width: 63%;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: solid 1px white;
|
||||
outline: none;
|
||||
font-family: 'Cutive Mono', monospace;
|
||||
color: white;
|
||||
font-size: 1em;
|
||||
padding: .5em 0;
|
||||
}
|
||||
|
||||
.keycard__img-card-container {
|
||||
width: 20%;
|
||||
box-sizing: border-box;
|
||||
margin: 4em auto;
|
||||
}
|
||||
|
||||
.keycard__img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.keycard__pairing-message {
|
||||
font-size: 1.5em;
|
||||
line-height: 2em;
|
||||
font-style: bold;
|
||||
font-family: 'Inconsolata', monospace;
|
||||
padding: 0 1em;
|
||||
color: white;
|
||||
transition: opacity 1s;
|
||||
}
|
||||
|
||||
.keycard__pairing-input-container {
|
||||
text-align: left;
|
||||
width: 60%;
|
||||
box-sizing: border-box;
|
||||
margin: 3em auto;
|
||||
}
|
||||
|
||||
.keycard__pairing-input-field {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.keycard__pairing-input-label {
|
||||
float: left;
|
||||
width: 50%;
|
||||
padding: 1em 0;
|
||||
font-size: 1.4em;
|
||||
}
|
||||
|
||||
.keycard__pairing-input {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid white;
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
float: right;
|
||||
width: 45%;
|
||||
outline: none;
|
||||
padding: .3em .5em;
|
||||
box-sizing: border-box;
|
||||
font-size: 1.4em;
|
||||
font-family: 'Cutive Mono', monospace;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.keycard__btn-container {
|
||||
text-align: center;
|
||||
padding: 2em 0;
|
||||
}
|
||||
|
||||
.keycard__btn {
|
||||
background: #6f85e4;
|
||||
padding: .8em 3em;
|
||||
box-shadow: 0 3px 3px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
border: none;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
font-family: 'Cutive Mono', monospace;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.2em;
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.keycard__loading {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
margin: 4em auto;
|
||||
}
|
||||
.keycard__loading div {
|
||||
position: absolute;
|
||||
border: 4px solid rgb(112, 112, 191);
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: keycard__loading 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
.keycard__loading div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
@keyframes keycard__loading {
|
||||
0% {
|
||||
top: 72px;
|
||||
left: 72px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 144px;
|
||||
height: 144px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.keycard__ident-form-enc-field-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.keycard__ident-file-field {
|
||||
float: left;
|
||||
margin: .8em 0;
|
||||
position: relative;
|
||||
width: 170px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.keycard__ident-file-label {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
color: rgb(15, 17, 22);
|
||||
padding: .6em 0;
|
||||
font-family: 'Cutive Mono', monospace;
|
||||
font-size: .9em;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 3px 3px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.keycard__ident-file-input {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.keycard__file-path-label {
|
||||
display: block;
|
||||
padding-top: .4em;
|
||||
overflow-x: scroll;
|
||||
width: 200%;
|
||||
}
|
||||
|
||||
.keycard__check {
|
||||
padding: 0;
|
||||
height: initial;
|
||||
width: initial;
|
||||
margin-bottom: 0;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.keycard__check-label {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-right: 2.5em;
|
||||
}
|
||||
|
||||
.keycard__check-label:before {
|
||||
content:'';
|
||||
-webkit-appearance: none;
|
||||
background-color: transparent;
|
||||
border: 2px solid rgb(72, 151, 127);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), inset 0px -15px 10px -12px rgba(0, 0, 0, 0.05);
|
||||
padding: .56em;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
margin-right: .8em;
|
||||
}
|
||||
|
||||
.keycard__check:checked + .keycard__check-label:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: .1em;
|
||||
left: .5em;
|
||||
width: .25em;
|
||||
height: .8em;
|
||||
border: solid rgb(72, 151, 127);
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.keycard-ident__destination-path-container {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.keycard-ident__select-btn {
|
||||
font-family: "Material Symbols Outlined", monospace;
|
||||
margin-top: .4em;
|
||||
float: right;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), inset 0px -15px 10px -12px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.keycard-ident__destination-path{
|
||||
border-bottom: solid 1px white;
|
||||
float: left;
|
||||
width: 55%;
|
||||
padding: .5em 0;
|
||||
font-size: 1em;
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
}
|
After Width: | Height: | Size: 596 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,61 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Keycard Desktop</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cutive+Mono&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@48,400,0,0" />
|
||||
<link rel="stylesheet" type="text/css" href="css/app.css">
|
||||
</head>
|
||||
|
||||
<body class="keycard__body-container">
|
||||
<div class="keycard__main-container group" id="main-container">
|
||||
<div class="keycard__ident-container">
|
||||
<div class="keycard__ident-form-container" id="ident-form">
|
||||
<div class="keycard__ident-form">
|
||||
<div class="keycard__ident-form-field-container group">
|
||||
<label for="lot-number" class="keycard__ident-form-field-label">Lot number</label>
|
||||
<input type="text" class="keycard__ident-form-field-input-text keycard__inp-right" id="lot-number" name="lot-number" placeholder="Lot number" required>
|
||||
</div>
|
||||
<div class="keycard__ident-form-field-container group">
|
||||
<label for="card-quantity" class="keycard__ident-form-field-label">Number of cards</label>
|
||||
<input type="number" class="keycard__ident-form-field-input-number keycard__inp-right" id="card-quantity" name="card-quantity" value="1" required>
|
||||
</div>
|
||||
<div class="keycard-ident__destination-path-container keycard__ident-form-field-container group">
|
||||
<span class="keycard__ident-form-field-label">Destination file</span>
|
||||
<div class="group">
|
||||
<span class="keycard-ident__destination-path" id="show-destination-path">No destination file selected</span>
|
||||
<button id="destination-path" class="keycard-ident__select-btn material-symbols-outlined">folder_open</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="keycard__ident-form-field-container keycard__ident-form-enc-field-container group">
|
||||
<label for="encryption-key" class="keycard__ident-form-field-label keycard-ident__enc-file-label">Output encryption key</label>
|
||||
<div class="keycard__ident-file-field keycard__inp-right group">
|
||||
<div class="keycard__file-inp-left">
|
||||
<input type="file" class="keycard__ident-file-input" accept=".asc" id="encryption-key" name="encryption-key" required>
|
||||
<span class="keycard__ident-file-label">
|
||||
Choose File
|
||||
</span>
|
||||
</div>
|
||||
<span id="file-enc-path-label" class="keycard__file-path-label">No file selected</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="keycard__ident-start-btn-container keycard__btn-container">
|
||||
<button class="keycard__ident-start-btn keycard__btn" id="start-btn" disabled>Start</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="keycard__card-info-container">
|
||||
<div class="keycard__card-messages" id="keycard-log-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="keycard__card-info-container-hidden" id="cmd-layout-container"></div>
|
||||
</body>
|
||||
<script>
|
||||
require('./out/renderer.js');
|
||||
</script>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
<section id="keycard__error-layout">
|
||||
<div class="keycard__img-card-container">
|
||||
<img src="./img/smartcard_error.png" alt="Smartcard" class="keycard__img">
|
||||
</div>
|
||||
<p class="keycard__pairing-message" id="error-message"></p>
|
||||
<div class="keycard__btn-container">
|
||||
<input type="submit" id="btn-error" class="keycard__btn" value="OK">
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,16 @@
|
|||
<section id="keycard__pairing-layout">
|
||||
<div class="keycard__img-card-container">
|
||||
<img src="./img/smartcard.png" alt="Smartcard" class="keycard__img">
|
||||
</div>
|
||||
<p class="keycard__pairing-message">There is no pairing saved. Please pair your card.</p>
|
||||
<div class="keycard__pairing-input-container">
|
||||
<div class="keycard__pairing-input-field group">
|
||||
<label for="pairing" class="keycard__pairing-input-label">Pairing password</label>
|
||||
<input type="password" id="pairing" class="keycard__pairing-input" name="pairing" required placeholder="********">
|
||||
</div>
|
||||
<div class="keycard__btn-container">
|
||||
<input type="submit" id="pair-btn" class="keycard__btn" value="Pair">
|
||||
<input type="submit" id="pair-cancel-btn" class="keycard__btn" value="Cancel">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,22 @@
|
|||
<section id="keycard__verify-pin-layout">
|
||||
<div class="keycard__img-card-container">
|
||||
<img src="./img/smartcard_verify.png" alt="Smartcard" class="keycard__img">
|
||||
</div>
|
||||
<p class="keycard__pairing-message">Please insert your pin. <span id="pin-retry-form"></span> tries left.</p>
|
||||
<div class="keycard__pairing-input-container">
|
||||
<div class="keycard__pairing-input-field group">
|
||||
<label for="verify-pin" class="keycard__pairing-input-label">PIN</label>
|
||||
<input type="password" id="verify-pin-inp" class="keycard__pairing-input" name="verify-pin" size="6" minlength="6"
|
||||
maxlength="6" required placeholder="******">
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<div class="keycard__btn-container">
|
||||
<input type="submit" id="verify-pin-btn" class="keycard__btn" value="Verify PIN" disabled="disabled">
|
||||
<input type="button" id="verify-pin-cancel" class="keycard__btn" value="Cancel">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,28 @@
|
|||
<section id="keycard__verify-puk-layout">
|
||||
<div class="keycard__img-card-container">
|
||||
<img src="./img/smartcard_verify.png" alt="Smartcard" class="keycard__img">
|
||||
</div>
|
||||
<p class="keycard__pairing-message">Too many invalid PIN attempts. Your PIN retry count is 0. Please insert your PUK.
|
||||
<span id="puk-retry-form"></span> tries left.</p>
|
||||
<div class="keycard__pairing-input-container">
|
||||
<div class="keycard__pairing-input-field group">
|
||||
<label for="verify-puk" class="keycard__pairing-input-label">PUK</label>
|
||||
<input type="password" id="verify-puk-inp" class="keycard__pairing-input keycard__verify-puk-el" name="verify-puk"
|
||||
size="12" minlength="12" maxlength="12" required placeholder="************">
|
||||
</div>
|
||||
<div class="keycard__pairing-input-field group">
|
||||
<label for="new-pin" class="keycard__pairing-input-label">New PIN</label>
|
||||
<input type="password" id="new-pin" class="keycard__pairing-input keycard__verify-puk-el" name="new-pin" size="6"
|
||||
minlength="6" maxlength="6" required placeholder="******">
|
||||
</div>
|
||||
<div class="keycard__pairing-input-field group">
|
||||
<label for="repeat-new-pin" class="keycard__pairing-input-label">Repeat PIN</label>
|
||||
<input type="password" id="repeat-new-pin" class="keycard__pairing-input keycard__verify-puk-el"
|
||||
name="repeat-new-pin" size="6" minlength="6" maxlength="6" required placeholder="******">
|
||||
</div>
|
||||
</div>
|
||||
<div class="keycard__btn-container">
|
||||
<input type="submit" id="verify-puk-btn" class="keycard__btn" value="Verify PUK" disabled="disabled">
|
||||
<input type="button" id="verify-puk-cancel" class="keycard__btn" value="Cancel">
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,10 @@
|
|||
<section id="keycard__create-mnemonic-layout">
|
||||
<div class="keycard__img-card-container">
|
||||
<img src="./img/smartcard.png" alt="Smartcard" class="keycard__img">
|
||||
</div>
|
||||
<p class="keycard__pairing-message" id="waiting-message"></p>
|
||||
<div class="keycard__loading">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</section>
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "keycard-certify",
|
||||
"version": "1.0.0",
|
||||
"description": "Keycard Ident Certificate Creation Application",
|
||||
"main": "./out/app.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "npm run build && electron ./out/app.js",
|
||||
"pack": "npm run build && electron-builder --dir",
|
||||
"dist": "npm run build && electron-builder",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.github.choppu.keycard-desktop",
|
||||
"productName": "Keycard Desktop",
|
||||
"files": [
|
||||
"**/*",
|
||||
"!tsconfig.json",
|
||||
"!README.md",
|
||||
"!src"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.utilities",
|
||||
"identity": null,
|
||||
"target": "zip"
|
||||
}
|
||||
},
|
||||
"author": "Ksenia Balistreri",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pokusew/pcsclite": "^0.6.0",
|
||||
"electron-store": "^8.1.0",
|
||||
"keycard-sdk": "github:choppu/keycard-sdk#ident",
|
||||
"openpgp": "^5.5.0",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^21.2.3",
|
||||
"electron-builder": "^24.0.0-alpha.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { app, BrowserWindow } from 'electron';
|
||||
import { Main } from './main';
|
||||
|
||||
Main.main(app, BrowserWindow);
|
|
@ -0,0 +1,302 @@
|
|||
import Keycard from "keycard-sdk"
|
||||
import { WebContents } from "electron";
|
||||
import { ipcMain, dialog } from "electron"
|
||||
import { SessionInfo } from "./session-info";
|
||||
import { Utils } from "./utils";
|
||||
import { Pairing } from "keycard-sdk/dist/pairing";
|
||||
import { Commandset } from "keycard-sdk/dist/commandset";
|
||||
import { KeyPath } from "keycard-sdk/dist/key-path";
|
||||
import { CardChannel } from "keycard-sdk/dist/card-channel";
|
||||
import { BIP32KeyPair } from "keycard-sdk/dist/bip32key";
|
||||
import { Certificate } from "keycard-sdk/dist/certificate";
|
||||
import { CryptoUtils } from "keycard-sdk/dist/crypto-utils";
|
||||
|
||||
const pcsclite = require("@pokusew/pcsclite");
|
||||
const Store = require('electron-store');
|
||||
const fs = require("fs");
|
||||
const openpgp = require('openpgp');
|
||||
const path = require('path');
|
||||
|
||||
const maxPINRetryCount = 3;
|
||||
const maxPUKRetryCount = 5;
|
||||
const maxPairing = 5;
|
||||
const dataHeader = "80e2000082";
|
||||
|
||||
export class Card {
|
||||
window: WebContents;
|
||||
channel?: CardChannel;
|
||||
cmdSet?: Commandset;
|
||||
sessionInfo: SessionInfo;
|
||||
pairingStore: any;
|
||||
|
||||
constructor(window: WebContents) {
|
||||
this.window = window;
|
||||
this.pairingStore = new Store();
|
||||
this.sessionInfo = new SessionInfo();
|
||||
this.installEventHandlers();
|
||||
}
|
||||
|
||||
savePairing(instanceUID: Uint8Array, pairing: string): void {
|
||||
this.pairingStore.set(Utils.hx(instanceUID), pairing);
|
||||
}
|
||||
|
||||
loadPairing(instanceUID: Uint8Array): string {
|
||||
return this.pairingStore.get(Utils.hx(instanceUID));
|
||||
}
|
||||
|
||||
isPaired(instanceUID: Uint8Array): boolean {
|
||||
return this.pairingStore.has(Utils.hx(instanceUID));
|
||||
}
|
||||
|
||||
deletePairing(instanceUID: Uint8Array): void {
|
||||
this.pairingStore.delete(Utils.hx(instanceUID));
|
||||
}
|
||||
|
||||
async connectCard(reader: any, protocol: number): Promise<void> {
|
||||
try {
|
||||
this.channel = new Keycard.PCSCCardChannel(reader, protocol);
|
||||
this.cmdSet = new Keycard.Commandset(this.channel);
|
||||
this.window.send('card-connected');
|
||||
} catch (err: any) {
|
||||
if (err.sw == 0x6a82) {
|
||||
this.window.send("card-exceptions", "Error: Keycard Applet not installed");
|
||||
} else {
|
||||
this.window.send("card-exceptions", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async openSecureChannel(): Promise<void> {
|
||||
(await this.cmdSet!.select()).checkOK();
|
||||
this.sessionInfo.secureChannelOpened = false;
|
||||
|
||||
while (!this.sessionInfo.secureChannelOpened) {
|
||||
try {
|
||||
if (!(await this.pairCard())) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.cmdSet!.autoOpenSecureChannel();
|
||||
this.window.send("secure-channel");
|
||||
this.sessionInfo.secureChannelOpened = true;
|
||||
} catch (err) {
|
||||
this.deletePairing(this.cmdSet!.applicationInfo.instanceUID);
|
||||
}
|
||||
}
|
||||
|
||||
await this.displayData();
|
||||
this.window.send("secure-channel-opened");
|
||||
this.window.send("disable-open-secure-channel");
|
||||
}
|
||||
|
||||
pairCard(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let pairingInfo: string;
|
||||
let instanceUID = this.cmdSet!.applicationInfo.instanceUID;
|
||||
|
||||
|
||||
if (this.isPaired(instanceUID)) {
|
||||
pairingInfo = this.loadPairing(instanceUID);
|
||||
this.cmdSet!.setPairing(Pairing.fromString(pairingInfo));
|
||||
this.window.send("pairing-found");
|
||||
resolve(true);
|
||||
} else {
|
||||
this.window.send("pairing-needed");
|
||||
ipcMain.once("pairing-pass-submitted", async (_, pairingPassword: string) => {
|
||||
if (pairingPassword) {
|
||||
try {
|
||||
await this.cmdSet!.autoPair(pairingPassword);
|
||||
} catch {
|
||||
reject("Error: invalid password");
|
||||
return;
|
||||
}
|
||||
(await this.cmdSet!.select()).checkOK();
|
||||
this.savePairing(this.cmdSet!.applicationInfo.instanceUID, this.cmdSet!.getPairing().toBase64());
|
||||
this.window.send("paired");
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async displayData(): Promise<void> {
|
||||
let status = new Keycard.ApplicationStatus((await this.cmdSet!.getStatus(Keycard.Constants.GET_STATUS_P1_APPLICATION)).checkOK().data);
|
||||
let path = new KeyPath((await this.cmdSet!.getStatus(Keycard.Constants.GET_STATUS_P1_KEY_PATH)).checkOK().data);
|
||||
this.sessionInfo.keyPath = path.toString();
|
||||
this.sessionInfo.setApplicationInfo(this.cmdSet!.applicationInfo);
|
||||
this.sessionInfo.setApplicationStatus(status);
|
||||
this.window.send('application-info', this.sessionInfo);
|
||||
this.window.send("enable-pin-verification");
|
||||
}
|
||||
|
||||
async verifyPIN(pin: string): Promise<void> {
|
||||
try {
|
||||
(await this.cmdSet!.verifyPIN(pin)).checkAuthOK();
|
||||
this.sessionInfo.pinRetry = maxPINRetryCount;
|
||||
this.sessionInfo.pinVerified = true;
|
||||
this.window.send('application-info', this.sessionInfo);
|
||||
this.window.send("pin-verified");
|
||||
this.window.send("verification-success");
|
||||
} catch (err: any) {
|
||||
if (err.retryAttempts != undefined) {
|
||||
this.sessionInfo.pinRetry = err.retryAttempts;
|
||||
this.window.send('application-info', this.sessionInfo);
|
||||
|
||||
if (err.retryAttempts > 0) {
|
||||
this.window.send("pin-screen-needed");
|
||||
} else {
|
||||
this.window.send("puk-screen-needed");
|
||||
this.window.send("pin-verification-failed", err.message);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPUK(puk: string, newPin: string): Promise<void> {
|
||||
try {
|
||||
(await this.cmdSet!.unblockPIN(puk, newPin)).checkOK();
|
||||
this.sessionInfo.pinRetry = maxPINRetryCount;
|
||||
this.sessionInfo.pukRetry = maxPUKRetryCount;
|
||||
this.sessionInfo.pinVerified = true;
|
||||
this.window.send('application-info', this.sessionInfo);
|
||||
this.window.send("puk-verified");
|
||||
this.window.send("pin-verified");
|
||||
this.window.send("verification-success");
|
||||
} catch (err) {
|
||||
this.sessionInfo.pukRetry = (typeof this.sessionInfo.pukRetry == "number") ? (this.sessionInfo.pukRetry--) : this.sessionInfo.pukRetry;
|
||||
this.window.send('application-info', this.sessionInfo);
|
||||
if (this.sessionInfo.pukRetry > 0) {
|
||||
this.window.send("puk-screen-needed");
|
||||
} else {
|
||||
this.window.send("unblock-pin-failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async identCert(gpgKey: string, lot: string, cardQuantity: string, destPath: string): Promise<void> {
|
||||
let cards = parseInt(cardQuantity);
|
||||
let data = (await this.cmdSet!.exportKey(1, false, "m/43'/60'/1581'/2'/0")).checkOK().data;
|
||||
let caKey = BIP32KeyPair.fromTLV(data);
|
||||
let encKey = fs.readFileSync(gpgKey, { encoding: 'utf8', flag: 'r' });
|
||||
let encData = "";
|
||||
|
||||
this.window.send("pub-key", Buffer.from(CryptoUtils.compressPublicKey(caKey.publicKey)).toString('hex'));
|
||||
|
||||
for (let i = 0; i < cards; i++) {
|
||||
let certificate = Certificate.generateNewCertificate(caKey);
|
||||
let certData = certificate.toStoreData();
|
||||
let num = Utils.formatNumtoString(i);
|
||||
let cardID = lot + num;
|
||||
let certDataString = dataHeader + Buffer.from(certData).toString('hex');
|
||||
let line = cardID + "," + certDataString + "\n";
|
||||
encData += line;
|
||||
}
|
||||
|
||||
let encryptedData = await openpgp.encrypt({
|
||||
message: await openpgp.createMessage({ text: encData }),
|
||||
encryptionKeys: await openpgp.readKey({ armoredKey: encKey }),
|
||||
});
|
||||
|
||||
fs.writeFileSync(destPath, encryptedData);
|
||||
|
||||
this.window.send("certificate-creation-success");
|
||||
}
|
||||
|
||||
openDestinationDialog() : void {
|
||||
let options = {
|
||||
title: 'Select the destination path to save the processed file',
|
||||
buttonLabel: "Choose",
|
||||
defaultPath: path.join(__dirname, 'certificates.csv.asc'),
|
||||
filters: [
|
||||
{
|
||||
name: 'ASC Files',
|
||||
extensions: ['csv.asc']
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
dialog.showSaveDialog(options).then((path) => {
|
||||
this.window.send("destination-path-selected", path.filePath);
|
||||
}).catch((err) => {
|
||||
throw(err);
|
||||
});
|
||||
}
|
||||
|
||||
resetConnection(): void {
|
||||
this.sessionInfo.reset();
|
||||
this.window.send("application-info", this.sessionInfo);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
let pcsc = pcsclite();
|
||||
let card = this;
|
||||
|
||||
pcsc.on('reader', (reader: any) => {
|
||||
card.window.send('card-detected', reader.name);
|
||||
|
||||
reader.on('error', function (err: Error) {
|
||||
card.window.send('card-detected', reader.name, err.message);
|
||||
});
|
||||
|
||||
reader.on('status', (status: any) => {
|
||||
let changes = reader.state ^ status.state;
|
||||
|
||||
if (!changes) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((changes & reader.SCARD_STATE_EMPTY) && (status.state & reader.SCARD_STATE_EMPTY)) {
|
||||
if (card.sessionInfo.cardConnected) {
|
||||
card.window.send('card-removed', reader.name);
|
||||
card.resetConnection();
|
||||
reader.disconnect(reader.SCARD_LEAVE_CARD, (_: Error) => { });
|
||||
}
|
||||
|
||||
} else if ((changes & reader.SCARD_STATE_PRESENT) && (status.state & reader.SCARD_STATE_PRESENT)) {
|
||||
reader.connect({ share_mode: reader.SCARD_SHARE_EXCLUSIVE }, async function (err: Error, protocol: number) {
|
||||
card.sessionInfo.cardConnected = true;
|
||||
if (err) {
|
||||
card.window.send('card-connection-err', err.message);
|
||||
return;
|
||||
}
|
||||
card.connectCard(reader, protocol);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
reader.on('end', () => {
|
||||
if (card.sessionInfo.cardConnected) {
|
||||
card.window.send('reader-removed', reader.name);
|
||||
card.resetConnection();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
withErrorHandler(fn: (...args: any) => Promise<void>): (ev: Event) => void {
|
||||
return async (_: Event, ...args: any) => {
|
||||
try {
|
||||
await fn.call(this, ...args);
|
||||
} catch (err: any) {
|
||||
this.window.send("card-exceptions", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installEventHandlers(): void {
|
||||
ipcMain.on("open-secure-channel", this.withErrorHandler(this.openSecureChannel));
|
||||
ipcMain.on("verify-pin", this.withErrorHandler(this.verifyPIN));
|
||||
ipcMain.on("verify-puk", this.withErrorHandler(this.verifyPUK));
|
||||
ipcMain.on("start-ident", this.withErrorHandler(this.identCert));
|
||||
ipcMain.on("open-destination-folder-dialog", (_) => this.openDestinationDialog());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import { UI } from "./ui";
|
||||
|
||||
export namespace Ident {
|
||||
export function initUI() : void {
|
||||
let filePath: string | undefined;
|
||||
let destinationPath: string;
|
||||
let fileLabel = document.getElementById("file-enc-path-label");
|
||||
let fileField = document.getElementById("encryption-key") as HTMLInputElement;
|
||||
let lot = document.getElementById("lot-number") as HTMLInputElement;
|
||||
let cardQuantity = document.getElementById("card-quantity") as HTMLInputElement;
|
||||
let startBtn = document.getElementById("start-btn") as HTMLInputElement;
|
||||
let destinationPathBtn = document.getElementById("destination-path") as HTMLInputElement;
|
||||
let destinationPathLabel = document.getElementById("show-destination-path") as HTMLElement;
|
||||
|
||||
fileField!.addEventListener("change", (e) => {
|
||||
let target = e.target as HTMLInputElement;
|
||||
filePath = target?.files![0] ? target.files[0].path : undefined;
|
||||
fileLabel!.innerHTML = filePath ? filePath : "No file selected";
|
||||
filePath && lot.value && cardQuantity.value ? startBtn.removeAttribute("disabled") : startBtn.setAttribute("disabled", "disabled");
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
lot.addEventListener("input", (e) => {
|
||||
filePath && lot.value && cardQuantity.value ? startBtn.removeAttribute("disabled") : startBtn.setAttribute("disabled", "disabled");
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
cardQuantity.addEventListener("input", (e) => {
|
||||
filePath && lot.value && cardQuantity.value ? startBtn.removeAttribute("disabled") : startBtn.setAttribute("disabled", "disabled");
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
destinationPathBtn.addEventListener("click", (e) => {
|
||||
ipcRenderer.send("open-destination-folder-dialog");
|
||||
});
|
||||
|
||||
ipcRenderer.on("verification-success", (_) => {
|
||||
ipcRenderer.send("start-ident", filePath, lot.value, cardQuantity.value, destinationPathLabel.innerHTML);
|
||||
|
||||
lot.value = "";
|
||||
cardQuantity.value = "";
|
||||
fileLabel!.innerHTML = "No file selected";
|
||||
destinationPathLabel.innerHTML = "No destination file selected"
|
||||
|
||||
UI.loadFragment("waiting.html", () => {
|
||||
document.getElementById("waiting-message")!.innerHTML = "Certificate generation in progress";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function setDestinationPath(path: string) : void {
|
||||
let destinationPathLabel = document.getElementById("show-destination-path");
|
||||
destinationPathLabel!.innerHTML = path;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { Card } from "./card"
|
||||
|
||||
export namespace Main {
|
||||
let mainWindow: Electron.BrowserWindow;
|
||||
let application: Electron.App;
|
||||
let BrowserWindow: any;
|
||||
let card: Card;
|
||||
|
||||
export function onWindowAllClosed() {
|
||||
application.quit();
|
||||
}
|
||||
|
||||
export function onClose(): void {
|
||||
mainWindow.destroy();
|
||||
}
|
||||
|
||||
export function onReady(): void {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1100, height: 850, minWidth: 1000, minHeight: 845, maximizable: true, webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
enableRemoteModule: true,
|
||||
}
|
||||
});
|
||||
mainWindow.removeMenu();
|
||||
mainWindow.loadFile(`${__dirname}/../index.html`);
|
||||
card = new Card(mainWindow.webContents);
|
||||
mainWindow.webContents.once("dom-ready", () => {
|
||||
card.start();
|
||||
});
|
||||
mainWindow.webContents.openDevTools();
|
||||
mainWindow.on('closed', Main.onClose);
|
||||
}
|
||||
|
||||
export function main(app: Electron.App, browserWindow: typeof BrowserWindow): void {
|
||||
BrowserWindow = browserWindow;
|
||||
application = app;
|
||||
application.setName("Keycard Desktop");
|
||||
application.on('window-all-closed', Main.onWindowAllClosed);
|
||||
application.on('ready', Main.onReady);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import { UI } from "./ui";
|
||||
|
||||
export namespace Pair {
|
||||
export function pair() : void {
|
||||
let pairingField = document.getElementById("pairing") as HTMLInputElement;
|
||||
let button = document.getElementById("pair-btn");
|
||||
let cancelBtn = document.getElementById("pair-cancel-btn");
|
||||
|
||||
button!.addEventListener("click", function (e) {
|
||||
ipcRenderer.send("pairing-pass-submitted", pairingField.value);
|
||||
UI.unloadFragment();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
cancelBtn!.addEventListener("click", (e) => {
|
||||
ipcRenderer.send("pairing-pass-submitted", null);
|
||||
UI.unloadFragment();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { UI, cardInfo } from "./ui";
|
||||
import { Utils } from "./utils";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
export namespace PIN {
|
||||
export function verifyPIN() : void {
|
||||
let pin = document.getElementById("verify-pin-inp") as HTMLInputElement;
|
||||
let submitBtn = document.getElementById("verify-pin-btn") as HTMLInputElement;
|
||||
let cancelBtn = document.getElementById("verify-pin-cancel");
|
||||
let pinRetryMess = document.getElementById("pin-retry-form");
|
||||
|
||||
pinRetryMess!.innerHTML = `${cardInfo.pinRetry}`;
|
||||
|
||||
pin.addEventListener("input", (e) => {
|
||||
Utils.checkInputNumericValue(pin.value, 6) ? submitBtn.removeAttribute("disabled") : submitBtn.setAttribute("disabled", "disabled");
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
submitBtn?.addEventListener("click", (e) => {
|
||||
ipcRenderer.send("verify-pin", pin.value);
|
||||
UI.unloadFragment();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
cancelBtn?.addEventListener("click", (e) => {
|
||||
pin.value = "";
|
||||
UI.unloadFragment();
|
||||
e.preventDefault;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import { UI, cardInfo } from "./ui";
|
||||
import { Utils } from "./utils";
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
export namespace PUK {
|
||||
export function verifyPUK() : void {
|
||||
let verifyPUKInputs = document.getElementsByClassName("keycard__verify-puk-el");
|
||||
let puk = document.getElementById("verify-puk-inp") as HTMLInputElement;
|
||||
let newPIN = document.getElementById("new-pin") as HTMLInputElement;
|
||||
let repeatPIN = document.getElementById("repeat-new-pin") as HTMLInputElement;
|
||||
let submitBtn = document.getElementById("verify-puk-btn") as HTMLInputElement;
|
||||
let cancelBtn = document.getElementById("verify-puk-cancel");
|
||||
let pukRetryMess = document.getElementById("puk-retry-form");
|
||||
|
||||
pukRetryMess!.innerHTML = `${cardInfo.pukRetry}`;
|
||||
|
||||
for(let i = 0; i < verifyPUKInputs.length; i++) {
|
||||
verifyPUKInputs[i] as HTMLInputElement;
|
||||
verifyPUKInputs[i].addEventListener("input", (e) => {
|
||||
if(Utils.checkInputNumericValue(puk.value, 12) && Utils.checkInputNumericValue(newPIN.value, 6) && Utils.checkInputNumericValue(repeatPIN.value, 6) && (newPIN.value == repeatPIN.value)) {
|
||||
submitBtn.removeAttribute("disabled");
|
||||
} else {
|
||||
submitBtn.setAttribute("disabled", "disabled");
|
||||
}
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
submitBtn?.addEventListener("click", (e) => {
|
||||
ipcRenderer.send("verify-puk", puk.value, newPIN.value);
|
||||
UI.unloadFragment();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
cancelBtn?.addEventListener("click", (e) => {
|
||||
puk.value = "";
|
||||
newPIN.value = "";
|
||||
repeatPIN.value = "";
|
||||
UI.unloadFragment();
|
||||
e.preventDefault;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
import { UI } from "./ui";
|
||||
import { Pair } from "./pair";
|
||||
import { PUK } from "./puk";
|
||||
import { PIN } from "./pin";
|
||||
import { Ident } from "./ident";
|
||||
|
||||
const { ipcRenderer } = require('electron');
|
||||
let defaultPass = true;
|
||||
|
||||
export function updateLogMessage(event: string, msg: string): void {
|
||||
ipcRenderer.on(event, (_) => {
|
||||
UI.addMessageToLog(msg);
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on("card-removed", (_, readerName) => {
|
||||
UI.unloadFragment();
|
||||
UI.addMessageToLog(`Card has been removed from ${readerName}`);
|
||||
});
|
||||
|
||||
ipcRenderer.on('reader-removed', (_, readerName) => {
|
||||
UI.unloadFragment();
|
||||
UI.addMessageToLog(`Reader ${readerName} removed`);
|
||||
});
|
||||
|
||||
ipcRenderer.on('card-detected', (_, readerName, err?) => {
|
||||
err ? UI.addMessageToLog(`Error ${readerName}: ${err}`) : UI.addMessageToLog(`New reader ${readerName} detected`);
|
||||
});
|
||||
|
||||
ipcRenderer.on("card-connection-err", (_, err) => {
|
||||
UI.addMessageToLog(`Error connecting to the card: ${err}`);
|
||||
});
|
||||
|
||||
ipcRenderer.on("application-info", function (_, sessionInfo) {
|
||||
UI.saveCardInfo(sessionInfo);
|
||||
});
|
||||
|
||||
ipcRenderer.on("card-exceptions", function (_, err) {
|
||||
UI.loadErrorFragment(err);
|
||||
});
|
||||
|
||||
ipcRenderer.on("pin-screen-needed", (_) => {
|
||||
UI.loadFragment('verify-pin.html', PIN.verifyPIN);
|
||||
});
|
||||
|
||||
ipcRenderer.on("puk-screen-needed", (_) => {
|
||||
UI.loadFragment('verify-puk.html', PUK.verifyPUK);
|
||||
});
|
||||
|
||||
ipcRenderer.on("pin-verified", (_) => {
|
||||
UI.addMessageToLog("PIN verified");
|
||||
});
|
||||
|
||||
ipcRenderer.on('pin-verification-failed', (_, msg) => {
|
||||
UI.addMessageToLog(msg);
|
||||
});
|
||||
|
||||
ipcRenderer.on("certificate-creation-success", (_) => {
|
||||
UI.unloadFragment();
|
||||
UI.addMessageToLog("Certificate generation finished");
|
||||
UI.unloadFragment();
|
||||
});
|
||||
|
||||
ipcRenderer.on("pub-key", (_, key) => {
|
||||
UI.addMessageToLog(`CA public key: ${key}`);
|
||||
});
|
||||
|
||||
ipcRenderer.on("pairing-needed", (_, defpair: boolean) => {
|
||||
UI.addMessageToLog("No pairing found");
|
||||
if(defaultPass) {
|
||||
defaultPass = false;
|
||||
ipcRenderer.send("pairing-pass-submitted", "KeycardDefaultPairing");
|
||||
} else {
|
||||
UI.loadFragment('pairing.html', Pair.pair);
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on("secure-channel-opened", (_) => {
|
||||
UI.renderVerifyPinLayout('verify-pin.html', 'verify-puk.html', PIN.verifyPIN, PUK.verifyPUK);
|
||||
});
|
||||
|
||||
ipcRenderer.on("destination-path-selected", (_, path) => {
|
||||
Ident.setDestinationPath(path);
|
||||
});
|
||||
|
||||
updateLogMessage('card-connected', "Selecting Keycard Wallet");
|
||||
updateLogMessage('pairing-found', "Pairing found");
|
||||
updateLogMessage('secure-channel', "Secure Channel opened");
|
||||
updateLogMessage('paired', "Paired successfully");
|
||||
updateLogMessage('puk-verified', "PIN unblocked successfully");
|
||||
updateLogMessage('unblock-pin-failed', "PUK tries exceeded. The card has been blocked. Please re-install the applet.");
|
||||
|
||||
document.getElementById("start-btn")?.addEventListener("click", (e) => {
|
||||
ipcRenderer.send("open-secure-channel");
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
Ident.initUI();
|
||||
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { ApplicationInfo } from "keycard-sdk/dist/application-info";
|
||||
import { Utils } from "./utils";
|
||||
import { ApplicationStatus } from "keycard-sdk/dist/application-status";
|
||||
|
||||
export class SessionInfo {
|
||||
cashAddress!: string;
|
||||
instanceUID!: string;
|
||||
appVersion!: string;
|
||||
pairingSlots!: string;
|
||||
keyUID!: string;
|
||||
keyPath!: string;
|
||||
pinRetry!: number | string;
|
||||
pukRetry!: number | string;
|
||||
hasMasterKey!: boolean;
|
||||
secureChannelOpened!: boolean;
|
||||
pinVerified!: boolean;
|
||||
cardConnected!: boolean;
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
setApplicationInfo(appInfo: ApplicationInfo) {
|
||||
this.instanceUID = Utils.hx(appInfo.instanceUID);
|
||||
this.appVersion = appInfo.getAppVersionString();
|
||||
this.pairingSlots = appInfo.freePairingSlots.toString();
|
||||
this.keyUID = Utils.hx(appInfo.keyUID);
|
||||
this.hasMasterKey = appInfo.hasMasterKey();
|
||||
}
|
||||
|
||||
setApplicationStatus(appStatus: ApplicationStatus) {
|
||||
this.pinRetry = appStatus.pinRetryCount;
|
||||
this.pukRetry = appStatus.pukRetryCount;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.cashAddress = "";
|
||||
this.instanceUID = "";
|
||||
this.appVersion = "";
|
||||
this.pairingSlots = "";
|
||||
this.keyUID = "";
|
||||
this.pinRetry = "No data available";
|
||||
this.pukRetry = "No data available";
|
||||
this.keyPath = "";
|
||||
this.hasMasterKey = false;
|
||||
this.secureChannelOpened = false;
|
||||
this.pinVerified = false;
|
||||
this.cardConnected = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import { Ident } from "./ident";
|
||||
import { SessionInfo } from "./session-info";
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
export let cardInfo: SessionInfo;
|
||||
|
||||
export namespace UI {
|
||||
export const mainContainer = document.getElementById("main-container");
|
||||
export const appInfoContainer = document.getElementById("keycard__card-info");
|
||||
export const layoutContainer = document.getElementById("cmd-layout-container");
|
||||
|
||||
const btns = document.getElementsByClassName("keycard__cmd-disabled");
|
||||
|
||||
export function saveCardInfo(appInfo: SessionInfo) : void {
|
||||
cardInfo = appInfo;
|
||||
}
|
||||
|
||||
export function addMessageToLog(mess: string): void {
|
||||
let logContainer = document.getElementById('keycard-log-container');
|
||||
let message = document.createElement("p");
|
||||
let date = new Date().toLocaleTimeString();
|
||||
|
||||
if (logContainer) {
|
||||
logContainer.appendChild(message);
|
||||
message.classList.add("keycard__card-info-container-message-text");
|
||||
message.innerHTML = `${date}: ${mess}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderAppInfo(): void {
|
||||
let msg = document.getElementById("no-card-detected-msg");
|
||||
}
|
||||
|
||||
export function renderCmdScreenLayout(btn: HTMLElement, layoutPath: string, onLoad: () => void) : void {
|
||||
btn.addEventListener("click", (e) => {
|
||||
loadFragment(layoutPath, onLoad);
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
export function renderVerifyPinLayout(layoutPin: string, layoutPuk: string, pinFunc: () => void, pukFunc: () => void) : void {
|
||||
cardInfo.pinRetry > 0 ? loadFragment(layoutPin, pinFunc) : loadFragment(layoutPuk, pukFunc);
|
||||
}
|
||||
|
||||
export function loadFragment(filename: string, onLoad: () => void) : void {
|
||||
let path = `${__dirname}/../layouts/${filename}`;
|
||||
layoutContainer!.innerHTML = "";
|
||||
|
||||
mainContainer?.classList.add("keycard__card-info-container-hidden");
|
||||
mainContainer?.classList.remove("keycard__main-container");
|
||||
layoutContainer?.classList.remove("keycard__card-info-container-hidden");
|
||||
layoutContainer?.classList.add("keycard__pairing-container");
|
||||
|
||||
fs.readFile(path, (_: Error, layout: string) => {
|
||||
layoutContainer!.innerHTML = layout;
|
||||
onLoad();
|
||||
});
|
||||
}
|
||||
|
||||
export function unloadFragment(): void {
|
||||
let startBtn = document.getElementById("start-btn") as HTMLInputElement;
|
||||
|
||||
layoutContainer!.innerHTML = "";
|
||||
layoutContainer?.classList.add("keycard__card-info-container-hidden");
|
||||
layoutContainer?.classList.remove("keycard__pairing-container");
|
||||
mainContainer?.classList.remove("keycard__card-info-container-hidden");
|
||||
mainContainer?.classList.add("keycard__main-container");
|
||||
startBtn.setAttribute("disabled", "disabled");
|
||||
}
|
||||
|
||||
export function loadErrorFragment(err: Error): void {
|
||||
loadFragment('error.html', () => {
|
||||
let errorMessage = document.getElementById("error-message");
|
||||
|
||||
errorMessage!.innerHTML = `${err}`;
|
||||
|
||||
document.getElementById("btn-error")?.addEventListener("click", function (e) {
|
||||
UI.unloadFragment();
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function renderErrorMess(errMessage: string, messField: HTMLElement) : void {
|
||||
messField.innerHTML = errMessage;
|
||||
setTimeout(() => {
|
||||
messField.innerHTML = "";
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
export function renderNoAppInfo() : void {
|
||||
let header = document.getElementById("app-info-header");
|
||||
header!.innerHTML = "No card connected";
|
||||
header!.classList.remove("keycard__app-info-header");
|
||||
header!.classList.add("keycard__card-info-container-message");
|
||||
document.getElementById("cash-address")!.innerHTML = "";
|
||||
document.getElementById("instance-uid")!.innerHTML = "";
|
||||
document.getElementById("app-version")!.innerHTML = "";
|
||||
document.getElementById("pairing-slots")!.innerHTML = "";
|
||||
document.getElementById("pin-retry")!.innerHTML = "";
|
||||
document.getElementById("puk-retry")!.innerHTML = "";
|
||||
document.getElementById("key-uid")!.innerHTML = "";
|
||||
document.getElementById("key-path")!.innerHTML = "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
export namespace Utils {
|
||||
const numStrLength = 6;
|
||||
|
||||
export function hx(arr: Uint8Array): string {
|
||||
return Buffer.from(arr).toString('hex');
|
||||
}
|
||||
|
||||
export function checkInputNumericValue(value: string, len: number) : boolean {
|
||||
if(value.length == len) {
|
||||
return value.split("").every((c) => '0123456789'.includes(c));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function formatNumtoString(num: number) : string {
|
||||
let res = num.toString();
|
||||
|
||||
for(let i = res.length; res.length < numStrLength; i++) {
|
||||
let zero = "0";
|
||||
res = zero + res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"outDir": "./out",
|
||||
"rootDir": "./src"
|
||||
}
|
||||
}
|