add proxmox::tools::tfa

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2020-08-04 11:12:14 +02:00
parent 05749ab419
commit 8cbf9cb7c8
8 changed files with 2507 additions and 1 deletions

View File

@ -11,6 +11,10 @@ description = "Proxmox library"
exclude = [ "debian" ]
[[examples]]
name = "u2ftest"
required-features = [ "tokio", "u2f" ]
[dependencies]
# General dependencies
anyhow = "1.0"
@ -19,6 +23,7 @@ libc = "0.2"
nix = "0.19"
# tools module:
base32 = { version = "0.4", optional = true }
base64 = "0.12"
endian_trait = { version = "0.6", features = ["arrays"] }
regex = "1.2"
@ -49,7 +54,7 @@ proxmox-api-macro = { path = "../proxmox-api-macro", optional = true, version =
proxmox-sortable-macro = { path = "../proxmox-sortable-macro", optional = true, version = "0.1.1" }
[features]
default = [ "router", "cli", "websocket" ]
default = [ "cli", "router", "tfa", "u2f", "websocket" ]
sortable-macro = ["proxmox-sortable-macro"]
# api:
@ -58,6 +63,10 @@ test-harness = []
cli = [ "router", "hyper", "tokio" ]
router = [ "hyper", "tokio" ]
websocket = [ "futures", "hyper", "openssl", "tokio/sync", "tokio/io-util", "openssl" ]
tfa = [ "openssl" ]
u2f = [ "base32" ]
examples = ["tokio/macros", "u2f"]
# tools:
#valgrind = ["proxmox-tools/valgrind"]

589
proxmox/examples/u2ftest.rs Normal file
View File

@ -0,0 +1,589 @@
//! # live u2f test
//!
//! Listens on `localhost:13905` via http (NOT https) and provides a u2f api test server.
//!
//! To use this, you'll need to create an https wrapper (eg. nginx reverse proxy) with a valid
//! appid, then run:
//!
//! ## Running the API:
//!
//! NOTE: you need to run this in a directory with a `u2f-api.js` file.
//!
//! ```
//! $ cargo run --example u2ftest --features='examples' <APPID>
//! ```
//!
//! Replace `<APPID>` with a working `https://...` url, the API is expected to be on the top level.
//!
//! ## Example Veverse Proxy via nginx:
//!
//! ```
//! server {
//! listen 443 ssl;
//! server_name u2ftest.enonet.errno.eu;
//!
//! ssl_certificate /etc/nginx/ssl/my.pem;
//! ssl_certificate_key /etc/nginx/ssl/my.key;
//! ssl_protocols TLSv1.2;
//! ssl_ciphers HIGH:!aNULL:!MD5;
//!
//! root /var/www/html;
//!
//! location / {
//! proxy_pass http://127.0.0.1:13905/;
//! }
//! }
//!
//! ## Debugging
//!
//! The registration and authentication api calls store the response data in
//! `test-registration.json` and `test-auth.json` which get "retried" at startup by the
//! `retry_reg()` and `retry_auth()` calls. This way less hardware interaction is required for
//! debugging.
//! ```
use std::collections::HashMap;
use std::io;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use anyhow::{bail, format_err, Error};
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::stream::StreamExt;
use proxmox::tools::tfa::u2f;
pub const PORT: u16 = 13905;
#[cfg(not(feature = "examples"))]
fn main() {
let _unused = do_main();
panic!("rebuild with: --features examples");
}
#[cfg(feature = "examples")]
#[tokio::main]
async fn main() -> Result<(), Error> {
do_main().await
}
async fn do_main() -> Result<(), Error> {
use std::net::SocketAddr;
use std::net::ToSocketAddrs;
let addr = ("localhost", PORT)
.to_socket_addrs()
.ok()
.and_then(|mut addrs| addrs.next())
.unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], PORT)));
let appid = std::env::args()
.skip(1)
.next()
.expect("please specify the appid/origin URL");
let sv = Arc::new(Sv::new(appid));
// retry the last registration challenge remembered in `test-registration.json`
sv.retry_reg()?;
// retry the last authentication challenge remembered in `test-auth.json`
sv.retry_auth()?;
let make_service = make_service_fn(move |_conn| {
let sv = Arc::clone(&sv);
async move {
Ok::<_, std::convert::Infallible>(service_fn(move |request: Request<Body>| {
let sv = Arc::clone(&sv);
async move {
let res = handle(&sv, request).await.or_else(|err| {
let err = serde_json::to_string(&serde_json::json!({
"error": err.to_string(),
}))
.unwrap();
Ok::<_, std::convert::Infallible>(
Response::builder()
.status(500)
.body(Body::from(err))
.unwrap(),
)
});
eprintln!("{:#?}", res);
res
}
}))
}
});
let server = Server::bind(&addr).serve(make_service);
server.await?;
Ok(())
}
async fn fetch_body(mut request_body: Body) -> Result<Vec<u8>, Error> {
let mut body = Vec::new();
while let Some(chunk) = request_body.try_next().await? {
if body.len() + chunk.len() > 1024 * 1024 {
bail!("request too big");
}
body.extend(chunk);
}
Ok(body)
}
async fn handle(sv: &Arc<Sv>, request: Request<Body>) -> Result<Response<Body>, Error> {
let (parts, body) = request.into_parts();
let body = fetch_body(body).await?;
eprintln!("fetching: {}", parts.uri.path());
match parts.uri.path() {
"/" => simple("text/html", INDEX_HTML),
"/index.html" => simple("text/html", INDEX_HTML),
"/u2f-api.js" => file("text/javascript", "u2f-api.js"),
"/style.css" => simple("text/css", STYLE_CSS),
"/registration" => sv.registration(),
"/finish-registration" => sv.finish_registration(serde_json::from_slice(&body)?),
"/authenticate" => sv.authenticate(serde_json::from_slice(&body)?),
"/finish-auth" => sv.finish_auth(serde_json::from_slice(&body)?),
_ => Ok(Response::builder()
.status(404)
.body(Body::from("not found"))
.unwrap()),
}
}
struct User {
data: u2f::Registration,
challenges: HashMap<usize, String>,
}
struct Sv {
context: u2f::U2f,
counter: AtomicUsize,
challenges: Mutex<HashMap<usize, String>>,
users: Mutex<HashMap<usize, User>>, // key handle
}
impl Sv {
fn new(appid: String) -> Self {
Self {
context: u2f::U2f::new(appid.clone(), appid),
counter: AtomicUsize::new(0),
challenges: Mutex::new(HashMap::new()),
users: Mutex::new(HashMap::new()),
}
}
fn nextid(&self) -> usize {
self.counter.fetch_add(1, Ordering::AcqRel)
}
fn registration(&self) -> Result<Response<Body>, Error> {
let challenge = self.context.registration_challenge()?;
let id = self.nextid();
let output = serde_json::json!({
"id": id,
"challenge": challenge,
"context": &self.context,
});
self.challenges
.lock()
.unwrap()
.insert(id, challenge.challenge);
json(output)
}
fn finish_registration(&self, mut response: Value) -> Result<Response<Body>, Error> {
let id = response["id"]
.as_u64()
.ok_or_else(|| format_err!("bad or missing ID in response"))? as usize;
let rspdata: Value = response
.as_object_mut()
.unwrap()
.remove("response")
.ok_or_else(|| format_err!("missing response data"))?;
let challenge = self
.challenges
.lock()
.unwrap()
.remove(&id)
.ok_or_else(|| format_err!("no such challenge"))?;
std::fs::write(
"test-registration.json",
serde_json::to_string(&serde_json::json!({
"challenge": challenge,
"response": &rspdata,
}))?,
)?;
let data = self
.context
.registration_verify_obj(&challenge, serde_json::from_value(rspdata)?)?;
match data {
Some(data) => {
self.users.lock().unwrap().insert(
id,
User {
data,
challenges: HashMap::new(),
},
);
json(serde_json::json!({ "id": id }))
}
None => bail!("registration failed"),
}
}
fn retry_reg(&self) -> Result<(), Error> {
let data = match std::fs::read("test-registration.json") {
Ok(data) => data,
Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err.into()),
};
#[derive(Deserialize)]
struct TestChallenge {
challenge: String,
response: u2f::RegistrationResponse,
}
let ts: TestChallenge = serde_json::from_slice(&data)?;
let res = self
.context
.registration_verify_obj(&ts.challenge, ts.response)?;
eprintln!("=> {:#?}", res);
Ok(())
}
fn authenticate(&self, params: Value) -> Result<Response<Body>, Error> {
let uid = params["uid"]
.as_u64()
.ok_or_else(|| format_err!("bad or missing user id in auth call"))?
as usize;
let mut users = self.users.lock().unwrap();
let user = users
.get_mut(&uid)
.ok_or_else(|| format_err!("no such user"))?;
let challenge = self.context.auth_challenge()?;
let id = self.nextid();
user.challenges.insert(id, challenge.challenge.clone());
let output = serde_json::json!({
"id": id,
"challenge": challenge,
"keys": [&user.data.key],
});
json(output)
}
fn finish_auth(&self, mut response: Value) -> Result<Response<Body>, Error> {
let uid = response["uid"]
.as_u64()
.ok_or_else(|| format_err!("bad or missing user id in auth call"))?
as usize;
let id = response["id"]
.as_u64()
.ok_or_else(|| format_err!("bad or missing ID in response"))? as usize;
let rspdata: Value = response
.as_object_mut()
.unwrap()
.remove("response")
.ok_or_else(|| format_err!("missing response data"))?;
let mut users = self.users.lock().unwrap();
let user = users
.get_mut(&uid)
.ok_or_else(|| format_err!("no such user"))?;
let challenge = user
.challenges
.remove(&id)
.ok_or_else(|| format_err!("no such challenge for user"))?;
std::fs::write(
"test-auth.json",
serde_json::to_string(&serde_json::json!({
"challenge": challenge,
"response": &rspdata,
"user": user.data,
}))?,
)?;
let rspdata: u2f::AuthResponse = serde_json::from_value(rspdata)?;
if user.data.key.key_handle != rspdata.key_handle() {
bail!("key handle mismatch");
}
let res = self
.context
.auth_verify_obj(&user.data.public_key, &challenge, rspdata)?;
match res {
Some(auth) => json(serde_json::json!({
"present": auth.user_present,
"counter": auth.counter,
})),
None => bail!("authentication failed"),
}
}
fn retry_auth(&self) -> Result<(), Error> {
let data = match std::fs::read("test-auth.json") {
Ok(data) => data,
Err(ref err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(err) => return Err(err.into()),
};
#[derive(Deserialize)]
struct TestChallenge {
challenge: String,
user: u2f::Registration,
response: u2f::AuthResponse,
}
let ts: TestChallenge = serde_json::from_slice(&data)?;
let res = self
.context
.auth_verify_obj(&ts.user.public_key, &ts.challenge, ts.response)?;
eprintln!("=> {:#?}", res);
Ok(())
}
}
fn json<T: Serialize>(data: T) -> Result<Response<Body>, Error> {
Ok(Response::builder()
.status(200)
.header("Content-Type", "application/json")
.body(Body::from(serde_json::to_string(&data)?))
.unwrap())
}
fn simple(content_type: &'static str, data: &'static str) -> Result<Response<Body>, Error> {
Ok(Response::builder()
.status(200)
.header("Content-Type", content_type)
.body(Body::from(data))
.unwrap())
}
fn file(content_type: &'static str, file_name: &'static str) -> Result<Response<Body>, Error> {
let file = std::fs::read(file_name)?;
Ok(Response::builder()
.status(200)
.header("Content-Type", content_type)
.body(Body::from(file))
.unwrap())
}
const INDEX_HTML: &str = r##"
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
<html>
<head>
<link rel="StyleSheet" type="text/css" href="style.css" />
<script type="text/javascript" src="/u2f-api.js"></script>
<script type="text/javascript">
var USER_ID = undefined;
function clear() {
document.getElementById("status").innerText = "";
}
function log(text) {
console.log(text);
document.getElementById("status").innerText += text + "\n";
}
function some(x) {
return x !== undefined && x !== null;
}
async function call(path, body) {
let opts = undefined;
if (some(body)) {
opts = {
'method': 'POST',
'Content-Type': 'application/json',
'body': JSON.stringify(body),
};
}
return (await fetch(path, opts)).json();
}
async function u2f_register(appId, registerRequests, registeredKeys, timeoutSecs) {
return new Promise((resolve, reject) => {
u2f.register(
appId,
registerRequests,
registeredKeys,
(rsp) => {
if (rsp.errorCode) {
reject(rsp);
} else {
delete rsp.errorCode;
resolve(rsp);
}
},
timeoutSecs,
);
});
}
async function u2f_sign(appId, challenge, registeredKeys, timeoutSecs) {
return new Promise((resolve, reject) => {
u2f.sign(
appId,
challenge,
registeredKeys,
(rsp) => {
if (rsp.errorCode) {
reject(rsp);
} else {
delete rsp.errorCode;
resolve(rsp);
}
},
timeoutSecs,
);
});
}
async function register() {
try {
clear();
log("fetching registration");
let registration = await call("/registration");
log("logging registration challenge to console");
console.log(registration);
let data = registration.challenge;
let challenge = {
"challenge": data.challenge,
"version": data.version,
};
log("please press the button on your u2f token");
let rsp = await u2f_register(
data.appId,
[challenge],
[],
10,
);
log("logging response to console");
console.log(rsp);
log("replying");
let reg = await call("/finish-registration", {
'response': rsp,
'id': registration.id,
});
log("server responded");
log(JSON.stringify(reg));
let id = reg.id;
log("Our user id is now: " + id);
USER_ID = id;
} catch (ex) {
log("An exception occurred:");
console.log(ex);
log(ex);
}
}
async function authenticate() {
try {
clear();
if (!some(USER_ID)) {
log("not authenticated");
return;
}
log("fetching authentication");
let auth = await call("/authenticate", {
'uid': USER_ID,
});
log("logging authentication");
console.log(auth);
log("please press the button on your u2f token");
let rsp = await u2f_sign(
auth.challenge.appId,
auth.challenge.challenge,
auth.keys,
10,
);
log("logging token to console");
console.log(rsp);
log("replying");
let reg = await call("/finish-auth", {
'response': rsp,
'uid': USER_ID,
'id': auth.id,
});
log("server responded");
log(JSON.stringify(reg));
} catch (ex) {
log("An exception occurred:");
console.log(ex);
log(ex);
}
}
</script>
</head>
<body>
<div id="buttons">
<a href="#" onclick="register();">Register</a>
<a href="#" onclick="authenticate();">Authenticate</a>
</div>
<div id="status">
Select an action.
</div>
</body>
</html>
"##;
const STYLE_CSS: &str = r##"
body {
background-color: #f8fff8;
padding: 3em 10em 3em 10em;
}
p, a, h1, h2, h3, h4 {
margin: 0;
padding: 0;
}
#status {
background-color: #fff;
border: 1px solid #ccc;
margin: auto;
width: 80em;
font-family: monospace;
}
"##;

