@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"window-state": "patch"
|
||||||
|
---
|
||||||
|
|
||||||
|
Address a couple of issues with restoring positions:
|
||||||
|
|
||||||
|
- Fix restoring window positions correctly when the top-left corner of the window was outside of the monitor.
|
||||||
|
- Fix restore maximization state only maximized on main monitor.
|
@ -1,4 +1,3 @@
|
|||||||
target
|
target
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
|
||||||
dist-js
|
dist-js
|
@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||||
|
/>
|
||||||
|
<title>Svelte + Vite App</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,8 @@
|
|||||||
|
Copyright (c) 2017
|
||||||
|
|
||||||
|
Licensed under either of
|
||||||
|
|
||||||
|
* Apache License, Version 2.0, (http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* MIT license (http://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
at your option.
|
@ -0,0 +1,66 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use bytes::{Buf, BufMut};
|
||||||
|
use openssl::sha::sha256;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use crate::u2f_crate::u2ferror::U2fError;
|
||||||
|
|
||||||
|
/// The `Result` type used in this crate.
|
||||||
|
type Result<T> = ::std::result::Result<T, U2fError>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Authorization {
|
||||||
|
pub counter: u32,
|
||||||
|
pub user_presence: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_sign_response(
|
||||||
|
app_id: String,
|
||||||
|
client_data: Vec<u8>,
|
||||||
|
public_key: Vec<u8>,
|
||||||
|
sign_data: Vec<u8>,
|
||||||
|
) -> Result<Authorization> {
|
||||||
|
if sign_data.len() <= 5 {
|
||||||
|
return Err(U2fError::InvalidSignatureData);
|
||||||
|
}
|
||||||
|
|
||||||
|
let user_presence_flag = &sign_data[0];
|
||||||
|
let counter = &sign_data[1..=4];
|
||||||
|
let signature = &sign_data[5..];
|
||||||
|
|
||||||
|
// Let's build the msg to verify the signature
|
||||||
|
let app_id_hash = sha256(&app_id.into_bytes());
|
||||||
|
let client_data_hash = sha256(&client_data[..]);
|
||||||
|
|
||||||
|
let mut msg = vec![];
|
||||||
|
msg.put(app_id_hash.as_ref());
|
||||||
|
msg.put_u8(*user_presence_flag);
|
||||||
|
msg.put(counter);
|
||||||
|
msg.put(client_data_hash.as_ref());
|
||||||
|
|
||||||
|
let public_key = super::crypto::NISTP256Key::from_bytes(&public_key)?;
|
||||||
|
|
||||||
|
// The signature is to be verified by the relying party using the public key obtained during registration.
|
||||||
|
let verified = public_key.verify_signature(signature, msg.as_ref())?;
|
||||||
|
if !verified {
|
||||||
|
return Err(U2fError::BadSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
let authorization = Authorization {
|
||||||
|
counter: get_counter(counter),
|
||||||
|
user_presence: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(authorization)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_counter(counter: &[u8]) -> u32 {
|
||||||
|
let mut buf = Cursor::new(counter);
|
||||||
|
buf.get_u32()
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
//! Cryptographic operation wrapper for Webauthn. This module exists to
|
||||||
|
//! allow ease of auditing, safe operation wrappers for the webauthn library,
|
||||||
|
//! and cryptographic provider abstraction. This module currently uses OpenSSL
|
||||||
|
//! as the cryptographic primitive provider.
|
||||||
|
|
||||||
|
// Source can be found here: https://github.com/Firstyear/webauthn-rs/blob/master/src/crypto.rs
|
||||||
|
|
||||||
|
#![allow(non_camel_case_types)]
|
||||||
|
|
||||||
|
use openssl::{bn, ec, hash, nid, sign, x509};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
// use super::constants::*;
|
||||||
|
use crate::u2f_crate::u2ferror::U2fError;
|
||||||
|
use openssl::pkey::Public;
|
||||||
|
|
||||||
|
// use super::proto::*;
|
||||||
|
|
||||||
|
// Why OpenSSL over another rust crate?
|
||||||
|
// - Well, the openssl crate allows us to reconstruct a public key from the
|
||||||
|
// x/y group coords, where most others want a pkcs formatted structure. As
|
||||||
|
// a result, it's easiest to use openssl as it gives us exactly what we need
|
||||||
|
// for these operations, and despite it's many challenges as a library, it
|
||||||
|
// has resources and investment into it's maintenance, so we can a least
|
||||||
|
// assert a higher level of confidence in it that <backyard crypto here>.
|
||||||
|
|
||||||
|
// Object({Integer(-3): Bytes([48, 185, 178, 204, 113, 186, 105, 138, 190, 33, 160, 46, 131, 253, 100, 177, 91, 243, 126, 128, 245, 119, 209, 59, 186, 41, 215, 196, 24, 222, 46, 102]), Integer(-2): Bytes([158, 212, 171, 234, 165, 197, 86, 55, 141, 122, 253, 6, 92, 242, 242, 114, 158, 221, 238, 163, 127, 214, 120, 157, 145, 226, 232, 250, 144, 150, 218, 138]), Integer(-1): U64(1), Integer(1): U64(2), Integer(3): I64(-7)})
|
||||||
|
//
|
||||||
|
|
||||||
|
/// An X509PublicKey. This is what is otherwise known as a public certificate
|
||||||
|
/// which comprises a public key and other signed metadata related to the issuer
|
||||||
|
/// of the key.
|
||||||
|
pub struct X509PublicKey {
|
||||||
|
pubk: x509::X509,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for X509PublicKey {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "X509PublicKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for X509PublicKey {
|
||||||
|
type Error = U2fError;
|
||||||
|
|
||||||
|
// Must be DER bytes. If you have PEM, base64decode first!
|
||||||
|
fn try_from(d: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
let pubk = x509::X509::from_der(d)?;
|
||||||
|
Ok(X509PublicKey { pubk })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl X509PublicKey {
|
||||||
|
pub(crate) fn common_name(&self) -> Option<String> {
|
||||||
|
let cert = &self.pubk;
|
||||||
|
|
||||||
|
let subject = cert.subject_name();
|
||||||
|
let common = subject
|
||||||
|
.entries_by_nid(openssl::nid::Nid::COMMONNAME)
|
||||||
|
.next()
|
||||||
|
.map(|b| b.data().as_slice());
|
||||||
|
|
||||||
|
if let Some(common) = common {
|
||||||
|
std::str::from_utf8(common).ok().map(|s| s.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_secp256r1(&self) -> Result<bool, U2fError> {
|
||||||
|
// Can we get the public key?
|
||||||
|
let pk = self.pubk.public_key()?;
|
||||||
|
|
||||||
|
let ec_key = pk.ec_key()?;
|
||||||
|
|
||||||
|
ec_key.check_key()?;
|
||||||
|
|
||||||
|
let ec_grpref = ec_key.group();
|
||||||
|
|
||||||
|
let ec_curve = ec_grpref.curve_name().ok_or(U2fError::OpenSSLNoCurveName)?;
|
||||||
|
|
||||||
|
Ok(ec_curve == nid::Nid::X9_62_PRIME256V1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn verify_signature(
|
||||||
|
&self,
|
||||||
|
signature: &[u8],
|
||||||
|
verification_data: &[u8],
|
||||||
|
) -> Result<bool, U2fError> {
|
||||||
|
let pkey = self.pubk.public_key()?;
|
||||||
|
|
||||||
|
// TODO: Should this determine the hash type from the x509 cert? Or other?
|
||||||
|
let mut verifier = sign::Verifier::new(hash::MessageDigest::sha256(), &pkey)?;
|
||||||
|
verifier.update(verification_data)?;
|
||||||
|
Ok(verifier.verify(signature)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NISTP256Key {
|
||||||
|
/// The key's public X coordinate.
|
||||||
|
pub x: [u8; 32],
|
||||||
|
/// The key's public Y coordinate.
|
||||||
|
pub y: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NISTP256Key {
|
||||||
|
pub fn from_bytes(public_key_bytes: &[u8]) -> Result<Self, U2fError> {
|
||||||
|
if public_key_bytes.len() != 65 {
|
||||||
|
return Err(U2fError::InvalidPublicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if public_key_bytes[0] != 0x04 {
|
||||||
|
return Err(U2fError::InvalidPublicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut x: [u8; 32] = Default::default();
|
||||||
|
x.copy_from_slice(&public_key_bytes[1..=32]);
|
||||||
|
|
||||||
|
let mut y: [u8; 32] = Default::default();
|
||||||
|
y.copy_from_slice(&public_key_bytes[33..=64]);
|
||||||
|
|
||||||
|
Ok(NISTP256Key { x, y })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key(&self) -> Result<ec::EcKey<Public>, U2fError> {
|
||||||
|
let ec_group = ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?;
|
||||||
|
|
||||||
|
let xbn = bn::BigNum::from_slice(&self.x)?;
|
||||||
|
let ybn = bn::BigNum::from_slice(&self.y)?;
|
||||||
|
|
||||||
|
let ec_key = openssl::ec::EcKey::from_public_key_affine_coordinates(&ec_group, &xbn, &ybn)?;
|
||||||
|
|
||||||
|
// Validate the key is sound. IIRC this actually checks the values
|
||||||
|
// are correctly on the curve as specified
|
||||||
|
ec_key.check_key()?;
|
||||||
|
|
||||||
|
Ok(ec_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_signature(
|
||||||
|
&self,
|
||||||
|
signature: &[u8],
|
||||||
|
verification_data: &[u8],
|
||||||
|
) -> Result<bool, U2fError> {
|
||||||
|
let pkey = self.get_key()?;
|
||||||
|
|
||||||
|
let signature = openssl::ecdsa::EcdsaSig::from_der(signature)?;
|
||||||
|
let hash = openssl::sha::sha256(verification_data);
|
||||||
|
|
||||||
|
Ok(signature.verify(hash.as_ref(), &pkey)?)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// As defined by FIDO U2F Javascript API.
|
||||||
|
// https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-javascript-api.html#registration
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct U2fRegisterRequest {
|
||||||
|
pub app_id: String,
|
||||||
|
pub register_requests: Vec<RegisterRequest>,
|
||||||
|
pub registered_keys: Vec<RegisteredKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
pub version: String,
|
||||||
|
pub challenge: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RegisteredKey {
|
||||||
|
pub version: String,
|
||||||
|
pub key_handle: Option<String>,
|
||||||
|
pub app_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RegisterResponse {
|
||||||
|
pub registration_data: String,
|
||||||
|
pub version: String,
|
||||||
|
pub client_data: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct U2fSignRequest {
|
||||||
|
pub app_id: String,
|
||||||
|
pub challenge: String,
|
||||||
|
pub registered_keys: Vec<RegisteredKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SignResponse {
|
||||||
|
pub key_handle: String,
|
||||||
|
pub signature_data: String,
|
||||||
|
pub client_data: String,
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
pub mod authorization;
|
||||||
|
mod crypto;
|
||||||
|
pub mod messages;
|
||||||
|
pub mod protocol;
|
||||||
|
pub mod register;
|
||||||
|
pub mod u2ferror;
|
@ -0,0 +1,192 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use crate::u2f_crate::authorization::*;
|
||||||
|
use crate::u2f_crate::messages::*;
|
||||||
|
use crate::u2f_crate::register::*;
|
||||||
|
use crate::u2f_crate::u2ferror::U2fError;
|
||||||
|
use crate::u2f_crate::util::*;
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use chrono::Duration;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
type Result<T> = ::std::result::Result<T, U2fError>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct U2f {
|
||||||
|
app_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Challenge {
|
||||||
|
pub app_id: String,
|
||||||
|
pub challenge: String,
|
||||||
|
pub timestamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Challenge {
|
||||||
|
// Not used in this plugin.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Challenge {
|
||||||
|
app_id: String::new(),
|
||||||
|
challenge: String::new(),
|
||||||
|
timestamp: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl U2f {
|
||||||
|
// The app ID is a string used to uniquely identify an U2F app
|
||||||
|
pub fn new(app_id: String) -> Self {
|
||||||
|
U2f { app_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used in this plugin.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn generate_challenge(&self) -> Result<Challenge> {
|
||||||
|
let utc: DateTime<Utc> = Utc::now();
|
||||||
|
|
||||||
|
let challenge_bytes = generate_challenge(32)?;
|
||||||
|
let challenge = Challenge {
|
||||||
|
challenge: URL_SAFE_NO_PAD.encode(challenge_bytes),
|
||||||
|
timestamp: format!("{:?}", utc),
|
||||||
|
app_id: self.app_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(challenge.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used in this plugin.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn request(
|
||||||
|
&self,
|
||||||
|
challenge: Challenge,
|
||||||
|
registrations: Vec<Registration>,
|
||||||
|
) -> Result<U2fRegisterRequest> {
|
||||||
|
let u2f_request = U2fRegisterRequest {
|
||||||
|
app_id: self.app_id.clone(),
|
||||||
|
register_requests: self.register_request(challenge),
|
||||||
|
registered_keys: self.registered_keys(registrations),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(u2f_request)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_request(&self, challenge: Challenge) -> Vec<RegisterRequest> {
|
||||||
|
let mut requests: Vec<RegisterRequest> = vec![];
|
||||||
|
|
||||||
|
let request = RegisterRequest {
|
||||||
|
version: U2F_V2.into(),
|
||||||
|
challenge: challenge.challenge,
|
||||||
|
};
|
||||||
|
requests.push(request);
|
||||||
|
|
||||||
|
requests
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_response(
|
||||||
|
&self,
|
||||||
|
challenge: Challenge,
|
||||||
|
response: RegisterResponse,
|
||||||
|
) -> Result<Registration> {
|
||||||
|
if expiration(challenge.timestamp) > Duration::seconds(300) {
|
||||||
|
return Err(U2fError::ChallengeExpired);
|
||||||
|
}
|
||||||
|
|
||||||
|
let registration_data: Vec<u8> = URL_SAFE_NO_PAD
|
||||||
|
.decode(&response.registration_data[..])
|
||||||
|
.unwrap();
|
||||||
|
let client_data: Vec<u8> = URL_SAFE_NO_PAD.decode(&response.client_data[..]).unwrap();
|
||||||
|
|
||||||
|
parse_registration(challenge.app_id, client_data, registration_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn registered_keys(&self, registrations: Vec<Registration>) -> Vec<RegisteredKey> {
|
||||||
|
let mut keys: Vec<RegisteredKey> = vec![];
|
||||||
|
|
||||||
|
for registration in registrations {
|
||||||
|
keys.push(get_registered_key(
|
||||||
|
self.app_id.clone(),
|
||||||
|
registration.key_handle,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used in this plugin.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn sign_request(
|
||||||
|
&self,
|
||||||
|
challenge: Challenge,
|
||||||
|
registrations: Vec<Registration>,
|
||||||
|
) -> U2fSignRequest {
|
||||||
|
let mut keys: Vec<RegisteredKey> = vec![];
|
||||||
|
|
||||||
|
for registration in registrations {
|
||||||
|
keys.push(get_registered_key(
|
||||||
|
self.app_id.clone(),
|
||||||
|
registration.key_handle,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let signed_request = U2fSignRequest {
|
||||||
|
app_id: self.app_id.clone(),
|
||||||
|
challenge: URL_SAFE_NO_PAD.encode(challenge.challenge.as_bytes()),
|
||||||
|
registered_keys: keys,
|
||||||
|
};
|
||||||
|
|
||||||
|
signed_request
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_response(
|
||||||
|
&self,
|
||||||
|
challenge: Challenge,
|
||||||
|
reg: Registration,
|
||||||
|
sign_resp: SignResponse,
|
||||||
|
counter: u32,
|
||||||
|
) -> Result<u32> {
|
||||||
|
if expiration(challenge.timestamp) > Duration::seconds(300) {
|
||||||
|
return Err(U2fError::ChallengeExpired);
|
||||||
|
}
|
||||||
|
|
||||||
|
if sign_resp.key_handle != get_encoded(®.key_handle[..]) {
|
||||||
|
return Err(U2fError::WrongKeyHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_data: Vec<u8> = URL_SAFE_NO_PAD
|
||||||
|
.decode(&sign_resp.client_data[..])
|
||||||
|
.map_err(|_e| U2fError::InvalidClientData)?;
|
||||||
|
let sign_data: Vec<u8> = URL_SAFE_NO_PAD
|
||||||
|
.decode(&sign_resp.signature_data[..])
|
||||||
|
.map_err(|_e| U2fError::InvalidSignatureData)?;
|
||||||
|
|
||||||
|
let public_key = reg.pub_key;
|
||||||
|
|
||||||
|
let auth = parse_sign_response(
|
||||||
|
self.app_id.clone(),
|
||||||
|
client_data.clone(),
|
||||||
|
public_key,
|
||||||
|
sign_data.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match auth {
|
||||||
|
Ok(ref res) => {
|
||||||
|
// CounterTooLow is raised when the counter value received from the device is
|
||||||
|
// lower than last stored counter value.
|
||||||
|
if res.counter < counter {
|
||||||
|
Err(U2fError::CounterTooLow)
|
||||||
|
} else {
|
||||||
|
Ok(res.counter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
|
use bytes::{BufMut, Bytes};
|
||||||
|
use openssl::sha::sha256;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::u2f_crate::messages::RegisteredKey;
|
||||||
|
use crate::u2f_crate::u2ferror::U2fError;
|
||||||
|
use crate::u2f_crate::util::*;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
/// The `Result` type used in this crate.
|
||||||
|
type Result<T> = ::std::result::Result<T, U2fError>;
|
||||||
|
|
||||||
|
// Single enrolment or pairing between an application and a token.
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Registration {
|
||||||
|
pub key_handle: Vec<u8>,
|
||||||
|
pub pub_key: Vec<u8>,
|
||||||
|
|
||||||
|
// AttestationCert can be null for Authenticate requests.
|
||||||
|
pub attestation_cert: Option<Vec<u8>>,
|
||||||
|
pub device_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_registration(
|
||||||
|
app_id: String,
|
||||||
|
client_data: Vec<u8>,
|
||||||
|
registration_data: Vec<u8>,
|
||||||
|
) -> Result<Registration> {
|
||||||
|
let reserved_byte = registration_data[0];
|
||||||
|
if reserved_byte != 0x05 {
|
||||||
|
return Err(U2fError::InvalidReservedByte);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut mem = Bytes::from(registration_data);
|
||||||
|
|
||||||
|
//Start parsing ... advance the reserved byte.
|
||||||
|
let _ = mem.split_to(1);
|
||||||
|
|
||||||
|
// P-256 NIST elliptic curve
|
||||||
|
let public_key = mem.split_to(65);
|
||||||
|
|
||||||
|
// Key Handle
|
||||||
|
let key_handle_size = mem.split_to(1);
|
||||||
|
let key_len = BigEndian::read_uint(&key_handle_size[..], 1);
|
||||||
|
let key_handle = mem.split_to(key_len as usize);
|
||||||
|
|
||||||
|
// The certificate length needs to be inferred by parsing.
|
||||||
|
let cert_len = asn_length(mem.clone()).unwrap();
|
||||||
|
let attestation_certificate = mem.split_to(cert_len);
|
||||||
|
|
||||||
|
// Remaining data corresponds to the signature
|
||||||
|
let signature = mem;
|
||||||
|
|
||||||
|
// Let's build the msg to verify the signature
|
||||||
|
let app_id_hash = sha256(&app_id.into_bytes());
|
||||||
|
let client_data_hash = sha256(&client_data[..]);
|
||||||
|
|
||||||
|
let mut msg = vec![0x00]; // A byte reserved for future use [1 byte] with the value 0x00
|
||||||
|
msg.put(app_id_hash.as_ref());
|
||||||
|
msg.put(client_data_hash.as_ref());
|
||||||
|
msg.put(key_handle.clone());
|
||||||
|
msg.put(public_key.clone());
|
||||||
|
|
||||||
|
// The signature is to be verified by the relying party using the public key certified
|
||||||
|
// in the attestation certificate.
|
||||||
|
let cerificate_public_key =
|
||||||
|
super::crypto::X509PublicKey::try_from(&attestation_certificate[..])?;
|
||||||
|
|
||||||
|
if !(cerificate_public_key.is_secp256r1()?) {
|
||||||
|
return Err(U2fError::BadCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
let verified = cerificate_public_key.verify_signature(&signature[..], &msg[..])?;
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
return Err(U2fError::BadCertificate);
|
||||||
|
}
|
||||||
|
|
||||||
|
let registration = Registration {
|
||||||
|
key_handle: key_handle[..].to_vec(),
|
||||||
|
pub_key: public_key[..].to_vec(),
|
||||||
|
attestation_cert: Some(attestation_certificate[..].to_vec()),
|
||||||
|
device_name: cerificate_public_key.common_name(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(registration)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_registered_key(app_id: String, key_handle: Vec<u8>) -> RegisteredKey {
|
||||||
|
RegisteredKey {
|
||||||
|
app_id,
|
||||||
|
version: U2F_V2.into(),
|
||||||
|
key_handle: Some(get_encoded(key_handle.as_slice())),
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum U2fError {
|
||||||
|
#[error("ASM1 Decoder error")]
|
||||||
|
Asm1DecoderError,
|
||||||
|
#[error("Not able to verify signature")]
|
||||||
|
BadSignature,
|
||||||
|
#[error("Not able to generate random bytes")]
|
||||||
|
RandomSecureBytesError,
|
||||||
|
#[error("Invalid Reserved Byte")]
|
||||||
|
InvalidReservedByte,
|
||||||
|
#[error("Challenge Expired")]
|
||||||
|
ChallengeExpired,
|
||||||
|
#[error("Wrong Key Handler")]
|
||||||
|
WrongKeyHandler,
|
||||||
|
#[error("Invalid Client Data")]
|
||||||
|
InvalidClientData,
|
||||||
|
#[error("Invalid Signature Data")]
|
||||||
|
InvalidSignatureData,
|
||||||
|
#[error("Invalid User Presence Byte")]
|
||||||
|
InvalidUserPresenceByte,
|
||||||
|
#[error("Failed to parse certificate")]
|
||||||
|
BadCertificate,
|
||||||
|
#[error("Not Trusted Anchor")]
|
||||||
|
NotTrustedAnchor,
|
||||||
|
#[error("Counter too low")]
|
||||||
|
CounterTooLow,
|
||||||
|
#[error("Invalid public key")]
|
||||||
|
OpenSSLNoCurveName,
|
||||||
|
#[error("OpenSSL no curve name")]
|
||||||
|
InvalidPublicKey,
|
||||||
|
#[error(transparent)]
|
||||||
|
OpenSSLError(#[from] openssl::error::ErrorStack),
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2021 Flavio Oliveira
|
||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
use crate::u2f_crate::u2ferror::U2fError;
|
||||||
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||||
|
use bytes::Bytes;
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use chrono::Duration;
|
||||||
|
use openssl::rand;
|
||||||
|
|
||||||
|
/// The `Result` type used in this crate.
|
||||||
|
type Result<T> = ::std::result::Result<T, U2fError>;
|
||||||
|
|
||||||
|
pub const U2F_V2: &str = "U2F_V2";
|
||||||
|
|
||||||
|
// Generates a challenge from a secure, random source.
|
||||||
|
pub fn generate_challenge(size: usize) -> Result<Vec<u8>> {
|
||||||
|
let mut bytes: Vec<u8> = vec![0; size];
|
||||||
|
rand::rand_bytes(&mut bytes).map_err(|_e| U2fError::RandomSecureBytesError)?;
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expiration(timestamp: String) -> Duration {
|
||||||
|
let now: DateTime<Utc> = Utc::now();
|
||||||
|
|
||||||
|
let ts = timestamp.parse::<DateTime<Utc>>();
|
||||||
|
|
||||||
|
now.signed_duration_since(ts.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode initial bytes of buffer as ASN and return the length of the encoded structure.
|
||||||
|
// http://en.wikipedia.org/wiki/X.690
|
||||||
|
pub fn asn_length(mem: Bytes) -> Result<usize> {
|
||||||
|
let buffer: &[u8] = &mem[..];
|
||||||
|
|
||||||
|
if mem.len() < 2 || buffer[0] != 0x30 {
|
||||||
|
// Type
|
||||||
|
return Err(U2fError::Asm1DecoderError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = buffer[1]; // Len
|
||||||
|
if len & 0x80 == 0 {
|
||||||
|
return Ok((len & 0x7f) as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
let numbem_of_bytes = len & 0x7f;
|
||||||
|
if numbem_of_bytes == 0 {
|
||||||
|
return Err(U2fError::Asm1DecoderError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut length: usize = 0;
|
||||||
|
for num in 0..numbem_of_bytes {
|
||||||
|
length = length * 0x100 + (buffer[(2 + num) as usize] as usize);
|
||||||
|
}
|
||||||
|
|
||||||
|
length += numbem_of_bytes as usize;
|
||||||
|
|
||||||
|
Ok(length + 2) // Add the 2 initial bytes: type and length.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_encoded(data: &[u8]) -> String {
|
||||||
|
let encoded: String = URL_SAFE_NO_PAD.encode(data);
|
||||||
|
|
||||||
|
encoded.trim_end_matches('=').to_string()
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
dist/**
|
||||||
|
!dist/.gitkeep
|
@ -1,10 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/build
|
|
||||||
/.svelte-kit
|
|
||||||
/package
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
vite.config.js.timestamp-*
|
|
||||||
vite.config.ts.timestamp-*
|
|
@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "svelte-app",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite dev",
|
|
||||||
"build": "vite build",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
||||||
"tauri": "tauri"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@sveltejs/adapter-auto": "2.1.1",
|
|
||||||
"@sveltejs/kit": "1.27.7",
|
|
||||||
"@tauri-apps/cli": "2.0.0-beta.0",
|
|
||||||
"svelte": "4.2.8",
|
|
||||||
"svelte-check": "3.6.2",
|
|
||||||
"typescript": "5.3.2",
|
|
||||||
"vite": "5.0.12"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tauri-apps/plugin-websocket": "link:..\\.."
|
|
||||||
},
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
// See https://kit.svelte.dev/docs/types#app
|
|
||||||
// for information about these interfaces
|
|
||||||
// and what to do when importing types
|
|
||||||
declare namespace App {
|
|
||||||
// interface Error {}
|
|
||||||
// interface Locals {}
|
|
||||||
// interface PageData {}
|
|
||||||
// interface Platform {}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<link rel="stylesheet" href="/global.css" />
|
|
||||||
%sveltekit.head%
|
|
||||||
</head>
|
|
||||||
<body data-sveltekit-preload-data="hover">
|
|
||||||
<div style="display: contents">%sveltekit.body%</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,44 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import WebSocket from "@tauri-apps/plugin-websocket";
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
let ws;
|
|
||||||
let response = "";
|
|
||||||
let message = "";
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
ws = await WebSocket.connect("ws://127.0.0.1:8080")
|
|
||||||
.then((r) => {
|
|
||||||
_updateResponse("Connected");
|
|
||||||
return r;
|
|
||||||
})
|
|
||||||
.catch(_updateResponse);
|
|
||||||
ws.addListener(_updateResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
function _updateResponse(returnValue) {
|
|
||||||
response +=
|
|
||||||
(typeof returnValue === "string"
|
|
||||||
? returnValue
|
|
||||||
: JSON.stringify(returnValue)) + "<br>";
|
|
||||||
}
|
|
||||||
|
|
||||||
function send() {
|
|
||||||
ws.send(message)
|
|
||||||
.then(() => _updateResponse("Message sent"))
|
|
||||||
.catch(_updateResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnect() {
|
|
||||||
ws.disconnect()
|
|
||||||
.then(() => _updateResponse("Disconnected"))
|
|
||||||
.catch(_updateResponse);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<input bind:value={message} />
|
|
||||||
<button on:click={send}>Send</button>
|
|
||||||
<button on:click={disconnect}>Disconnect</button>
|
|
||||||
</div>
|
|
||||||
<div>{@html response}</div>
|
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,19 +0,0 @@
|
|||||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import adapter from "@sveltejs/adapter-static";
|
|
||||||
import { vitePreprocess } from "@sveltejs/kit/vite";
|
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
|
||||||
const config = {
|
|
||||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
|
||||||
// for more information about preprocessors
|
|
||||||
preprocess: vitePreprocess(),
|
|
||||||
|
|
||||||
kit: {
|
|
||||||
adapter: adapter(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./.svelte-kit/tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true
|
|
||||||
}
|
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
|
||||||
//
|
|
||||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
|
||||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import { sveltekit } from "@sveltejs/kit/vite";
|
|
||||||
import type { UserConfig } from "vite";
|
|
||||||
|
|
||||||
const config: UserConfig = {
|
|
||||||
plugins: [sveltekit()],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist/**
|
||||||
|
!dist/.gitkeep
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
@ -0,0 +1,20 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>WebSocket Plugin Example</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div>
|
||||||
|
<input bind:value="{message}" />
|
||||||
|
<button on:click="{send}">Send</button>
|
||||||
|
<button on:click="{disconnect}">Disconnect</button>
|
||||||
|
</div>
|
||||||
|
<div id="response-container"></div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "tauri-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "1.5.9",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.0.12"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tauri-plugin-websocket-api": "link:..\\.."
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
import WebSocket from "tauri-plugin-websocket-api";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
let ws: WebSocket;
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
document.querySelector("#send")?.addEventListener("click", send);
|
||||||
|
document.querySelector("#disconnect")?.addEventListener("click", disconnect);
|
||||||
|
await connect();
|
||||||
|
});
|
||||||
|
|
||||||
|
function _updateResponse(returnValue: unknown) {
|
||||||
|
const msg = document.createElement("p");
|
||||||
|
msg.textContent =
|
||||||
|
typeof returnValue === "string" ? returnValue : JSON.stringify(returnValue);
|
||||||
|
document.querySelector("#response-container")?.appendChild(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
try {
|
||||||
|
ws = await WebSocket.connect("ws://127.0.0.1:8080").then((r) => {
|
||||||
|
_updateResponse("Connected");
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
_updateResponse(e);
|
||||||
|
}
|
||||||
|
ws.addListener(_updateResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function send() {
|
||||||
|
ws.send(document.querySelector("#msg-input")?.textContent || "")
|
||||||
|
.then(() => _updateResponse("Message sent"))
|
||||||
|
.catch(_updateResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
ws.disconnect()
|
||||||
|
.then(() => _updateResponse("Disconnected"))
|
||||||
|
.catch(_updateResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<input type="text" />
|
||||||
|
<button id="send">send</button>
|
||||||
|
<button id="disconnect">disconnect</button>
|
||||||
|
<div id="response-container"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|