initial commit

This commit is contained in:
Ksenia Lebedeva 2022-11-16 21:28:43 +01:00
commit 9d3b9a096c
35 changed files with 6452 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
out/
node_modules/
dist/

21
LICENSE Normal file
View File

@ -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.

28
README.md Normal file
View File

@ -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

387
build/config.gypi Normal file
View File

@ -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"
}
}

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

426
css/app.css Normal file
View File

@ -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;
}

BIN
img/app_info_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

BIN
img/smartcard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
img/smartcard_error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
img/smartcard_install.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
img/smartcard_success.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
img/smartcard_unpair.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
img/smartcard_update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
img/smartcard_verify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

61
index.html Normal file
View File

@ -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>

9
layouts/error.html Normal file
View File

@ -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>

16
layouts/pairing.html Normal file
View File

@ -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>

22
layouts/verify-pin.html Normal file
View File

@ -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>

28
layouts/verify-puk.html Normal file
View File

@ -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>

10
layouts/waiting.html Normal file
View File

@ -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>

4608
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
package.json Normal file
View File

@ -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"
}
}

4
src/app.ts Normal file
View File

@ -0,0 +1,4 @@
import { app, BrowserWindow } from 'electron';
import { Main } from './main';
Main.main(app, BrowserWindow);

302
src/card.ts Normal file
View File

@ -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());
}
}

56
src/ident.ts Normal file
View File

@ -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;
}
}

42
src/main.ts Normal file
View File

@ -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);
}
}

22
src/pair.ts Normal file
View File

@ -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();
});
}
}

31
src/pin.ts Normal file
View File

@ -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;
});
}
}

43
src/puk.ts Normal file
View File

@ -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;
});
}
}

100
src/renderer.ts Normal file
View File

@ -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();

50
src/session-info.ts Normal file
View File

@ -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;
}
}

106
src/ui.ts Normal file
View File

@ -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 = "";
}
}

26
src/utils.ts Normal file
View File

@ -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;
}
}

10
tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"outDir": "./out",
"rootDir": "./src"
}
}