View File

@ -24,6 +24,9 @@ pub mod vec;
#[cfg(feature = "websocket")]
pub mod websocket;
#[cfg(feature = "tfa")]
pub mod tfa;
#[doc(inline)]
pub use uuid::Uuid;

View File

@ -134,3 +134,49 @@ pub mod string_as_base64 {
})
}
}
/// Serialize Vec<u8> as base64url encoded string without padding.
///
/// Usage example:
/// ```
/// use serde::{Deserialize, Serialize};
///
/// # #[derive(Debug)]
/// #[derive(Deserialize, PartialEq, Serialize)]
/// struct Foo {
/// #[serde(with = "proxmox::tools::serde::bytes_as_base64url_nopad")]
/// data: Vec<u8>,
/// }
///
/// let obj = Foo { data: vec![1, 2, 3, 4] };
/// let json = serde_json::to_string(&obj).unwrap();
/// assert_eq!(json, r#"{"data":"AQIDBA"}"#);
///
/// let deserialized: Foo = serde_json::from_str(&json).unwrap();
/// assert_eq!(obj, deserialized);
/// ```
pub mod bytes_as_base64url_nopad {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: AsRef<[u8]>,
S: Serializer,
{
serializer.serialize_str(&base64::encode_config(
data.as_ref(),
base64::URL_SAFE_NO_PAD,
))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
String::deserialize(deserializer).and_then(|string| {
base64::decode_config(&string, base64::URL_SAFE_NO_PAD)
.map_err(|err| Error::custom(err.to_string()))
})
}
}

