merge from v2
@@ -27,8 +27,10 @@ authenticator = "0.3.1"
|
||||
once_cell = "1"
|
||||
sha2 = "0.10"
|
||||
base64 = "0.21"
|
||||
u2f = "0.2"
|
||||
chrono = ">= 0.4.0, <0.4.30"
|
||||
chrono = "0.4"
|
||||
bytes = "1"
|
||||
byteorder = "1"
|
||||
openssl = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
@@ -11,7 +11,7 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
JSON(#[from] serde_json::Error),
|
||||
#[error(transparent)]
|
||||
U2F(#[from] u2f::u2ferror::U2fError),
|
||||
U2F(#[from] crate::u2f_crate::u2ferror::U2fError),
|
||||
#[error(transparent)]
|
||||
Auth(#[from] authenticator::errors::AuthenticatorError),
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
mod auth;
|
||||
mod error;
|
||||
mod u2f;
|
||||
mod u2f_crate;
|
||||
|
||||
use tauri::{
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::u2f_crate::messages::*;
|
||||
use crate::u2f_crate::protocol::*;
|
||||
use crate::u2f_crate::register::*;
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
|
||||
use chrono::prelude::*;
|
||||
use serde::Serialize;
|
||||
use std::convert::Into;
|
||||
use u2f::messages::*;
|
||||
use u2f::protocol::*;
|
||||
use u2f::register::*;
|
||||
|
||||
static VERSION: &str = "U2F_V2";
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -18,4 +18,4 @@ tauri = { workspace = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tiny_http = "0.12"
|
||||
http = "0.2"
|
||||
http = "1"
|
||||
|
||||
@@ -20,7 +20,7 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
serde_repr = "0.1"
|
||||
byte-unit = "4.0"
|
||||
byte-unit = "5"
|
||||
log = { workspace = true, features = [ "kv_unstable" ] }
|
||||
time = { version = "0.3", features = [ "formatting", "local-offset" ] }
|
||||
fern = "0.6"
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
node_modules/
|
||||
dist/**
|
||||
!dist/.gitkeep
|
||||
@@ -147,7 +147,6 @@ fn main() {
|
||||
];
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(
|
||||
tauri_plugin_sql::Builder::default()
|
||||
.add_migrations("sqlite:mydatabase.db", migrations)
|
||||
|
||||
@@ -22,10 +22,11 @@ serde_json = { workspace = true }
|
||||
tauri = { workspace = true }
|
||||
log = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
http = "1"
|
||||
rand = "0.8"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1", features = [ "net", "sync" ] }
|
||||
tokio-tungstenite = { version = "0.20" }
|
||||
tokio-tungstenite = { version = "0.21" }
|
||||
|
||||
[features]
|
||||
native-tls = [ "tokio-tungstenite/native-tls" ]
|
||||
|
||||
@@ -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:..\\.."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -11,7 +11,7 @@ tauri = { workspace = true }
|
||||
tokio = { version = "1", features = ["net"] }
|
||||
futures-util = "0.3"
|
||||
tauri-plugin-websocket = { path = "../../../" }
|
||||
tokio-tungstenite = "0.20"
|
||||
tokio-tungstenite = "0.21"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { workspace = true }
|
||||
|
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>
|
||||
`;
|
||||
@@ -66,3 +66,7 @@ button:not(:disabled):active {
|
||||
button:focus {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -2,5 +2,4 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
export const prerender = true;
|
||||
export const ssr = false;
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
)]
|
||||
|
||||
use futures_util::{stream::SplitSink, SinkExt, StreamExt};
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use serde::{ser::Serializer, Deserialize, Serialize};
|
||||
use tauri::{
|
||||
ipc::Channel,
|
||||
@@ -22,6 +23,7 @@ use tokio::{net::TcpStream, sync::Mutex};
|
||||
use tokio_tungstenite::{
|
||||
connect_async_with_config,
|
||||
tungstenite::{
|
||||
client::IntoClientRequest,
|
||||
protocol::{CloseFrame as ProtocolCloseFrame, WebSocketConfig},
|
||||
Message,
|
||||
},
|
||||
@@ -30,8 +32,6 @@ use tokio_tungstenite::{
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use tauri::http::header::{HeaderName, HeaderValue};
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
|
||||
type Id = u32;
|
||||
type WebSocket = WebSocketStream<MaybeTlsStream<TcpStream>>;
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
|
||||
- [`d198c01`](https://github.com/tauri-apps/plugins-workspace/commit/d198c014863ee260cb0de88a14b7fc4356ef7474)([#862](https://github.com/tauri-apps/plugins-workspace/pull/862)) Update to tauri beta.
|
||||
|
||||
- [`14f59615`](https://github.com/tauri-apps/plugins-workspace/commit/14f5961569c7d759d8d6d836352c787484594bd5) 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.
|
||||
|
||||
## \[2.0.0-alpha.5]
|
||||
|
||||
- [`387c2f9`](https://github.com/tauri-apps/plugins-workspace/commit/387c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.13.
|
||||
|
||||
@@ -67,6 +67,11 @@ struct WindowState {
|
||||
height: f64,
|
||||
x: i32,
|
||||
y: i32,
|
||||
// prev_x and prev_y are used to store position
|
||||
// before maximization happened, because maximization
|
||||
// will set x and y to the top-left corner of the monitor
|
||||
prev_x: i32,
|
||||
prev_y: i32,
|
||||
maximized: bool,
|
||||
visible: bool,
|
||||
decorated: bool,
|
||||
@@ -80,6 +85,8 @@ impl Default for WindowState {
|
||||
height: Default::default(),
|
||||
x: Default::default(),
|
||||
y: Default::default(),
|
||||
prev_x: Default::default(),
|
||||
prev_y: Default::default(),
|
||||
maximized: Default::default(),
|
||||
visible: true,
|
||||
decorated: true,
|
||||
@@ -149,13 +156,23 @@ impl<R: Runtime> WindowExt for Window<R> {
|
||||
}
|
||||
|
||||
if flags.contains(StateFlags::POSITION) {
|
||||
let position = (state.x, state.y).into();
|
||||
let size = (state.width, state.height).into();
|
||||
// restore position to saved value if saved monitor exists
|
||||
// otherwise, let the OS decide where to place the window
|
||||
for m in self.available_monitors()? {
|
||||
if m.contains((state.x, state.y).into()) {
|
||||
if m.intersects(position, size) {
|
||||
self.set_position(PhysicalPosition {
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
x: if state.maximized {
|
||||
state.prev_x
|
||||
} else {
|
||||
state.x
|
||||
},
|
||||
y: if state.maximized {
|
||||
state.prev_y
|
||||
} else {
|
||||
state.y
|
||||
},
|
||||
})?;
|
||||
}
|
||||
}
|
||||
@@ -251,22 +268,17 @@ impl<R: Runtime> WindowExtInternal for Window<R> {
|
||||
.unwrap_or(1.);
|
||||
let size = self.inner_size()?.to_logical(scale_factor);
|
||||
|
||||
// It doesn't make sense to save a self with 0 height or width
|
||||
// It doesn't make sense to save a window with 0 height or width
|
||||
if size.width > 0. && size.height > 0. && !is_maximized {
|
||||
state.width = size.width;
|
||||
state.height = size.height;
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(StateFlags::POSITION) {
|
||||
if flags.contains(StateFlags::POSITION) && !is_maximized {
|
||||
let position = self.outer_position()?;
|
||||
if let Ok(Some(monitor)) = self.current_monitor() {
|
||||
// save only window positions that are inside the current monitor
|
||||
if monitor.contains(position) && !is_maximized {
|
||||
state.x = position.x;
|
||||
state.y = position.y;
|
||||
}
|
||||
}
|
||||
state.x = position.x;
|
||||
state.y = position.y;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -281,6 +293,10 @@ pub struct Builder {
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the state flags to control what state gets restored and saved.
|
||||
pub fn with_state_flags(mut self, flags: StateFlags) -> Self {
|
||||
self.state_flags = flags;
|
||||
@@ -354,13 +370,25 @@ impl Builder {
|
||||
.or_insert_with(WindowState::default);
|
||||
}
|
||||
|
||||
window.on_window_event(move |e| {
|
||||
if let WindowEvent::CloseRequested { .. } = e {
|
||||
window.on_window_event(move |e| match e {
|
||||
WindowEvent::CloseRequested { .. } => {
|
||||
let mut c = cache.lock().unwrap();
|
||||
if let Some(state) = c.get_mut(&label) {
|
||||
let _ = window_clone.update_state(state, flags);
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::Moved(position) if flags.contains(StateFlags::POSITION) => {
|
||||
let mut c = cache.lock().unwrap();
|
||||
if let Some(state) = c.get_mut(&label) {
|
||||
state.prev_x = state.x;
|
||||
state.prev_y = state.y;
|
||||
|
||||
state.x = position.x;
|
||||
state.y = position.y;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
})
|
||||
.on_event(move |app, event| {
|
||||
@@ -373,17 +401,31 @@ impl Builder {
|
||||
}
|
||||
|
||||
trait MonitorExt {
|
||||
fn contains(&self, position: PhysicalPosition<i32>) -> bool;
|
||||
fn intersects(&self, position: PhysicalPosition<i32>, size: LogicalSize<u32>) -> bool;
|
||||
}
|
||||
|
||||
impl MonitorExt for Monitor {
|
||||
fn contains(&self, position: PhysicalPosition<i32>) -> bool {
|
||||
fn intersects(&self, position: PhysicalPosition<i32>, size: LogicalSize<u32>) -> bool {
|
||||
let size = size.to_physical::<u32>(self.scale_factor());
|
||||
|
||||
let PhysicalPosition { x, y } = *self.position();
|
||||
let PhysicalSize { width, height } = *self.size();
|
||||
|
||||
x < position.x as _
|
||||
&& position.x < (x + width as i32)
|
||||
&& y < position.y as _
|
||||
&& position.y < (y + height as i32)
|
||||
let left = x;
|
||||
let right = x + width as i32;
|
||||
let top = y;
|
||||
let bottom = y + height as i32;
|
||||
|
||||
[
|
||||
(position.x, position.y),
|
||||
(position.x + size.width as i32, position.y),
|
||||
(position.x, position.y + size.height as i32),
|
||||
(
|
||||
position.x + size.width as i32,
|
||||
position.y + size.height as i32,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.any(|(x, y)| x >= left && x < right && y >= top && y < bottom)
|
||||
}
|
||||
}
|
||||
|
||||