mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 08:44:40 +01:00
Add import of Raivo OTP files
This adds support for importing exports from [Raivo OTP], which are AES-encrypted ZIP archives. [Raivo OTP]: https://raivo-otp.com/
This commit is contained in:
parent
cfeb8797e7
commit
b2e2f3e5ed
6 changed files with 284 additions and 6 deletions
124
Cargo.lock
generated
124
Cargo.lock
generated
|
@ -278,6 +278,7 @@ dependencies = [
|
|||
"uuid",
|
||||
"zbar-rust",
|
||||
"zeroize",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -307,6 +308,12 @@ version = "0.21.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
|
@ -327,7 +334,7 @@ checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780"
|
|||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"constant_time_eq",
|
||||
"constant_time_eq 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -400,6 +407,27 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.11+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.18.2"
|
||||
|
@ -471,6 +499,7 @@ version = "1.0.83"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
|
@ -534,6 +563,12 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.0"
|
||||
|
@ -1890,6 +1925,15 @@ version = "1.0.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.64"
|
||||
|
@ -2285,7 +2329,7 @@ dependencies = [
|
|||
"num",
|
||||
"num-bigint-dig",
|
||||
"once_cell",
|
||||
"pbkdf2",
|
||||
"pbkdf2 0.12.2",
|
||||
"rand",
|
||||
"serde",
|
||||
"sha2",
|
||||
|
@ -2418,12 +2462,35 @@ dependencies = [
|
|||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"hmac",
|
||||
"password-hash",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
|
@ -2774,7 +2841,7 @@ checksum = "1e71971821b3ae0e769e4a4328dbcb517607b434db7697e9aba17203ec14e46a"
|
|||
dependencies = [
|
||||
"base64",
|
||||
"blake2b_simd",
|
||||
"constant_time_eq",
|
||||
"constant_time_eq 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2873,7 +2940,7 @@ version = "0.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"pbkdf2",
|
||||
"pbkdf2 0.12.2",
|
||||
"salsa20",
|
||||
"sha2",
|
||||
]
|
||||
|
@ -3845,6 +3912,55 @@ dependencies = [
|
|||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"byteorder",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.1.5",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
"hmac",
|
||||
"pbkdf2 0.11.0",
|
||||
"sha1",
|
||||
"time",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.11.2+zstd.1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "5.0.2+zstd.1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.9+zstd.1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "3.15.0"
|
||||
|
|
|
@ -48,3 +48,4 @@ url = "2.2"
|
|||
uuid = {version = "1.0", features = ["v4"]}
|
||||
zbar-rust = "0.0"
|
||||
zeroize = {version = "1", features = ["zeroize_derive"]}
|
||||
zip = { version = "0.6.6", features = ["aes-crypto"] }
|
||||
|
|
|
@ -93,7 +93,8 @@ mod freeotp;
|
|||
mod freeotp_json;
|
||||
mod google;
|
||||
mod legacy;
|
||||
mod raivootp;
|
||||
pub use self::{
|
||||
aegis::Aegis, andotp::AndOTP, bitwarden::Bitwarden, freeotp::FreeOTP,
|
||||
freeotp_json::FreeOTPJSON, google::Google, legacy::LegacyAuthenticator,
|
||||
freeotp_json::FreeOTPJSON, google::Google, legacy::LegacyAuthenticator, raivootp::RaivoOTP,
|
||||
};
|
||||
|
|
159
src/backup/raivootp.rs
Normal file
159
src/backup/raivootp.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use anyhow::Result;
|
||||
use gettextrs::gettext;
|
||||
use serde::{de::Deserializer, Deserialize, Serialize};
|
||||
use std::io::Cursor;
|
||||
use zip::{self, ZipArchive};
|
||||
|
||||
use super::{Restorable, RestorableItem};
|
||||
use crate::models::{Algorithm, Method};
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RaivoOTP;
|
||||
|
||||
/// A Raivo OTP entry.
|
||||
///
|
||||
/// [See Raivo's source code for each item's serialized form.][0]
|
||||
///
|
||||
/// [0]: https://github.com/raivo-otp/ios-application/blob/3a8aaa0ea16a761e6205abd2700ac90dd4c9c9b6/Raivo/Models/Password.swift#L104-L116
|
||||
#[derive(Deserialize)]
|
||||
pub struct Item {
|
||||
issuer: String,
|
||||
account: String,
|
||||
secret: String,
|
||||
algorithm: Algorithm,
|
||||
#[serde(deserialize_with = "deserialize_raivo_u32")]
|
||||
digits: Option<u32>,
|
||||
#[serde(rename = "kind")]
|
||||
method: Method,
|
||||
#[serde(rename = "timer")]
|
||||
#[serde(deserialize_with = "deserialize_raivo_u32")]
|
||||
period: Option<u32>,
|
||||
#[serde(deserialize_with = "deserialize_raivo_u32")]
|
||||
counter: Option<u32>,
|
||||
}
|
||||
|
||||
fn deserialize_raivo_u32<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let n: u32 = String::deserialize(deserializer)?
|
||||
.parse()
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
Ok(Some(n))
|
||||
}
|
||||
|
||||
impl RestorableItem for Item {
|
||||
fn account(&self) -> String {
|
||||
self.account.clone()
|
||||
}
|
||||
|
||||
fn issuer(&self) -> String {
|
||||
self.issuer.clone()
|
||||
}
|
||||
|
||||
fn secret(&self) -> String {
|
||||
self.secret.clone()
|
||||
}
|
||||
|
||||
fn period(&self) -> Option<u32> {
|
||||
match self.method() {
|
||||
Method::TOTP => self.period,
|
||||
Method::HOTP | Method::Steam => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn method(&self) -> Method {
|
||||
self.method
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> Algorithm {
|
||||
self.algorithm
|
||||
}
|
||||
|
||||
fn digits(&self) -> Option<u32> {
|
||||
self.digits
|
||||
}
|
||||
|
||||
fn counter(&self) -> Option<u32> {
|
||||
match self.method() {
|
||||
Method::HOTP => self.counter,
|
||||
Method::TOTP | Method::Steam => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Restorable for RaivoOTP {
|
||||
const ENCRYPTABLE: bool = true;
|
||||
const SCANNABLE: bool = false;
|
||||
const IDENTIFIER: &'static str = "raivootp";
|
||||
type Item = Item;
|
||||
|
||||
fn title() -> String {
|
||||
gettext("Raivo OTP")
|
||||
}
|
||||
|
||||
fn subtitle() -> String {
|
||||
gettext("From a ZIP export generated by Raivo OTP")
|
||||
}
|
||||
|
||||
/// Restore from a ZIP file generated by Raivo OTP.
|
||||
///
|
||||
/// See Raivo's source code for [exporting the AES-encrypted ZIP][0].
|
||||
///
|
||||
/// [0]: https://github.com/raivo-otp/ios-application/blob/3a8aaa0ea16a761e6205abd2700ac90dd4c9c9b6/Raivo/Features/DataExportFeature.swift#L188-L195
|
||||
fn restore_from_data(from: &[u8], key: Option<&str>) -> Result<Vec<Self::Item>> {
|
||||
let password: &[u8] = match key {
|
||||
None => &[],
|
||||
Some(k) => k.as_bytes(),
|
||||
};
|
||||
let mut archive = ZipArchive::new(Cursor::new(from))?;
|
||||
let file = archive.by_name_decrypt("raivo-otp-export.json", password)??;
|
||||
let items = serde_json::from_reader(file)?;
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{super::RestorableItem, *};
|
||||
use crate::models::{Algorithm, Method};
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
|
||||
let items = RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).unwrap();
|
||||
|
||||
assert_eq!(items.len(), 2);
|
||||
|
||||
assert_eq!(items[0].account(), "mason");
|
||||
assert_eq!(items[0].issuer(), "Example A");
|
||||
assert_eq!(items[0].secret(), "ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789");
|
||||
assert_eq!(items[0].period(), Some(30));
|
||||
assert_eq!(items[0].method(), Method::TOTP);
|
||||
assert_eq!(items[0].algorithm(), Algorithm::SHA1);
|
||||
assert_eq!(items[0].digits(), Some(6));
|
||||
assert_eq!(items[0].counter(), None);
|
||||
|
||||
assert_eq!(items[1].account(), "james");
|
||||
assert_eq!(items[1].issuer(), "Example B");
|
||||
assert_eq!(items[1].secret(), "12345678ABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
||||
assert_eq!(items[1].period(), Some(123));
|
||||
assert_eq!(items[1].method(), Method::TOTP);
|
||||
assert_eq!(items[1].algorithm(), Algorithm::SHA256);
|
||||
assert_eq!(items[1].digits(), Some(8));
|
||||
assert_eq!(items[1].counter(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_zip() {
|
||||
let data: [u8; 3] = [1, 2, 3];
|
||||
assert!(RaivoOTP::restore_from_data(&data, Some("RaivoTest123")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_password() {
|
||||
let data = std::fs::read("./src/backup/tests/raivootp.zip").unwrap();
|
||||
assert!(RaivoOTP::restore_from_data(&data, Some("bad password")).is_err());
|
||||
}
|
||||
}
|
BIN
src/backup/tests/raivootp.zip
Normal file
BIN
src/backup/tests/raivootp.zip
Normal file
Binary file not shown.
|
@ -11,7 +11,7 @@ use super::{camera_page::CameraPage, password_page::PasswordPage};
|
|||
use crate::{
|
||||
backup::{
|
||||
Aegis, AndOTP, Backupable, Bitwarden, FreeOTP, FreeOTPJSON, Google, LegacyAuthenticator,
|
||||
Operation, Restorable, RestorableItem,
|
||||
Operation, RaivoOTP, Restorable, RestorableItem,
|
||||
},
|
||||
models::{ProvidersModel, SETTINGS},
|
||||
utils::spawn,
|
||||
|
@ -174,6 +174,7 @@ impl PreferencesWindow {
|
|||
self.register_restore::<Bitwarden>(&["application/json"]);
|
||||
self.register_restore::<Google>(&[]);
|
||||
self.register_restore::<LegacyAuthenticator>(&["application/json"]);
|
||||
self.register_restore::<RaivoOTP>(&["application/zip"]);
|
||||
}
|
||||
|
||||
fn register_backup<T: Backupable>(&self, filters: &'static [&str]) {
|
||||
|
|
Loading…
Add table
Reference in a new issue