View File

@ -0,0 +1,4 @@
#[cfg(feature = "u2f")]
pub mod u2f;
pub mod totp;

View File

@ -0,0 +1,561 @@
//! Implementation of TOTP, U2F and other mechanisms.
use std::convert::TryFrom;
use std::fmt;
use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Error};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Signer;
use percent_encoding::{percent_decode, percent_encode};
use serde::{Serialize, Serializer};
/// Algorithms supported by the TOTP. This is simply an enum limited to the most common
/// available implementations.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Algorithm {
Sha1,
Sha256,
Sha512,
}
impl Into<MessageDigest> for Algorithm {
fn into(self) -> MessageDigest {
match self {
Algorithm::Sha1 => MessageDigest::sha1(),
Algorithm::Sha256 => MessageDigest::sha256(),
Algorithm::Sha512 => MessageDigest::sha512(),
}
}
}
/// Displayed in a way compatible with the `otpauth` URI specification.
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Algorithm::Sha1 => write!(f, "SHA1"),
Algorithm::Sha256 => write!(f, "SHA256"),
Algorithm::Sha512 => write!(f, "SHA512"),
}
}
}
/// Parsed in a way compatible with the `otpauth` URI specification.
impl std::str::FromStr for Algorithm {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
Ok(match s {
"SHA1" => Algorithm::Sha1,
"SHA256" => Algorithm::Sha256,
"SHA512" => Algorithm::Sha512,
_ => bail!("unsupported algorithm: {}", s),
})
}
}
/// OTP secret builder.
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct TotpBuilder {
inner: Totp,
}
impl From<Totp> for TotpBuilder {
#[inline]
fn from(inner: Totp) -> Self {
Self { inner }
}
}
impl TotpBuilder {
pub fn secret(mut self, secret: Vec<u8>) -> Self {
self.inner.secret = secret;
self
}
/// Set the requested number of decimal digits.
pub fn digits(mut self, digits: u8) -> Self {
self.inner.digits = digits;
self
}
/// Set the algorithm.
pub fn algorithm(mut self, algorithm: Algorithm) -> Self {
self.inner.algorithm = algorithm;
self
}
/// Set the issuer.
pub fn issuer(mut self, issuer: String) -> Self {
self.inner.issuer = Some(issuer);
self
}
/// Set the account name. This is required to create an URI.
pub fn account_name(mut self, account_name: String) -> Self {
self.inner.account_name = Some(account_name);
self
}
/// Set the duration, in seconds, for which a value is valid.
///
/// Panics if `seconds` is 0.
pub fn step(mut self, seconds: usize) -> Self {
if seconds == 0 {
panic!("zero as 'step' value is invalid");
}
self.inner.step = seconds;
self
}
/// Finalize the OTP instance.
pub fn build(self) -> Totp {
self.inner
}
}
/// OTP secret key to produce OTP values with and the desired default number of decimal digits to
/// use for its values (defaults to 6).
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Totp {
/// The secret shared with the client.
secret: Vec<u8>,
/// The requested number decimal digits.
digits: u8,
/// The algorithm, defaults to sha1.
algorithm: Algorithm,
/// The duration, in seconds, for which a value is valid. Defaults to 30 seconds.
step: usize,
/// An optional issuer. To help users identify their TOTP settings.
issuer: Option<String>,
/// An optional account name, possibly chosen by the user, to identify their TOTP settings.
account_name: Option<String>,
}
impl Totp {
/// Allow modifying parameters by turning this into a builder.
pub fn into_builder(self) -> TotpBuilder {
self.into()
}
/// Duplicate the value into a new builder to modify parameters.
pub fn to_builder(&self) -> TotpBuilder {
self.clone().into()
}
/// Create a new empty OTP instance with default values and a predefined secret key.
pub fn empty() -> Self {
Self {
secret: Vec::new(),
digits: 6,
algorithm: Algorithm::Sha1,
step: 30,
issuer: None,
account_name: None,
}
}
/// Create an OTP builder prefilled with default values.
pub fn builder() -> TotpBuilder {
TotpBuilder {
inner: Self::empty(),
}
}
/// Create a new OTP secret key builder using a secret specified in hexadecimal bytes.
pub fn builder_from_hex(secret: &str) -> Result<TotpBuilder, Error> {
crate::tools::hex_to_bin(secret).map(|secret| Self::builder().secret(secret))
}
/// Get the secret key in binary form.
pub fn secret(&self) -> &[u8] {
&self.secret
}
/// Get the used algorithm.
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
/// Get the step duration.
pub fn step(&self) -> Duration {
Duration::from_secs(self.step as u64)
}
/// Get the issuer, if any.
pub fn issuer(&self) -> Option<&str> {
self.issuer.as_ref().map(|s| s.as_str())
}
/// Get the account name, if any.
pub fn account_name(&self) -> Option<&str> {
self.account_name.as_ref().map(|s| s.as_str())
}
/// Raw signing function.
fn sign(&self, input_data: &[u8]) -> Result<TotpValue, Error> {
let secret = PKey::hmac(&self.secret)
.map_err(|err| anyhow!("error instantiating hmac key: {}", err))?;
let mut signer = Signer::new(self.algorithm.into(), &secret)
.map_err(|err| anyhow!("error instantiating hmac signer: {}", err))?;
signer
.update(input_data)
.map_err(|err| anyhow!("error calculating hmac (error in update): {}", err))?;
let hmac = signer
.sign_to_vec()
.map_err(|err| anyhow!("error calculating hmac (error in sign): {}", err))?;
let byte_offset = usize::from(
hmac.last()
.ok_or_else(|| anyhow!("error calculating hmac (too short)"))?
& 0xF,
);
let value = u32::from_be_bytes(
TryFrom::try_from(
hmac.get(byte_offset..(byte_offset + 4))
.ok_or_else(|| anyhow!("error calculating hmac (too short)"))?,
)
.unwrap(),
) & 0x7fffffff;
Ok(TotpValue {
value,
digits: u32::from(self.digits),
})
}
/// Create a HOTP value for a counter.
///
/// This is currently private as for actual counter mode we should have a validate helper
/// which forces handling of too-low-but-within-range values explicitly!
fn counter(&self, count: u64) -> Result<TotpValue, Error> {
self.sign(&count.to_be_bytes())
}
/// Convert a time stamp into a counter value. This makes it easier and cheaper to check a
/// range of values.
fn time_to_counter(&self, time: SystemTime) -> Result<u64, Error> {
match time.duration_since(SystemTime::UNIX_EPOCH) {
Ok(epoch) => Ok(epoch.as_secs() / (self.step as u64)),
Err(_) => bail!("refusing to create otp value for negative time"),
}
}
/// Create a TOTP value for a time stamp.
pub fn time(&self, time: SystemTime) -> Result<TotpValue, Error> {
self.counter(self.time_to_counter(time)?)
}
/// Verify a time value within a range.
///
/// This will iterate through `steps` and check if the provided `time + step * step_size`
/// matches. If a match is found, the matching step will be returned.
pub fn verify(
&self,
digits: &str,
time: SystemTime,
steps: std::ops::RangeInclusive<isize>,
) -> Result<Option<isize>, Error> {
let count = self.time_to_counter(time)? as i64;
for step in steps {
if self.counter((count + step as i64) as u64)? == digits {
return Ok(Some(step));
}
}
Ok(None)
}
/// Create an otpauth URI for this configuration.
pub fn to_uri(&self) -> Result<String, Error> {
use std::fmt::Write;
let mut out = String::new();
write!(out, "otpauth://totp/")?;
let account_name = match &self.account_name {
Some(account_name) => account_name,
None => bail!("cannot create otpauth uri without an account name"),
};
let issuer = match &self.issuer {
Some(issuer) => {
let issuer = percent_encode(issuer.as_bytes(), percent_encoding::NON_ALPHANUMERIC)
.to_string();
write!(out, "{}:", issuer)?;
Some(issuer)
}
None => None,
};
write!(
out,
"{}?secret={}",
percent_encode(account_name.as_bytes(), percent_encoding::NON_ALPHANUMERIC),
base32::encode(base32::Alphabet::RFC4648 { padding: false }, &self.secret),
)?;
write!(out, "&digits={}", self.digits)?;
write!(out, "&algorithm={}", self.algorithm)?;
write!(out, "&step={}", self.step)?;
if let Some(issuer) = issuer {
write!(out, "&issuer={}", issuer)?;
}
Ok(out)
}
}
impl std::str::FromStr for Totp {
type Err = Error;
fn from_str(uri: &str) -> Result<Self, Error> {
if !uri.starts_with("otpauth://totp/") {
bail!("not an otpauth uri");
}
let uri = &uri.as_bytes()[15..];
let qmark = uri
.iter()
.position(|&b| b == b'?')
.ok_or_else(|| anyhow!("missing '?' in otp uri"))?;
let account = &uri[..qmark];
let uri = &uri[(qmark + 1)..];
// FIXME: Also split on "%3A" / "%3a"
let mut account = account.splitn(2, |&b| b == b':');
let first_part = percent_decode(
&account
.next()
.ok_or_else(|| anyhow!("missing account in otpauth uri"))?,
)
.decode_utf8_lossy()
.into_owned();
let mut totp = Totp::empty();
match account.next() {
Some(account_name) => {
totp.issuer = Some(first_part);
totp.account_name =
Some(percent_decode(account_name).decode_utf8_lossy().to_string());
}
None => totp.account_name = Some(first_part),
}
for parts in uri.split(|&b| b == b'&') {
let mut parts = parts.splitn(2, |&b| b == b'=');
let key = percent_decode(
&parts
.next()
.ok_or_else(|| anyhow!("bad key in otpauth uri"))?,
)
.decode_utf8()?;
let value = percent_decode(
&parts
.next()
.ok_or_else(|| anyhow!("bad value in otpauth uri"))?,
);
match &*key {
"secret" => {
totp.secret = base32::decode(
base32::Alphabet::RFC4648 { padding: false },
&value.decode_utf8()?,
)
.ok_or_else(|| anyhow!("failed to decode otp secret in otpauth url"))?
}
"digits" => totp.digits = value.decode_utf8()?.parse()?,
"algorithm" => totp.algorithm = value.decode_utf8()?.parse()?,
"step" => totp.step = value.decode_utf8()?.parse()?,
"issuer" => totp.issuer = Some(value.decode_utf8_lossy().into_owned()),
_other => bail!("unrecognized otpauth uri parameter: {}", key),
}
}
if totp.secret.is_empty() {
bail!("missing secret in otpauth url");
}
Ok(totp)
}
}
crate::forward_deserialize_to_from_str!(Totp);
impl Serialize for Totp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::Error;
serializer.serialize_str(
&self
.to_uri()
.map_err(|err| Error::custom(err.to_string()))?,
)
}
}
/// A HOTP value with a decimal digit limit.
#[derive(Clone, Copy, Debug)]
pub struct TotpValue {
value: u32,
digits: u32,
}
impl TotpValue {
/// Change the number of decimal digits used for this HOTP value.
pub fn digits(self, digits: u32) -> Self {
Self { digits, ..self }
}
/// Get the raw integer value before truncation.
pub fn raw(&self) -> u32 {
self.value
}
/// Get the integer value truncated to the requested number of decimal digits.
pub fn value(&self) -> u32 {
self.value % 10u32.pow(self.digits)
}
}
impl fmt::Display for TotpValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{0:0width$}",
self.value(),
width = (self.digits as usize)
)
}
}
impl PartialEq<u32> for TotpValue {
fn eq(&self, other: &u32) -> bool {
self.value() == *other
}
}
/// For convenience we allow directly comparing with a string. This will make sure the string has
/// the exact number of digits while parsing it explicitly as a decimal string.
impl PartialEq<&str> for TotpValue {
fn eq(&self, other: &&str) -> bool {
// Since we use `from_str_radix` with a radix of 10 explicitly, we can check the number of
// bytes against the number of digits.
if other.as_bytes().len() != (self.digits as usize) {
return false;
}
match u32::from_str_radix(*other, 10) {
Ok(value) => self.value() == value,
Err(_) => false,
}
}
}
#[test]
fn test_otp() {
// Validated via:
// ```sh
// $ oathtool --hotp -c1 87259aa6550f059bca8c
// 337037
// ```
const SECRET_1: &str = "87259aa6550f059bca8c";
const EXPECTED_1: &str = "337037";
const EXPECTED_2: &str = "296746";
const EXPECTED_3: &str = "251167";
const EXPECTED_4_D8: &str = "11899249";
let hotp = Totp::builder_from_hex(SECRET_1)
.expect("failed to create Totp key")
.digits(6)
.build();
assert_eq!(
hotp.counter(1).expect("failed to create hotp value"),
EXPECTED_1,
);
assert_eq!(
hotp.counter(2)
.expect("failed to create hotp value")
.digits(6),
EXPECTED_2,
);
assert_eq!(
hotp.counter(3)
.expect("failed to create hotp value")
.digits(6),
EXPECTED_3,
);
assert_eq!(
hotp.counter(4)
.expect("failed to create hotp value")
.digits(8),
EXPECTED_4_D8,
);
let hotp = hotp
.into_builder()
.account_name("My Account".to_string())
.build();
let uri = hotp.to_uri().expect("failed to create otpauth uri");
let parsed: Totp = uri.parse().expect("failed to parse otp uri");
assert_eq!(parsed, hotp);
assert_eq!(parsed.issuer, None);
assert_eq!(
parsed.account_name.as_ref().map(String::as_str),
Some("My Account")
);
const SECRET_2: &str = "a60b1b20679b1a64e21a";
const EXPECTED: &str = "7757717";
// Validated via:
// ```sh
// $ oathtool --totp -d7 -s30 --now='2020-08-04 15:14:23 UTC' a60b1b20679b1a64e21a
// 7757717
// $ date -d'2020-08-04 15:14:23 UTC' +%s
// 1596554063
// ```
//
let totp = Totp::builder_from_hex(SECRET_2)
.expect("failed to create Totp key")
.build();
assert_eq!(
totp.time(SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1596554063))
.expect("failed to create totp value")
.digits(7),
EXPECTED,
);
let totp = totp
.into_builder()
.account_name("The Account Name".to_string())
.issuer("An Issuer".to_string())
.build();
let uri = totp.to_uri().expect("failed to create otpauth uri");
let parsed: Totp = uri.parse().expect("failed to parse otp uri");
assert_eq!(parsed, totp);
assert_eq!(
parsed.issuer.as_ref().map(String::as_str),
Some("An Issuer")
);
assert_eq!(
parsed.account_name.as_ref().map(String::as_str),
Some("The Account Name")
);
}

