Skip to main content

bitwarden_vault/cipher/blob/
v1.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4use crate::cipher::{
5    field::FieldType, linked_id::LinkedIdType, login::UriMatchType, secure_note::SecureNoteType,
6};
7
8#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
9#[serde(rename_all = "camelCase")]
10pub(crate) struct CipherBlobV1 {
11    pub name: String,
12    pub notes: Option<String>,
13    pub type_data: CipherTypeDataV1,
14    #[serde(default, skip_serializing_if = "Vec::is_empty")]
15    pub fields: Vec<FieldDataV1>,
16    #[serde(default, skip_serializing_if = "Vec::is_empty")]
17    pub password_history: Vec<PasswordHistoryDataV1>,
18}
19
20impl bitwarden_crypto::safe::SealableData for CipherBlobV1 {}
21
22// IdentityDataV1 is significantly larger than other variants (432 bytes vs ~144 bytes).
23// Boxing could be considered if this becomes a performance concern, but since these types are
24// only constructed during deserialization from encrypted blobs, the stack size is acceptable.
25#[allow(clippy::large_enum_variant)]
26#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
27#[serde(tag = "type", rename_all = "camelCase")]
28pub(crate) enum CipherTypeDataV1 {
29    Login(LoginDataV1),
30    Card(CardDataV1),
31    Identity(IdentityDataV1),
32    SecureNote(SecureNoteDataV1),
33    SshKey(SshKeyDataV1),
34    BankAccount(BankAccountDataV1),
35    Passport(PassportDataV1),
36    DriversLicense(DriversLicenseDataV1),
37}
38
39#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
40#[serde(rename_all = "camelCase")]
41pub(crate) struct LoginDataV1 {
42    pub username: Option<String>,
43    pub password: Option<String>,
44    pub password_revision_date: Option<DateTime<Utc>>,
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub uris: Vec<LoginUriDataV1>,
47    pub totp: Option<String>,
48    pub autofill_on_page_load: Option<bool>,
49    #[serde(default, skip_serializing_if = "Vec::is_empty")]
50    pub fido2_credentials: Vec<Fido2CredentialDataV1>,
51}
52
53#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
54#[serde(rename_all = "camelCase")]
55pub(crate) struct LoginUriDataV1 {
56    pub uri: Option<String>,
57    pub r#match: Option<UriMatchType>,
58}
59
60#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
61#[serde(rename_all = "camelCase")]
62pub(crate) struct Fido2CredentialDataV1 {
63    pub credential_id: String,
64    pub key_type: String,
65    pub key_algorithm: String,
66    pub key_curve: String,
67    pub key_value: String,
68    pub rp_id: String,
69    pub user_handle: Option<String>,
70    pub user_name: Option<String>,
71    pub counter: u64,
72    pub rp_name: Option<String>,
73    pub user_display_name: Option<String>,
74    pub discoverable: bool,
75    pub creation_date: DateTime<Utc>,
76}
77
78#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
79#[serde(rename_all = "camelCase")]
80pub(crate) struct CardDataV1 {
81    pub cardholder_name: Option<String>,
82    pub exp_month: Option<String>,
83    pub exp_year: Option<String>,
84    pub code: Option<String>,
85    pub brand: Option<String>,
86    pub number: Option<String>,
87}
88
89#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
90#[serde(rename_all = "camelCase")]
91pub(crate) struct IdentityDataV1 {
92    pub title: Option<String>,
93    pub first_name: Option<String>,
94    pub middle_name: Option<String>,
95    pub last_name: Option<String>,
96    pub address1: Option<String>,
97    pub address2: Option<String>,
98    pub address3: Option<String>,
99    pub city: Option<String>,
100    pub state: Option<String>,
101    pub postal_code: Option<String>,
102    pub country: Option<String>,
103    pub company: Option<String>,
104    pub email: Option<String>,
105    pub phone: Option<String>,
106    pub ssn: Option<String>,
107    pub username: Option<String>,
108    pub passport_number: Option<String>,
109    pub license_number: Option<String>,
110}
111
112#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
113#[serde(rename_all = "camelCase")]
114pub(crate) struct SecureNoteDataV1 {
115    #[serde(rename = "secureNoteType")]
116    pub r#type: SecureNoteType,
117}
118
119#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
120#[serde(rename_all = "camelCase")]
121pub(crate) struct SshKeyDataV1 {
122    pub private_key: String,
123    pub public_key: String,
124    pub fingerprint: String,
125}
126
127#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
128#[serde(rename_all = "camelCase")]
129pub(crate) struct PassportDataV1 {
130    pub surname: Option<String>,
131    pub given_name: Option<String>,
132    pub date_of_birth: Option<String>,
133    pub sex: Option<String>,
134    pub birth_place: Option<String>,
135    pub nationality: Option<String>,
136    pub issuing_country: Option<String>,
137    pub passport_number: Option<String>,
138    pub passport_type: Option<String>,
139    pub national_identification_number: Option<String>,
140    pub issuing_authority: Option<String>,
141    pub issue_date: Option<String>,
142    pub expiration_date: Option<String>,
143}
144
145#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
146#[serde(rename_all = "camelCase")]
147pub(crate) struct DriversLicenseDataV1 {
148    pub first_name: Option<String>,
149    pub middle_name: Option<String>,
150    pub last_name: Option<String>,
151    pub date_of_birth: Option<String>,
152    pub license_number: Option<String>,
153    pub issuing_country: Option<String>,
154    pub issuing_state: Option<String>,
155    pub issue_date: Option<String>,
156    pub expiration_date: Option<String>,
157    pub issuing_authority: Option<String>,
158    pub license_class: Option<String>,
159}
160
161#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
162#[serde(rename_all = "camelCase")]
163pub(crate) struct BankAccountDataV1 {
164    pub bank_name: Option<String>,
165    pub name_on_account: Option<String>,
166    pub account_type: Option<String>,
167    pub account_number: Option<String>,
168    pub routing_number: Option<String>,
169    pub branch_number: Option<String>,
170    pub pin: Option<String>,
171    pub swift_code: Option<String>,
172    pub iban: Option<String>,
173    pub bank_contact_phone: Option<String>,
174}
175
176#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
177#[serde(rename_all = "camelCase")]
178pub(crate) struct FieldDataV1 {
179    pub name: Option<String>,
180    pub value: Option<String>,
181    pub r#type: FieldType,
182    pub linked_id: Option<LinkedIdType>,
183}
184
185#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
186#[serde(rename_all = "camelCase")]
187pub(crate) struct PasswordHistoryDataV1 {
188    pub password: String,
189    pub last_used_date: DateTime<Utc>,
190}
191
192#[cfg(test)]
193mod tests {
194    use bitwarden_core::key_management::KeySlotIds;
195    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey, safe::DataEnvelope};
196    use bitwarden_encoding::B64;
197    use chrono::TimeZone;
198
199    use super::*;
200    use crate::cipher::{
201        blob::CipherBlob, linked_id::LoginLinkedIdType, secure_note::SecureNoteType,
202    };
203
204    const TEST_VECTOR_SECURE_NOTE_CEK: &str =
205        "pQEEAlApYxU9rgfIc9v9sHuglhkKAzoAARFvBIEEIFggDKYmieFA7a0UoOMAt4BErOpk7VfwZI8Dk0Yc9FxFzWIB";
206    const TEST_VECTOR_SECURE_NOTE_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFApYxU9rgfIc9v9sHuglhkKOgABOIECOgABOIABoQVYGMihknYX7mrmC03/w0V7rTMij2+q237p21h2H2Sq0hnlzZ4ka8yMW1yFOOMSt5ZS3tNQxzT07qeeY10tAeczgizA2g5AvmIQJdXK+7KR5mP3zk5VaVfGjC9mZUUFXwIAXsxC16HUII7Z1Iwhpd+MrJDf+itZGFVd07ExaXkH5+MjfaXhJqXBSxAStCG5zLGsEg==";
207
208    const TEST_VECTOR_LOGIN_CEK: &str =
209        "pQEEAlClJ9tO9x8fN2JVe5N8uzAaAzoAARFvBIEEIFggZwJXkLK7A6Sy5Y9+dacJrzCg9bo4RMRxXaRGDWYfbTYB";
210    const TEST_VECTOR_LOGIN_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFClJ9tO9x8fN2JVe5N8uzAaOgABOIECOgABOIABoQVYGNYP1rgAT3D2T6q2lGTRjIPHR2IELUDWE1kC2erlZM4Dqyeew1VlDkdXnZIE+t2g4SJh8IFSHo9WuzmyY+qC+V9cuGW3QHt+sg7pfZ5kQBh40U9uxUcxOHdVF3jQhcAmgB9abShR68u41NMDwwLSafG8PLzsUfhhxpCG0+ZuOda3tFVM1y5TyiDPJBBoJECuYK/K/1RLNAWAy5AU98yI0RgdK0MHxpoOqdSC8dXXu6fGgON7XLQWkkceFWILp0o51/c7OvdJ3B1nCiCIZjwAyS96+oWOzLrsPaGkBjqedBCi5iwelzLOttXk6nrzE/FfC0PkeeSsPSr9UdXBbeuUSK7wKtir9Lx/gtJ4sMPFidtTNdXCcDT9RA7y21h+3+wHQgGSlOGRigXFgXWi5ajSPCs9zLn7ERoG4BZ4IHa6EMSJbGH1pANW+Ibg4aadBF7LzOi/BZx2oZ/6z54XfAfqb44FR3/XXdNFHosPH9IH7CkKbNvLTuGrOTk9S998qFDNkWeGthrDYclaYSFktSHULvmHhSPadRL4uM6254HTmSjTAG6FDhhqdJU3SvoifrvBeHqjEP7F5R+zjVQol+JUcs/ExwmlrxXETOTzGyC/++FflZyPIUnH9U7ZqXhiYGd0ZvcyrnrnieehYToWFdwFR9ho+6h9hB/SCjhGudP5CRPZDL9GkNAUS5+pAd4ZC19fDjWpwMnEbgPTuthXKl6YRHxxCV+xpc9jncVQt9zF31e0bSIP7kVcdmlgjXaV6Nmd3aZ+PqeJnj27gCxG2tZkMimdJmEgxkL/cfvNHENg43+rpnV2mCTrCAO/X+RDGKdi7SIzsLesPVXVHEZCM4UBv2v+S/vpDFC3ie09cz525PCgt/7Je68k8S5WsTwKVeL0+T/ysqDo7wJvFSToY2G/LOGnBYMUYcCLfJGQbD8g2xI3oC+go8kcyVDJv+936/MkAYSxnvZDrMr30zEZaNm5YBh8cRqKr4MxOjJBk1XWjwDNsSQrDw==";
211
212    const TEST_VECTOR_CARD_CEK: &str =
213        "pQEEAlBqeNyImi1f1pMtJVlvDuV1AzoAARFvBIEEIFggyyOmMlDBj/oRic4qPjXnnKXGf4QGYMq7KvztZO3it+cB";
214    const TEST_VECTOR_CARD_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFBqeNyImi1f1pMtJVlvDuV1OgABOIECOgABOIABoQVYGHZO47yxMBN7r8jgWNJ8c+1bfUld0uzdNli2xQgFm6X0chG+qNdkiROVAUxi75+cN4jiTOzt41pG7bXyo7U4D8R38zR+l7jePi39w3YV4tnmxIenwBPK/0qdO8pLKUMwi8PMIBqJ2UxanLqRhP6Ru2i43rpVZMgAmasGgzGG8hhJttii1CidG8ntNP/zRvRl38F/7bphlN1a08/FeycdIAfQUf7tgzoaj1JegSwEs1M7/+ONHlPtlkmovN/zJTP0ZHL7U7NAt/JBIWbeScbGP6E=";
215
216    const TEST_VECTOR_IDENTITY_CEK: &str =
217        "pQEEAlBKyRoZr3LVRJsXJQ1msUhQAzoAARFvBIEEIFggVf/R4MMZFsa5DDtjjTG/1GozhF9jNQACFt9KpMpA9D8B";
218    const TEST_VECTOR_IDENTITY_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFBKyRoZr3LVRJsXJQ1msUhQOgABOIECOgABOIABoQVYGNZ9Ckqt+ftCL1eTn3LHTP/bLQVSiT2nFFkBl2ON9MRyKHrBCGlRlKxcGgMhFQhf3LY7kiDTjvvUhbZr3/rbc/fA8+7HS2UYu/SMOnxF4fg5AlBDc2kwE5iPqwJAJU4fnMqlQm+0jBAE2MS7oppPwHYh8cxDE9pqEa7Ehz1XygUgmUWtEpgGo2Nia+mdCnltws2X+uLCeAf6x5Ioc+HvFUIzFiYEN4WQ+NLmaCNrES9Zw9TXQTSh0drdPqaW7SSMjpBLk1TRtKX4hnSqE+tlRcfG24hPf214jG8On9tw1cdMQbF9GeC2FidfX+snjrU5psmje2bCcExfnvL7pgPeTV/R6+Fct0Jx5pKFXdTQM3SM1Ms8I+sq22sSc9Bu++V9nXFlLIyvWF9H/9FMYXrUR6HfBzSMJk7BSSin4wk/BKTEE59uaW+MtT/sDsW76aRo29VUqymbd5aezHCNxM8CFYaRGEXqWmakwXOPkqZh6CRhT3IZ6MjMQw2GbmDG+qv/KcbJatlKT1ZE6LUos/zpErOf0AT16D1WkS+9QwIeTP5QLv6F291nlBR2xDPg9v/cuauw";
219
220    const TEST_VECTOR_SSH_KEY_CEK: &str =
221        "pQEEAlApE2RsnNwb3+3FyIr/kcfWAzoAARFvBIEEIFggDk3igU6wYnicl6jRSYILSaPlDWYCjnRUqMLdqfPkVKAB";
222    const TEST_VECTOR_SSH_KEY_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFApE2RsnNwb3+3FyIr/kcfWOgABOIECOgABOIABoQVYGHPwqnuSuDHdwTg3twT5B0b3AXKVK+cySVkBSzorjdnfAdt1aNM32x3BPUg4QMkR99SQum3yc4eIT5eqi2FZjHyvEVPMwxfcWqg26g8UTc3dsRW57RYRF4ajx4+MGcJj+wWTrI8jPmthhLAnEHT11eC2YjYIW1INWKGFJTKnTjwHw1LTVJvEzA9MAZRk2y2NC+qkkdDM3wKmhl4PqoEPmt/x6qBjlR5+rlA4rUqkm9ja+NqqEbz8McGXBw8QWOh99/xE1PorFk7S+o9LW1Kcv1/GL+1wv6X7tTo1dYVYa2uCo9Hp9C8D5zXz/iVLm9w98NQFZQlteO8yibEOp+F/VNpgpsmZjOQzJ6wf0hKabFF2eXIUJ2RT1vJT+zUdcfc+TMkypaBbJEagmAiEBnZFcxVEhQ3tn1ZyJFRUcMzm91azIHQMmQ9cS6h/SqTGFF3z+q0H4+8w2S+yl+D5/OVWQHKcSOFvsPA=";
223
224    const TEST_VECTOR_BANK_ACCOUNT_CEK: &str =
225        "pQEEAlCz1mvOGP9yRKdx0pA5WbP7AzoAARFvBIEEIFggF30KGp58Duu4VcVvoFJ+Lhw1yEpfQvTUW2dvOP+WMd0B";
226    const TEST_VECTOR_BANK_ACCOUNT_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFCz1mvOGP9yRKdx0pA5WbP7OgABOIECOgABOIABoQVYGDbBFtW702QwCdi03+f9Uahq4Xf0bJ8i7VkBQxZB8XgvwLS13sHp8iz3VmTVcWJCyoxp6ycEUNSllpzURnZtfTsm9hkHCM0iFvMAXgDHBamHpI+8cX4sZ1qyjrGx4JDkGL1wDPUKMY7pLIN6alssjgYNl/6ijicWk2uNDneAGVgJdAHmxVKYPKbwYp0e8bLeAjgj6FOSFHaXv1a6TdF82iRCF/r5Uh/Ohx1FEbtRnaCSMJ4tLsf8YC9oq3duarJzSB2aINL9EnGAqqUlJ8cy8lyfkopUxV0OMnRWiHpja4CrEphhNeKKPoFRezsVoDYQ3f7kjryVAQ661gVxsEG3FB03+CcvVsT849QfrDcERxsQoKwy1E9yHaoE2kgWiYTHS+6gCH/gikDw1t4GBBUdjeJhP3bqQJbmM4cgRxWMgyswfFAfZok25kcA15EpHabkczydiPtnG2UW9qfu+bfw";
227
228    const TEST_VECTOR_PASSPORT_CEK: &str =
229        "pQEEAlBt4j9Ll1AOH1qIRqCdCyTpAzoAARFvBIEEIFggQRD5JRuLW/LBh6tOH9pwzI79EH/uwnkc1g7MmHnQxjsB";
230    const TEST_VECTOR_PASSPORT_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFBt4j9Ll1AOH1qIRqCdCyTpOgABOIECOgABOIABoQVYGMtbFi7dX35y8b7HfGSBUkExZu9xbwLdTVkBfk+53EwDwOJqa1BTtHaKepq8gpn3PD6tX7WmcgM/fMM9g1bqVlKEa9zAuAsjXXqc1jsTmy9J8+y6b9ExcP7JCCTZTNrNDpyghuQc+Xm4URgJMrXryg4aEi5rVD4bI6iI20IsM70ZA0jpvz0rNz6IcyL9zwL6f6aZRtIKAHxQ4AzmBnNItuZShM4UEDDgxGVpkOqTBhfh2DBogQMOAE8DNNbI91gMrj4SVf8zJXIIQhjHCeDJYWp4assoI+3kMyLuoPXSPCLqmvWMtQnQMU3BdONVl/WDtw2GUMp0PpuFVToc6IfieSRVJaFWxqRZpK4GNLECkR6YkZjz9kHg2tXFSi+77RWUg6ZkJ7zYX+vQ1GnnLQ0cWJtt2obwyHVpg8ejz83gAZHFaKN+LLdGW/wt/G188au4zpXjJTxxPSoy1/rd55mGoiwV/cc049L44cbmhjUkdaaiPv2Gtx1MhuM2bJaC2fdUqe4luzTwhzSCcPphwGkFcNM/mANHYwvdXiQ=";
231
232    const TEST_VECTOR_DRIVERS_LICENSE_CEK: &str =
233        "pQEEAlAAl+WJTHKGitbdU9BlEgYOAzoAARFvBIEEIFggvuVlFL4NaxQ6mc2tROVjCg20rDmr8CFy3+dmADa+1JsB";
234    const TEST_VECTOR_DRIVERS_LICENSE_ENVELOPE: &str = "g1hLpQE6AAERbwN4I2FwcGxpY2F0aW9uL3guYml0d2FyZGVuLmNib3ItcGFkZGVkBFAAl+WJTHKGitbdU9BlEgYOOgABOIECOgABOIABoQVYGJYtxnw1bOqtBrpNsa5PLUa6b21jEHl7zFkBUcdN1jS6PfqOuYVrIfqU73+jrZlq7jxK2Hcp8iGsnInC+wyXpWJety8XfWs2X3CfFSVmnO5nc58LWagfY6AAr66GWuE+HCJJ30QinqQ7NbjInJpOvGysXGnu1f5Mjk9Ow4cUobh4Nxp0rsrznwaUTskR0RJVdzoelutqBWLSvkrM1z81syz9QLdhbBRmv4A18/Bc1U85RWYUYhvxCCxsydp1HFmBNsNk3tS3wd/4svgNjpjnrvDLNxuN2o4cyQDdkTLPvQa98Ld0RCzh1D62zFxLlJ634NV6ROEstYN8CHaat3LIyaCzXjqMiEhP0v3JRWTsrJm6FwjZF8TULif4jrJcMdSdxXn7l6GGHwLBMvXRdYdfPqpgIc2CjPJUj62fa9Wx5aBVcDJRriznuaAWmeapcaxOeSOyG1QMpmYMRXJK8pqnkMH5mT4OGJTVeuLkrXM=";
235
236    fn test_blob_secure_note() -> CipherBlobV1 {
237        CipherBlobV1 {
238            name: "Test Secure Note".to_string(),
239            notes: Some("Some notes".to_string()),
240            type_data: CipherTypeDataV1::SecureNote(SecureNoteDataV1 {
241                r#type: SecureNoteType::Generic,
242            }),
243            fields: Vec::new(),
244            password_history: Vec::new(),
245        }
246    }
247
248    fn test_blob_login() -> CipherBlobV1 {
249        CipherBlobV1 {
250            name: "Test Login".to_string(),
251            notes: Some("Login notes".to_string()),
252            type_data: CipherTypeDataV1::Login(LoginDataV1 {
253                username: Some("[email protected]".to_string()),
254                password: Some("p@ssw0rd123".to_string()),
255                password_revision_date: Some(Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap()),
256                uris: vec![LoginUriDataV1 {
257                    uri: Some("https://example.com/login".to_string()),
258                    r#match: Some(UriMatchType::Domain),
259                }],
260                totp: Some("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP".to_string()),
261                autofill_on_page_load: Some(true),
262                fido2_credentials: vec![Fido2CredentialDataV1 {
263                    credential_id: "credential-id-123".to_string(),
264                    key_type: "public-key".to_string(),
265                    key_algorithm: "ECDSA".to_string(),
266                    key_curve: "P-256".to_string(),
267                    key_value: "key-value-base64".to_string(),
268                    rp_id: "example.com".to_string(),
269                    user_handle: Some("user-handle-456".to_string()),
270                    user_name: Some("testuser".to_string()),
271                    counter: 42,
272                    rp_name: Some("Example".to_string()),
273                    user_display_name: Some("Test User".to_string()),
274                    discoverable: true,
275                    creation_date: Utc.with_ymd_and_hms(2024, 6, 1, 10, 30, 0).unwrap(),
276                }],
277            }),
278            fields: vec![FieldDataV1 {
279                name: Some("Custom Field".to_string()),
280                value: Some("custom-value".to_string()),
281                r#type: FieldType::Linked,
282                linked_id: Some(LinkedIdType::Login(LoginLinkedIdType::Username)),
283            }],
284            password_history: vec![PasswordHistoryDataV1 {
285                password: "old-password-1".to_string(),
286                last_used_date: Utc.with_ymd_and_hms(2023, 12, 1, 8, 0, 0).unwrap(),
287            }],
288        }
289    }
290
291    fn test_blob_card() -> CipherBlobV1 {
292        CipherBlobV1 {
293            name: "Test Card".to_string(),
294            notes: Some("Card notes".to_string()),
295            type_data: CipherTypeDataV1::Card(CardDataV1 {
296                cardholder_name: Some("John Doe".to_string()),
297                exp_month: Some("12".to_string()),
298                exp_year: Some("2028".to_string()),
299                code: Some("123".to_string()),
300                brand: Some("Visa".to_string()),
301                number: Some("4111111111111111".to_string()),
302            }),
303            fields: Vec::new(),
304            password_history: Vec::new(),
305        }
306    }
307
308    fn test_blob_identity() -> CipherBlobV1 {
309        CipherBlobV1 {
310            name: "Test Identity".to_string(),
311            notes: Some("Identity notes".to_string()),
312            type_data: CipherTypeDataV1::Identity(IdentityDataV1 {
313                title: Some("Mr".to_string()),
314                first_name: Some("John".to_string()),
315                middle_name: Some("Michael".to_string()),
316                last_name: Some("Doe".to_string()),
317                address1: Some("123 Main St".to_string()),
318                address2: Some("Apt 4B".to_string()),
319                address3: Some("Building C".to_string()),
320                city: Some("New York".to_string()),
321                state: Some("NY".to_string()),
322                postal_code: Some("10001".to_string()),
323                country: Some("US".to_string()),
324                company: Some("Acme Corp".to_string()),
325                email: Some("[email protected]".to_string()),
326                phone: Some("555-0123".to_string()),
327                ssn: Some("123-45-6789".to_string()),
328                username: Some("johndoe".to_string()),
329                passport_number: Some("P12345678".to_string()),
330                license_number: Some("DL-987654".to_string()),
331            }),
332            fields: Vec::new(),
333            password_history: Vec::new(),
334        }
335    }
336
337    fn test_blob_ssh_key() -> CipherBlobV1 {
338        CipherBlobV1 {
339            name: "Test SSH Key".to_string(),
340            notes: Some("SSH key notes".to_string()),
341            type_data: CipherTypeDataV1::SshKey(SshKeyDataV1 {
342                private_key: "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEA\n-----END OPENSSH PRIVATE KEY-----".to_string(),
343                public_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI [email protected]".to_string(),
344                fingerprint: "SHA256:abcdefghijklmnopqrstuvwxyz012345678901234567".to_string(),
345            }),
346            fields: Vec::new(),
347            password_history: Vec::new(),
348        }
349    }
350
351    fn test_blob_bank_account() -> CipherBlobV1 {
352        CipherBlobV1 {
353            name: "Test Bank Account".to_string(),
354            notes: Some("Bank account notes".to_string()),
355            type_data: CipherTypeDataV1::BankAccount(BankAccountDataV1 {
356                bank_name: Some("Test Bank".to_string()),
357                name_on_account: Some("John Doe".to_string()),
358                account_type: Some("Checking".to_string()),
359                account_number: Some("1234567890".to_string()),
360                routing_number: Some("021000021".to_string()),
361                branch_number: Some("001".to_string()),
362                pin: Some("1234".to_string()),
363                swift_code: Some("TESTUS33".to_string()),
364                iban: Some("US12345678901234567890".to_string()),
365                bank_contact_phone: Some("555-0123".to_string()),
366            }),
367            fields: Vec::new(),
368            password_history: Vec::new(),
369        }
370    }
371
372    fn test_blob_passport() -> CipherBlobV1 {
373        CipherBlobV1 {
374            name: "Test Passport".to_string(),
375            notes: Some("Passport notes".to_string()),
376            type_data: CipherTypeDataV1::Passport(PassportDataV1 {
377                surname: Some("Doe".to_string()),
378                given_name: Some("Jane".to_string()),
379                date_of_birth: Some("1990-01-01".to_string()),
380                sex: Some("F".to_string()),
381                birth_place: Some("New York".to_string()),
382                nationality: Some("American".to_string()),
383                issuing_country: Some("US".to_string()),
384                passport_number: Some("P12345678".to_string()),
385                passport_type: Some("P".to_string()),
386                national_identification_number: Some("123-45-6789".to_string()),
387                issuing_authority: Some("US State Department".to_string()),
388                issue_date: Some("2020-01-01".to_string()),
389                expiration_date: Some("2030-01-01".to_string()),
390            }),
391            fields: Vec::new(),
392            password_history: Vec::new(),
393        }
394    }
395
396    fn test_blob_drivers_license() -> CipherBlobV1 {
397        CipherBlobV1 {
398            name: "Test Driver's License".to_string(),
399            notes: Some("Driver's license notes".to_string()),
400            type_data: CipherTypeDataV1::DriversLicense(DriversLicenseDataV1 {
401                first_name: Some("John".to_string()),
402                middle_name: Some("Michael".to_string()),
403                last_name: Some("Doe".to_string()),
404                date_of_birth: Some("1985-06-15".to_string()),
405                license_number: Some("DL-987654".to_string()),
406                issuing_country: Some("US".to_string()),
407                issuing_state: Some("NY".to_string()),
408                issue_date: Some("2020-01-01".to_string()),
409                expiration_date: Some("2028-01-01".to_string()),
410                issuing_authority: Some("NY DMV".to_string()),
411                license_class: Some("D".to_string()),
412            }),
413            fields: Vec::new(),
414            password_history: Vec::new(),
415        }
416    }
417
418    #[test]
419    #[ignore]
420    fn generate_test_vectors() {
421        let blobs: Vec<(&str, CipherBlobV1)> = vec![
422            ("LOGIN", test_blob_login()),
423            ("CARD", test_blob_card()),
424            ("IDENTITY", test_blob_identity()),
425            ("SECURE_NOTE", test_blob_secure_note()),
426            ("SSH_KEY", test_blob_ssh_key()),
427            ("BANK_ACCOUNT", test_blob_bank_account()),
428            ("DRIVERS_LICENSE", test_blob_drivers_license()),
429            ("PASSPORT", test_blob_passport()),
430        ];
431
432        for (name, blob) in blobs {
433            let data: CipherBlob = blob.into();
434            let store: KeyStore<KeySlotIds> = KeyStore::default();
435            let mut ctx = store.context_mut();
436            let (envelope, cek_id) = DataEnvelope::seal(data, &mut ctx).unwrap();
437
438            #[allow(deprecated)]
439            let cek = ctx.dangerous_get_symmetric_key(cek_id).unwrap();
440            println!(
441                "const TEST_VECTOR_{}_CEK: &str = \"{}\";",
442                name,
443                cek.to_base64()
444            );
445            println!(
446                "const TEST_VECTOR_{}_ENVELOPE: &str = \"{}\";",
447                name,
448                String::from(envelope)
449            );
450            println!();
451        }
452    }
453
454    fn verify_test_vector(cek_str: &str, envelope_str: &str, expected: CipherBlobV1) {
455        let cek = SymmetricCryptoKey::try_from(B64::try_from(cek_str).unwrap()).unwrap();
456
457        let store: KeyStore<KeySlotIds> = KeyStore::default();
458        let mut ctx = store.context_mut();
459        let cek_id = ctx.add_local_symmetric_key(cek);
460
461        let envelope: DataEnvelope = envelope_str.parse().unwrap();
462        let unsealed: CipherBlob = envelope
463            .unseal(cek_id, &mut ctx)
464            .expect("CipherBlobV1 has changed in a backwards-incompatible way. Existing encrypted data must remain deserializable. If a new format is needed, create a new version instead of modifying V1.");
465        assert_eq!(unsealed, expected.into());
466    }
467
468    #[test]
469    fn test_recorded_secure_note_test_vector() {
470        verify_test_vector(
471            TEST_VECTOR_SECURE_NOTE_CEK,
472            TEST_VECTOR_SECURE_NOTE_ENVELOPE,
473            test_blob_secure_note(),
474        );
475    }
476
477    #[test]
478    fn test_recorded_login_test_vector() {
479        verify_test_vector(
480            TEST_VECTOR_LOGIN_CEK,
481            TEST_VECTOR_LOGIN_ENVELOPE,
482            test_blob_login(),
483        );
484    }
485
486    #[test]
487    fn test_recorded_card_test_vector() {
488        verify_test_vector(
489            TEST_VECTOR_CARD_CEK,
490            TEST_VECTOR_CARD_ENVELOPE,
491            test_blob_card(),
492        );
493    }
494
495    #[test]
496    fn test_recorded_identity_test_vector() {
497        verify_test_vector(
498            TEST_VECTOR_IDENTITY_CEK,
499            TEST_VECTOR_IDENTITY_ENVELOPE,
500            test_blob_identity(),
501        );
502    }
503
504    #[test]
505    fn test_recorded_ssh_key_test_vector() {
506        verify_test_vector(
507            TEST_VECTOR_SSH_KEY_CEK,
508            TEST_VECTOR_SSH_KEY_ENVELOPE,
509            test_blob_ssh_key(),
510        );
511    }
512
513    #[test]
514    fn test_recorded_bank_account_test_vector() {
515        verify_test_vector(
516            TEST_VECTOR_BANK_ACCOUNT_CEK,
517            TEST_VECTOR_BANK_ACCOUNT_ENVELOPE,
518            test_blob_bank_account(),
519        );
520    }
521
522    #[test]
523    fn test_recorded_passport_test_vector() {
524        verify_test_vector(
525            TEST_VECTOR_PASSPORT_CEK,
526            TEST_VECTOR_PASSPORT_ENVELOPE,
527            test_blob_passport(),
528        );
529    }
530
531    #[test]
532    fn test_recorded_drivers_license_test_vector() {
533        verify_test_vector(
534            TEST_VECTOR_DRIVERS_LICENSE_CEK,
535            TEST_VECTOR_DRIVERS_LICENSE_ENVELOPE,
536            test_blob_drivers_license(),
537        );
538    }
539}