@ -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
|
||||
node_modules
|
||||
dist
|
||||
dist-js
|
@ -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,65 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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,156 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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,54 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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,12 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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,191 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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,101 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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,39 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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,66 @@
|
||||
// Copyright 2021 Flavio Oliveira
|
||||
// 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/
|
||||
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,50 @@
|
||||
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 @@
|
||||
/// <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"]
|
||||
}
|