View File

@ -0,0 +1,546 @@
//! U2F implementation.
use std::mem::MaybeUninit;
use anyhow::{bail, format_err, Error};
use openssl::ec::{EcGroup, EcKey, EcPoint};
use openssl::ecdsa::EcdsaSig;
use openssl::pkey::Public;
use openssl::sha;
use openssl::x509::X509;
use serde::{Deserialize, Serialize};
use crate::tools::serde::{bytes_as_base64, bytes_as_base64url_nopad};
const CHALLENGE_LEN: usize = 32;
const U2F_VERSION: &str = "U2F_V2";
/// The "key" part of a registration, passed to `u2f.sign` in the registered keys list.
///
/// Part of the U2F API, therefore `camelCase` and base64url without padding.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisteredKey {
/// Identifies the key handle on the client side. Used to create authentication challenges, so
/// the client knows which key to use. Must be remembered.
#[serde(with = "bytes_as_base64url_nopad")]
pub key_handle: Vec<u8>,
pub version: String,
}
/// Data we get when a u2f token responds to a registration challenge.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Registration {
/// The key consisting of key handle and version, which can be passed to the registered-keys
/// list in `u2f.sign` in the browser.
pub key: RegisteredKey,
/// Public part of the client key identified via the `key_handle`. Required to verify future
/// authentication responses. Must be remembered.
#[serde(with = "bytes_as_base64")]
pub public_key: Vec<u8>,
/// Attestation certificate (in DER format) from which we originally copied the `key_handle`.
/// Not necessary for authentication, unless the hardware tokens should be restricted to
/// specific provider identities. Optional.
#[serde(with = "bytes_as_base64")]
pub certificate: Vec<u8>,
}
/// Result from a successful authentication. The client's hardware token will inform us about the
/// user-presence (it may have been configured to respond automatically instead of requiring user
/// interaction), and the number of authentications the key has performed.
/// We probably won't make much use of this.
#[derive(Clone, Debug)]
pub struct Authentication {
/// `true` if the user had to be present.
pub user_present: bool,
/// authentication count
pub counter: usize,
}
/// The hardware replies with a client data json object containing some information - this is the
/// subset we actually make use of.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ClientData {
/// The challenge the device responded to. This should be compared against the server side
/// cached challenge!
challenge: String,
/// The origin the the browser told the device the challenge was coming from.
origin: String,
}
/// A registration challenge to be sent to the `u2f.register` function in the browser.
///
/// Part of the U2F API, therefore `camelCase`.
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RegistrationChallenge {
pub challenge: String,
pub version: String,
pub app_id: String,
}
/// The response we get from a successful call to the `u2f.register` function in the browser.
///
/// Part of the U2F API, therefore `camelCase`.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegistrationResponse {
registration_data: String,
client_data: String,
version: String,
}
/// Authentication challenge data to be sent to the `u2f.sign` function in the browser. Does not
/// include the registered keys.
///
/// Part of the U2F API, therefore `camelCase`.
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthChallenge {
pub challenge: String,
pub app_id: String,
}
/// The response we get from a successful call to the `u2f.sign` function in the browser.
///
/// Part of the U2F API, therefore `camelCase` and base64url without padding.
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthResponse {
#[serde(with = "bytes_as_base64url_nopad")]
key_handle: Vec<u8>,
client_data: String,
signature_data: String,
}
impl AuthResponse {
pub fn key_handle(&self) -> &[u8] {
&self.key_handle
}
}
/// A u2f context to create or verify challenges with.
#[derive(Deserialize, Serialize)]
pub struct U2f {
app_id: String,
origin: String,
}
impl U2f {
/// Create a new U2F context consisting of an appid and origin.
pub fn new(app_id: String, origin: String) -> Self {
Self { app_id, origin }
}
/// Get a challenge object which can be directly passed to `u2f.register` on the browser side.
pub fn registration_challenge(&self) -> Result<RegistrationChallenge, Error> {
Ok(RegistrationChallenge {
challenge: challenge()?,
version: U2F_VERSION.to_owned(),
app_id: self.app_id.clone(),
})
}
/// Convenience method to verify the json formatted response object string.
pub fn registration_verify(
&self,
challenge: &str,
response: &str,
) -> Result<Option<Registration>, Error> {
let response: RegistrationResponse = serde_json::from_str(response)
.map_err(|err| format_err!("error parsing response: {}", err))?;
self.registration_verify_obj(challenge, response)
}
/// Verifies the registration response object.
pub fn registration_verify_obj(
&self,
challenge: &str,
response: RegistrationResponse,
) -> Result<Option<Registration>, Error> {
let client_data_decoded = decode(&response.client_data)
.map_err(|err| format_err!("error decoding client data in response: {}", err))?;
let client_data: ClientData = serde_json::from_reader(&mut &client_data_decoded[..])
.map_err(|err| format_err!("error parsing client data: {}", err))?;
if client_data.challenge != challenge {
bail!("registration challenge did not match");
}
if client_data.origin != self.origin {
bail!(
"origin in client registration did not match: {:?} != {:?}",
client_data.origin,
self.origin,
);
}
let registration_data = decode(&response.registration_data)
.map_err(|err| format_err!("error decoding registration data in response: {}", err))?;
let registration_data = RegistrationResponseData::from_raw(&registration_data)?;
let mut digest = sha::Sha256::new();
digest.update(&[0u8]);
digest.update(&sha::sha256(self.app_id.as_bytes()));
digest.update(&sha::sha256(&client_data_decoded));
digest.update(registration_data.key_handle);
digest.update(registration_data.public_key);
let digest = digest.finish();
let signature = EcdsaSig::from_der(registration_data.signature)
.map_err(|err| format_err!("error decoding signature in response: {}", err))?;
// can we decode the public key?
drop(decode_public_key(registration_data.public_key)?);
match signature.verify(&digest, &registration_data.cert_key) {
Ok(true) => Ok(Some(Registration {
key: RegisteredKey {
key_handle: registration_data.key_handle.to_vec(),
version: response.version,
},
public_key: registration_data.public_key.to_vec(),
certificate: registration_data.certificate.to_vec(),
})),
Ok(false) => Ok(None),
Err(err) => bail!("openssl error while verifying signature: {}", err),
}
}
/// Get a challenge object which can be directly passwd to `u2f.sign` on the browser side.
pub fn auth_challenge(&self) -> Result<AuthChallenge, Error> {
Ok(AuthChallenge {
challenge: challenge()?,
app_id: self.app_id.clone(),
})
}
/// Convenience method to verify the json formatted response object string.
pub fn auth_verify(
&self,
public_key: &[u8],
challenge: &str,
response: &str,
) -> Result<Option<Authentication>, Error> {
let response: AuthResponse = serde_json::from_str(response)
.map_err(|err| format_err!("error parsing response: {}", err))?;
self.auth_verify_obj(public_key, challenge, response)
}
/// Verifies the authentication response object.
pub fn auth_verify_obj(
&self,
public_key: &[u8],
challenge: &str,
response: AuthResponse,
) -> Result<Option<Authentication>, Error> {
let client_data_decoded = decode(&response.client_data)
.map_err(|err| format_err!("error decoding client data in response: {}", err))?;
let client_data: ClientData = serde_json::from_reader(&mut &client_data_decoded[..])
.map_err(|err| format_err!("error parsing client data: {}", err))?;
if client_data.challenge != challenge {
bail!("authentication challenge did not match");
}
if client_data.origin != self.origin {
bail!(
"origin in client authentication did not match: {:?} != {:?}",
client_data.origin,
self.origin,
);
}
let signature_data = decode(&response.signature_data)
.map_err(|err| format_err!("error decoding signature data in response: {}", err))?;
// an ecdsa signature is much longer than 16 bytes but we only need to parse the first 5
// anyway...
if signature_data.len() < 1 + 4 + 16 {
bail!("invalid signature data");
}
let presence_and_counter_bytes = &signature_data[0..5];
let user_present = presence_and_counter_bytes[0] != 0;
let counter_bytes = &presence_and_counter_bytes[1..];
let counter: u32 =
u32::from_be(unsafe { std::ptr::read_unaligned(counter_bytes.as_ptr() as *const u32) });
let signature = EcdsaSig::from_der(&signature_data[5..])
.map_err(|err| format_err!("error decoding signature in response: {}", err))?;
let public_key = decode_public_key(public_key)?;
let mut digest = sha::Sha256::new();
digest.update(&sha::sha256(self.app_id.as_bytes()));
digest.update(presence_and_counter_bytes);
digest.update(&sha::sha256(&client_data_decoded));
let digest = digest.finish();
match signature.verify(&digest, &public_key) {
Ok(true) => Ok(Some(Authentication {
user_present,
counter: counter as usize,
})),
Ok(false) => Ok(None),
Err(err) => bail!("openssl error while verifying signature: {}", err),
}
}
}
/// base64url encoding
fn encode(data: &[u8]) -> String {
let mut out = base64::encode_config(data, base64::URL_SAFE_NO_PAD);
while out.ends_with('=') {
out.pop();
}
out
}
/// base64url decoding
fn decode(data: &str) -> Result<Vec<u8>, Error> {
Ok(base64::decode_config(data, base64::URL_SAFE_NO_PAD)?)
}
/// produce a challenge, which is just a bunch of random data
fn challenge() -> Result<String, Error> {
let mut data = MaybeUninit::<[u8; CHALLENGE_LEN]>::uninit();
Ok(encode(&unsafe {
crate::sys::linux::fill_with_random_data(&mut *data.as_mut_ptr())?;
data.assume_init()
}))
}
/// Used while parsing the binary registration response. The slices point directly into the
/// original response byte data, and the public key is extracted from the contained X509
/// certificate.
#[derive(Debug)]
pub struct RegistrationResponseData<'a> {
public_key: &'a [u8],
key_handle: &'a [u8],
certificate: &'a [u8],
signature: &'a [u8],
/// The client's public key in decoded and parsed form.
cert_key: EcKey<Public>,
}
impl<'a> RegistrationResponseData<'a> {
/// Parse the binary registration data into its parts and extract the certificate's public key.
///
/// See https://fidoalliance.org/specs/fido-u2f-v1.2-ps-20170411/fido-u2f-raw-message-formats-v1.2-ps-20170411.html
pub fn from_raw(data: &'a [u8]) -> Result<Self, Error> {
// [ 0x05 | 65b pubkey | 1b keyhandle len | keyhandle | certificate (1 DER obj) | signature ]
if data.len() <= (1 + 65 + 1 + 71) {
bail!("registration data too short");
}
if data[0] != 0x05 {
bail!(
"invalid registration data, reserved byte is 0x{:02x}, expected 0x05",
data[0]
);
}
let public_key = &data[1..66];
let key_handle_len = usize::from(data[66]);
let data = &data[67..];
if data.len() <= key_handle_len + 71 {
bail!("registration data invalid too short");
}
let key_handle = &data[..key_handle_len];
let data = &data[key_handle_len..];
if data[0] != 0x30 {
bail!("error decoding X509 certificate: not a SEQUENCE tag");
}
let cert_len = der_length(&data[1..])? + 1; // plus the tag!
let certificate = &data[..cert_len];
let x509 = X509::from_der(certificate)
.map_err(|err| format_err!("error decoding X509 certificate: {}", err))?;
let signature = &data[cert_len..];
Ok(Self {
public_key,
key_handle,
certificate,
signature,
cert_key: x509.public_key()?.ec_key()?,
})
}
}
/// Decode the raw 65 byte ec public key into an `openssl::EcKey<Public>`.
fn decode_public_key(data: &[u8]) -> Result<EcKey<Public>, Error> {
if data.len() != 65 {
bail!("invalid public key length {}, expected 65", data.len());
}
let group = EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)
.map_err(|err| format_err!("openssl error, failed to instantiate ec curve: {}", err))?;
let mut bn = openssl::bn::BigNumContext::new().map_err(|err| {
format_err!(
"openssl error, failed to instantiate bignum context: {}",
err
)
})?;
let point = EcPoint::from_bytes(&group, data, &mut bn)
.map_err(|err| format_err!("failed to decode public key point: {}", err))?;
let key = EcKey::from_public_key(&group, &point)
.map_err(|err| format_err!("failed to instantiate public key: {}", err))?;
key.check_key()
.map_err(|err| format_err!("public key failed self check: {}", err))?;
Ok(key)
}
/// The only DER thing we need: lengths.
///
/// Returns the length *including* the size of the length itself.
fn der_length(data: &[u8]) -> Result<usize, Error> {
if data[0] == 0 {
bail!("error decoding X509 certificate: bad length (0)");
}
if data[0] < 0x80 {
return Ok(usize::from(data[0]) + 1);
}
let count = usize::from(data[0] & 0x7F);
if count == 0x7F {
// X.609; 8.1.3.5, the value `1111111` shall not be used
bail!("error decoding X509 certificate: illegal length value");
}
if count == 0 {
// "indefinite" form not allowed in DER
bail!("error decoding X509 certificate: illegal length form");
}
if count > std::mem::size_of::<usize>() {
bail!("error decoding X509 certificate: unsupported length");
}
if count > (data.len() - 1) {
bail!("error decoding X509 certificate: truncated length data");
}
let mut len = 0;
for i in 0..count {
len = (len << 8) | usize::from(data[1 + i]);
}
Ok(len + count + 1)
}
#[cfg(test)]
mod test {
// The test data in here is generated with a yubi key...
use serde::Deserialize;
const TEST_APPID: &str = "https://u2ftest.enonet.errno.eu";
const TEST_REGISTRATION_JSON: &str =
"{\"challenge\":\"mZoWLngnAh8p98nPkFOIBXecd0CbmgEx5tEd5jNswgY\",\"response\":{\"client\
Data\":\"eyJjaGFsbGVuZ2UiOiJtWm9XTG5nbkFoOHA5OG5Qa0ZPSUJYZWNkMENibWdFeDV0RWQ1ak5zd2dZI\
iwib3JpZ2luIjoiaHR0cHM6Ly91MmZ0ZXN0LmVub25ldC5lcnJuby5ldSIsInR5cCI6Im5hdmlnYXRvci5pZC5\
maW5pc2hFbnJvbGxtZW50In0\",\"registrationData\":\"BQR_9TmMowVeoAHp3ABljCa90eNG87t76D4W\
c9nsmK9ihNhhYNxYIq9tnRUPTBZ2X4kZKSB0LXMm32lOKQlNB56QQHlt81cRBfID7BvHk_XIJZc5ks5D3R1ZV1\
1fJudp3F-ii_KSdZaFb4cGaq0rEaVDfNR2ZR0T0ApMMCeTIaDAJRQwggJEMIIBLqADAgECAgRVYr6gMAsGCSqG\
SIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MD\
EwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTQzMjUz\
NDY4ODBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEszH3c9gUS5mVy-RYVRfhdYOqR2I2lcvoWsSCyAGfLJuU\
Z64EWw5m8TGy6jJDyR_aYC4xjz_F2NKnq65yvRQwmjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0\
ODIuMS41MBMGCysGAQQBguUcAgEBBAQDAgUgMAsGCSqGSIb3DQEBCwOCAQEArBbZs262s6m3bXWUs09Z9Pc-28\
n96yk162tFHKv0HSXT5xYU10cmBMpypXjjI-23YARoXwXn0bm-BdtulED6xc_JMqbK-uhSmXcu2wJ4ICA81BQd\
PutvaizpnjlXgDJjq6uNbsSAp98IStLLp7fW13yUw-vAsWb5YFfK9f46Yx6iakM3YqNvvs9M9EUJYl_VrxBJqn\
yLx2iaZlnpr13o8NcsKIJRdMUOBqt_ageQg3ttsyq_3LyoNcu7CQ7x8NmeCGm_6eVnZMQjDmwFdymwEN4OxfnM\
5MkcKCYhjqgIGruWkVHsFnJa8qjZXneVvKoiepuUQyDEJ2GcqvhU2YKY1zBFAiEA2mcfAS2XRcWy1lLJikFHGJ\
SbtOrrwswjOKEzwp6EonkCIFBxbLAmwUnblAWOVELASi610ZfPK-7qx2VwkWfHqnll\",\"version\":\"U2F\
_V2\"}}";
const TEST_AUTH_JSON: &str =
"{\"challenge\":\"8LE_-7Rd1vB3Otn3vJ7GyiwRQtYPMv-BWliCejH0d4Y\",\"response\":{\"clientD\
ata\":\"eyJjaGFsbGVuZ2UiOiI4TEVfLTdSZDF2QjNPdG4zdko3R3lpd1JRdFlQTXYtQldsaUNlakgwZDRZIiw\
ib3JpZ2luIjoiaHR0cHM6Ly91MmZ0ZXN0LmVub25ldC5lcnJuby5ldSIsInR5cCI6Im5hdmlnYXRvci5pZC5nZX\
RBc3NlcnRpb24ifQ\",\"keyHandle\":\"eW3zVxEF8gPsG8eT9cgllzmSzkPdHVlXXV8m52ncX6KL8pJ1loVv\
hwZqrSsRpUN81HZlHRPQCkwwJ5MhoMAlFA\",\"signatureData\":\"AQAAAQEwRAIgKdM9cmCLZDxntY-dT_\
OXbcVA1D5ewQunXVC-CYZ65pUCIAIOUBsu-dOmTym0ITZt6x75BFUSGlqYRuH5JKBcyO3M\"},\"user\":{\"c\
ertificate\":\"MIICRDCCAS6gAwIBAgIEVWK+oDALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUy\
RiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDA\
mBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDE0MzI1MzQ2ODgwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAA\
RLMx93PYFEuZlcvkWFUX4XWDqkdiNpXL6FrEgsgBnyyblGeuBFsOZvExsuoyQ8kf2mAuMY8/xdjSp6uucr0UMJo\
zswOTAiBgkrBgEEAYLECgIEFTEuMy42LjEuNC4xLjQxNDgyLjEuNTATBgsrBgEEAYLlHAIBAQQEAwIFIDALBgkq\
hkiG9w0BAQsDggEBAKwW2bNutrOpt211lLNPWfT3PtvJ/espNetrRRyr9B0l0+cWFNdHJgTKcqV44yPtt2AEaF8\
F59G5vgXbbpRA+sXPyTKmyvroUpl3LtsCeCAgPNQUHT7rb2os6Z45V4AyY6urjW7EgKffCErSy6e31td8lMPrwL\
Fm+WBXyvX+OmMeompDN2Kjb77PTPRFCWJf1a8QSap8i8dommZZ6a9d6PDXLCiCUXTFDgarf2oHkIN7bbMqv9y8q\
DXLuwkO8fDZnghpv+nlZ2TEIw5sBXcpsBDeDsX5zOTJHCgmIY6oCBq7lpFR7BZyWvKo2V53lbyqInqblEMgxCdh\
nKr4VNmCmNc=\",\"key\":{\"keyHandle\":\"eW3zVxEF8gPsG8eT9cgllzmSzkPdHVlXXV8m52ncX6KL8pJ\
1loVvhwZqrSsRpUN81HZlHRPQCkwwJ5MhoMAlFA\",\"version\":\"U2F_V2\"},\"public-key\":\"BH/1\
OYyjBV6gAencAGWMJr3R40bzu3voPhZz2eyYr2KE2GFg3Fgir22dFQ9MFnZfiRkpIHQtcybfaU4pCU0HnpA=\"}\
}";
#[test]
fn test_registration() {
let data = TEST_REGISTRATION_JSON;
#[derive(Deserialize)]
struct TestChallenge {
challenge: String,
response: super::RegistrationResponse,
}
let ts: TestChallenge =
serde_json::from_str(&data).expect("failed to parse json test data");
let context = super::U2f::new(TEST_APPID.to_string(), TEST_APPID.to_string());
let res = context
.registration_verify_obj(&ts.challenge, ts.response)
.expect("error trying to verify registration");
assert!(
res.is_some(),
"test registration signature fails verification"
);
}
#[test]
fn test_authentication() {
let data = TEST_AUTH_JSON;
#[derive(Deserialize)]
struct TestChallenge {
challenge: String,
user: super::Registration,
response: super::AuthResponse,
}
let ts: TestChallenge =
serde_json::from_str(&data).expect("failed to parse json test data");
let context = super::U2f::new(TEST_APPID.to_string(), TEST_APPID.to_string());
let res = context
.auth_verify_obj(&ts.user.public_key, &ts.challenge, ts.response)
.expect("error trying to verify authentication");
assert!(
res.is_some(),
"test authentication signature fails verification"
);
}
}

748
proxmox/u2f-api.js Normal file
View File

@ -0,0 +1,748 @@
//Copyright 2014-2015 Google Inc. All rights reserved.
//Use of this source code is governed by a BSD-style
//license that can be found in the LICENSE file or at
//https://developers.google.com/open-source/licenses/bsd
/**
* @fileoverview The U2F api.
*/
'use strict';
/**
* Namespace for the U2F api.
* @type {Object}
*/
var u2f = u2f || {};
/**
* FIDO U2F Javascript API Version
* @number
*/
var js_api_version;
/**
* The U2F extension id
* @const {string}
*/
// The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the package Chrome app and does not require installing the U2F Chrome extension.
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
// The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
/**
* Message types for messsages to/from the extension
* @const
* @enum {string}
*/
u2f.MessageTypes = {
'U2F_REGISTER_REQUEST': 'u2f_register_request',
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
'U2F_SIGN_REQUEST': 'u2f_sign_request',
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
};
/**
* Response status codes
* @const
* @enum {number}
*/
u2f.ErrorCodes = {
'OK': 0,
'OTHER_ERROR': 1,
'BAD_REQUEST': 2,
'CONFIGURATION_UNSUPPORTED': 3,
'DEVICE_INELIGIBLE': 4,
'TIMEOUT': 5
};
/**
* A message for registration requests
* @typedef {{
* type: u2f.MessageTypes,
* appId: ?string,
* timeoutSeconds: ?number,
* requestId: ?number
* }}
*/
u2f.U2fRequest;
/**
* A message for registration responses
* @typedef {{
* type: u2f.MessageTypes,
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
* requestId: ?number
* }}
*/
u2f.U2fResponse;
/**
* An error object for responses
* @typedef {{
* errorCode: u2f.ErrorCodes,
* errorMessage: ?string
* }}
*/
u2f.Error;
/**
* Data object for a single sign request.
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
*/
u2f.Transport;
/**
* Data object for a single sign request.
* @typedef {Array<u2f.Transport>}
*/
u2f.Transports;
/**
* Data object for a single sign request.
* @typedef {{
* version: string,
* challenge: string,
* keyHandle: string,
* appId: string
* }}
*/
u2f.SignRequest;
/**
* Data object for a sign response.
* @typedef {{
* keyHandle: string,
* signatureData: string,
* clientData: string
* }}
*/
u2f.SignResponse;
/**
* Data object for a registration request.
* @typedef {{
* version: string,
* challenge: string
* }}
*/
u2f.RegisterRequest;
/**
* Data object for a registration response.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: Transports,
* appId: string
* }}
*/
u2f.RegisterResponse;
/**
* Data object for a registered key.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: ?Transports,
* appId: ?string
* }}
*/
u2f.RegisteredKey;
/**
* Data object for a get API register response.
* @typedef {{
* js_api_version: number
* }}
*/
u2f.GetJsApiVersionResponse;
//Low level MessagePort API support
/**
* Sets up a MessagePort to the U2F extension using the
* available mechanisms.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/
u2f.getMessagePort = function(callback) {
if (typeof chrome != 'undefined' && chrome.runtime) {
// The actual message here does not matter, but we need to get a reply
// for the callback to run. Thus, send an empty signature request
// in order to get a failure response.
var msg = {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: []
};
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
if (!chrome.runtime.lastError) {
// We are on a whitelisted origin and can talk directly
// with the extension.
u2f.getChromeRuntimePort_(callback);
} else {
// chrome.runtime was available, but we couldn't message
// the extension directly, use iframe
u2f.getIframePort_(callback);
}
});
} else if (u2f.isAndroidChrome_()) {
u2f.getAuthenticatorPort_(callback);
} else if (u2f.isIosChrome_()) {
u2f.getIosPort_(callback);
} else {
// chrome.runtime was not available at all, which is normal
// when this origin doesn't have access to any extensions.
u2f.getIframePort_(callback);
}
};
/**
* Detect chrome running on android based on the browser's useragent.
* @private
*/
u2f.isAndroidChrome_ = function() {
var userAgent = navigator.userAgent;
return userAgent.indexOf('Chrome') != -1 &&
userAgent.indexOf('Android') != -1;
};
/**
* Detect chrome running on iOS based on the browser's platform.
* @private
*/
u2f.isIosChrome_ = function() {
return $.inArray(navigator.platform, ["iPhone", "iPad", "iPod"]) > -1;
};
/**
* Connects directly to the extension via chrome.runtime.connect.
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private
*/
u2f.getChromeRuntimePort_ = function(callback) {
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
{'includeTlsChannelId': true});
setTimeout(function() {
callback(new u2f.WrappedChromeRuntimePort_(port));
}, 0);
};
/**
* Return a 'port' abstraction to the Authenticator app.
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private
*/
u2f.getAuthenticatorPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedAuthenticatorPort_());
}, 0);
};
/**
* Return a 'port' abstraction to the iOS client app.
* @param {function(u2f.WrappedIosPort_)} callback
* @private
*/
u2f.getIosPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedIosPort_());
}, 0);
};
/**
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
* @param {Port} port
* @constructor
* @private
*/
u2f.WrappedChromeRuntimePort_ = function(port) {
this.port_ = port;
};
/**
* Format and return a sign request compliant with the JS API version supported by the extension.
* @param {Array<u2f.SignRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatSignRequest_ =
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: challenge,
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: signRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
appId: appId,
challenge: challenge,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Format and return a register request compliant with the JS API version supported by the extension..
* @param {Array<u2f.SignRequest>} signRequests
* @param {Array<u2f.RegisterRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatRegisterRequest_ =
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
for (var i = 0; i < registerRequests.length; i++) {
registerRequests[i].appId = appId;
}
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: registerRequests[0],
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
signRequests: signRequests,
registerRequests: registerRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
appId: appId,
registerRequests: registerRequests,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Posts a message on the underlying channel.
* @param {Object} message
*/
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
this.port_.postMessage(message);
};
/**
* Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message' || name == 'onmessage') {
this.port_.onMessage.addListener(function(message) {
// Emulate a minimal MessageEvent object
handler({'data': message});
});
} else {
console.error('WrappedChromeRuntimePort only supports onMessage');
}
};
/**
* Wrap the Authenticator app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedAuthenticatorPort_ = function() {
this.requestId_ = -1;
this.requestObject_ = null;
}
/**
* Launch the Authenticator intent.
* @param {Object} message
*/
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
var intentUrl =
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
';end';
document.location = intentUrl;
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
return "WrappedAuthenticatorPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message') {
var self = this;
/* Register a callback to that executes when
* chrome injects the response. */
window.addEventListener(
'message', self.onRequestUpdate_.bind(self, handler), false);
} else {
console.error('WrappedAuthenticatorPort only supports message');
}
};
/**
* Callback invoked when a response is received from the Authenticator.
* @param function({data: Object}) callback
* @param {Object} message message Object
*/
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
function(callback, message) {
var messageObject = JSON.parse(message.data);
var intentUrl = messageObject['intentURL'];
var errorCode = messageObject['errorCode'];
var responseObject = null;
if (messageObject.hasOwnProperty('data')) {
responseObject = /** @type {Object} */ (
JSON.parse(messageObject['data']));
}
callback({'data': responseObject});
};
/**
* Base URL for intents to Authenticator.
* @const
* @private
*/
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
/**
* Wrap the iOS client app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedIosPort_ = function() {};
/**
* Launch the iOS client app request
* @param {Object} message
*/
u2f.WrappedIosPort_.prototype.postMessage = function(message) {
var str = JSON.stringify(message);
var url = "u2f://auth?" + encodeURI(str);
location.replace(url);
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedIosPort_.prototype.getPortType = function() {
return "WrappedIosPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name !== 'message') {
console.error('WrappedIosPort only supports message');
}
};
/**
* Sets up an embedded trampoline iframe, sourced from the extension.
* @param {function(MessagePort)} callback
* @private
*/
u2f.getIframePort_ = function(callback) {
// Create the iframe
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
var iframe = document.createElement('iframe');
iframe.src = iframeOrigin + '/u2f-comms.html';
iframe.setAttribute('style', 'display:none');
document.body.appendChild(iframe);
var channel = new MessageChannel();
var ready = function(message) {
if (message.data == 'ready') {
channel.port1.removeEventListener('message', ready);
callback(channel.port1);
} else {
console.error('First event on iframe port was not "ready"');
}
};
channel.port1.addEventListener('message', ready);
channel.port1.start();
iframe.addEventListener('load', function() {
// Deliver the port to the iframe and initialize
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
});
};
//High-level JS API
/**
* Default extension response timeout in seconds.
* @const
*/
u2f.EXTENSION_TIMEOUT_SEC = 30;
/**
* A singleton instance for a MessagePort to the extension.
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
* @private
*/
u2f.port_ = null;
/**
* Callbacks waiting for a port
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private
*/
u2f.waitingForPort_ = [];
/**
* A counter for requestIds.
* @type {number}
* @private
*/
u2f.reqCounter_ = 0;
/**
* A map from requestIds to client callbacks
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
* |function((u2f.Error|u2f.SignResponse)))>}
* @private
*/
u2f.callbackMap_ = {};
/**
* Creates or retrieves the MessagePort singleton to use.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private
*/
u2f.getPortSingleton_ = function(callback) {
if (u2f.port_) {
callback(u2f.port_);
} else {
if (u2f.waitingForPort_.length == 0) {
u2f.getMessagePort(function(port) {
u2f.port_ = port;
u2f.port_.addEventListener('message',
/** @type {function(Event)} */ (u2f.responseHandler_));
// Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length)
u2f.waitingForPort_.shift()(u2f.port_);
});
}
u2f.waitingForPort_.push(callback);
}
};
/**
* Handles response messages from the extension.
* @param {MessageEvent.<u2f.Response>} message
* @private
*/
u2f.responseHandler_ = function(message) {
var response = message.data;
var reqId = response['requestId'];
if (!reqId || !u2f.callbackMap_[reqId]) {
console.error('Unknown or missing requestId in response.');
return;
}
var cb = u2f.callbackMap_[reqId];
delete u2f.callbackMap_[reqId];
cb(response['responseData']);
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the sign request.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual sign request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual sign request in the supported API version.
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
}
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the register request.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual register request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual register request in the supported API version.
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
}
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatRegisterRequest_(
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches a message to the extension to find out the supported
* JS API version.
* If the user is on a mobile phone and is thus using Google Authenticator instead
* of the Chrome extension, don't send the request and simply return 0.
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
// If we are using Android Google Authenticator or iOS client app,
// do not fire an intent to ask which JS API version to use.
if (port.getPortType) {
var apiVersion;
switch (port.getPortType()) {
case 'WrappedIosPort_':
case 'WrappedAuthenticatorPort_':
apiVersion = 1.1;
break;
default:
apiVersion = 0;
break;
}
callback({ 'js_api_version': apiVersion });
return;
}
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var req = {
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
requestId: reqId
};
port.postMessage(req);
});
};