ate-auth 1.9.0

Represents a standardized data model and API for authenticating an ATE chain-of-trust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
#![allow(unused_imports)]
use error_chain::bail;
use once_cell::sync::Lazy;
use qrcode::render::unicode;
use qrcode::QrCode;
use regex::Regex;
use std::io::stdout;
use std::io::Write;
use std::ops::Deref;
use std::sync::Arc;
use tracing::{debug, error, info, instrument, span, trace, warn, Level};
use url::Url;

use ate::error::LoadError;
use ate::prelude::*;
use ate::utils::chain_key_4hex;

use crate::error::*;
use crate::helper::*;
use crate::model::*;
use crate::prelude::*;
use crate::request::*;
use crate::service::AuthService;

static BANNED_USERNAMES: Lazy<Vec<&'static str>> =
    Lazy::new(|| vec!["nobody", "admin", "support", "help", "root"]);

impl AuthService {
    pub async fn process_create_user(
        self: Arc<Self>,
        request: CreateUserRequest,
    ) -> Result<CreateUserResponse, CreateUserFailed> {
        Ok(self
            .process_create_user_internal(request, UserStatus::Nominal)
            .await?
            .0)
    }

    pub async fn process_create_user_internal(
        self: Arc<Self>,
        request: CreateUserRequest,
        initial_status: UserStatus,
    ) -> Result<(CreateUserResponse, DaoMut<User>), CreateUserFailed> {
        info!("create user: {}", request.email);

        // Check the username matches the regex
        let regex = Regex::new("^([a-z0-9\\.!#$%&'*+/=?^_`{|}~-]{1,})@([a-z0-9\\.!#$%&'*+/=?^_`{|}~-]{1,}).([a-z0-9\\.!#$%&'*+/=?^_`{|}~-]{1,})$").unwrap();
        if regex.is_match(request.email.as_str()) == false {
            warn!("invalid email address - {}", request.email);
            return Err(CreateUserFailed::InvalidEmail);
        }

        // Get the master write key
        let master_write_key = match self.master_session.user.write_keys().next() {
            Some(a) => a.clone(),
            None => {
                warn!("no master write key");
                return Err(CreateUserFailed::NoMasterKey);
            }
        };

        // If the username is on the banned list then dont allow it
        if BANNED_USERNAMES.contains(&request.email.as_str()) {
            warn!("banned username - {}", request.email);
            return Err(CreateUserFailed::InvalidEmail);
        }

        // Compute the super_key, super_super_key (elevated rights) and the super_session
        let key_size = request.secret.size();
        let (super_key, token) = match self.compute_master_key(&request.secret) {
            Some(a) => a,
            None => {
                warn!("failed to generate super key");
                return Err(CreateUserFailed::NoMasterKey);
            }
        };
        let (super_super_key, super_token) = match self.compute_master_key(&super_key) {
            Some(a) => a,
            None => {
                warn!("failed to generate super super key");
                return Err(CreateUserFailed::NoMasterKey);
            }
        };
        let mut super_session = self.master_session.clone();
        super_session.user.add_read_key(&super_key);
        super_session.user.add_read_key(&super_super_key);
        super_session.token = Some(super_token);

        // Generate the recovery code
        let recovery_code = AteHash::generate().to_hex_string().to_uppercase();
        let recovery_code = format!(
            "{}-{}-{}-{}-{}",
            &recovery_code[0..4],
            &recovery_code[4..8],
            &recovery_code[8..12],
            &recovery_code[12..16],
            &recovery_code[16..20]
        );
        let recovery_prefix = format!("recover-login:{}:", request.email);
        let recovery_key =
            password_to_read_key(&recovery_prefix, &recovery_code, 15, KeySize::Bit192);
        let (super_recovery_key, _) = match self.compute_master_key(&recovery_key) {
            Some(a) => a,
            None => {
                warn!("failed to generate recovery key");
                return Err(CreateUserFailed::NoMasterKey);
            }
        };
        super_session.user.add_read_key(&super_recovery_key);

        // Create the access object
        let read_key = EncryptKey::generate(key_size);
        let private_read_key = PrivateEncryptKey::generate(key_size);
        let write_key = PrivateSignKey::generate(key_size);
        let mut access = Vec::new();
        access.push(Authorization {
            read: read_key.clone(),
            private_read: private_read_key.clone(),
            write: write_key.clone(),
        });

        // Create an aggregation session
        let mut session = self.master_session.clone();
        session.user.add_read_key(&read_key);
        session.user.add_private_read_key(&private_read_key);
        session.user.add_write_key(&write_key);
        session.token = Some(token.clone());

        // Compute which chain the user should exist within
        let user_chain_key = chain_key_4hex(&request.email, Some("redo"));
        let chain = self.registry.open(&self.auth_url, &user_chain_key).await?;
        let dio = chain.dio_full(&super_session).await;

        // Try and find a free UID
        let mut uid = None;
        for n in 0u32..50u32 {
            let mut uid_test = estimate_user_name_as_uid(request.email.clone());
            if uid_test < 1000 {
                uid_test = uid_test + 1000;
            }
            uid_test = uid_test + n;
            if dio.exists(&PrimaryKey::from(uid_test as u64)).await {
                continue;
            }
            uid = Some(uid_test);
            break;
        }
        let uid = match uid {
            Some(a) => a,
            None => {
                return Err(CreateUserFailed::NoMoreRoom);
            }
        };

        // If the terms and conditions don't match then reject it
        if request.accepted_terms != self.terms_and_conditions {
            if let Some(terms) = &self.terms_and_conditions {
                warn!("did not accept terms and conditions");
                return Err(CreateUserFailed::TermsAndConditions(terms.clone()));
            }
        }

        // Generate a QR code
        let google_auth = google_authenticator::GoogleAuthenticator::new();
        let secret = google_auth.create_secret(32);
        let google_auth_secret = format!(
            "otpauth://totp/{}:{}?secret={}",
            request.auth.to_string(),
            request.email,
            secret.clone()
        );

        // Build the QR image
        let qr_code = QrCode::new(google_auth_secret.as_bytes())
            .unwrap()
            .render::<unicode::Dense1x2>()
            .dark_color(unicode::Dense1x2::Light)
            .light_color(unicode::Dense1x2::Dark)
            .build();

        // Create all the sudo keys
        let sudo_read_key = EncryptKey::generate(key_size);
        let sudo_private_read_key = PrivateEncryptKey::generate(key_size);
        let sudo_write_key = PrivateSignKey::generate(key_size);
        let mut sudo_access = Vec::new();
        sudo_access.push(Authorization {
            read: sudo_read_key.clone(),
            private_read: sudo_private_read_key.clone(),
            write: sudo_write_key.clone(),
        });

        // We generate a derived contract encryption key which we will give back to the caller
        let contract_read_key = {
            let contract_read_key_entropy = format!("contract-read:{}", request.email);
            let contract_read_key_entropy =
                AteHash::from_bytes(contract_read_key_entropy.as_bytes());
            self.compute_contract_key_from_hash(&contract_read_key_entropy)
        };

        // Attempt to load it as maybe it is still being verified
        // Otherwise create the record
        let user_key = PrimaryKey::from(request.email.clone());
        let mut user = match dio.load::<User>(&user_key).await.ok() {
            Some(user) => {
                if user.last_login.is_none() {
                    user
                } else {
                    // Fail
                    warn!("username already active: {}", request.email);
                    return Err(CreateUserFailed::AlreadyExists(
                        "your account already exists and is in use".to_string(),
                    ));
                }
            }
            None => {
                // If it already exists then fail
                if dio.exists(&user_key).await {
                    // Fail
                    warn!("username already exists: {}", request.email);
                    return Err(CreateUserFailed::AlreadyExists(
                        "an account already exists for this username".to_string(),
                    ));
                }

                // Generate the broker encryption keys used to extend trust without composing
                // the confidentiality of the chain through wide blast radius
                let broker_read = PrivateEncryptKey::generate(key_size);
                let broker_write = PrivateSignKey::generate(key_size);

                // Generate a verification code (if the inital state is not nominal)
                let verification_code = if initial_status == UserStatus::Unverified {
                    let v = AteHash::generate().to_hex_string().to_uppercase();
                    let v = format!("{}-{}-{}", &v[0..4], &v[4..8], &v[8..12]);
                    Some(v)
                } else {
                    None
                };

                // Create the user and save it
                let user = User {
                    person: DaoChild::new(),
                    email: request.email.clone(),
                    uid,
                    role: UserRole::Human,
                    status: initial_status,
                    verification_code,
                    last_login: None,
                    access: access,
                    foreign: DaoForeign::default(),
                    sudo: DaoChild::new(),
                    advert: DaoChild::new(),
                    recovery: DaoChild::new(),
                    accepted_terms: DaoChild::new(),
                    nominal_read: read_key.hash(),
                    nominal_public_read: private_read_key.as_public_key().clone(),
                    nominal_write: write_key.as_public_key().clone(),
                    sudo_read: sudo_read_key.hash(),
                    sudo_public_read: sudo_private_read_key.as_public_key().clone(),
                    sudo_write: sudo_write_key.as_public_key().clone(),
                    broker_read: broker_read.clone(),
                    broker_write: broker_write.clone(),
                };
                dio.store_with_key(user, user_key.clone())?
            }
        };
        user.auth_mut().read = ReadOption::from_key(&super_key);
        user.auth_mut().write =
            WriteOption::Any(vec![master_write_key.hash(), sudo_write_key.hash()]);

        // Generate the account recovery object
        let recovery_key_entropy = format!("recovery:{}", request.email.clone()).to_string();
        let recovery_key = PrimaryKey::from(recovery_key_entropy);
        let mut recovery = {
            let mut user = user.as_mut();
            if let Some(mut recovery) = user.recovery.load_mut().await? {
                let mut recovery_mut = recovery.as_mut();
                recovery_mut.email = request.email.clone();
                recovery_mut.login_secret = request.secret.clone();
                recovery_mut.sudo_secret = secret.clone();
                recovery_mut.google_auth = google_auth_secret.clone();
                recovery_mut.qr_code = qr_code.clone();
                drop(recovery_mut);
                recovery
            } else {
                let recovery = UserRecovery {
                    email: request.email.clone(),
                    login_secret: request.secret.clone(),
                    sudo_secret: secret.clone(),
                    google_auth: google_auth_secret.clone(),
                    qr_code: qr_code.clone(),
                };
                user.recovery.store_with_key(recovery, recovery_key).await?
            }
        };
        recovery.auth_mut().read = ReadOption::from_key(&super_recovery_key);
        recovery.auth_mut().write = WriteOption::Specific(master_write_key.hash());

        // Get or create the sudo object and save it using another elevation of the key
        let mut sudo = {
            let mut user = user.as_mut();
            if let Some(mut sudo) = user.sudo.load_mut().await? {
                let mut sudo_mut = sudo.as_mut();
                sudo_mut.email = request.email.clone();
                sudo_mut.uid = uid;
                sudo_mut.google_auth = google_auth_secret;
                sudo_mut.secret = secret.clone();
                sudo_mut.groups = Vec::new();
                sudo_mut.access = sudo_access;
                sudo_mut.contract_read_key = contract_read_key;
                sudo_mut.qr_code = qr_code.clone();
                sudo_mut.failed_attempts = 0u32;
                drop(sudo_mut);
                sudo
            } else {
                let sudo = Sudo {
                    email: request.email.clone(),
                    uid,
                    google_auth: google_auth_secret,
                    secret: secret.clone(),
                    groups: Vec::new(),
                    access: sudo_access,
                    contract_read_key,
                    qr_code: qr_code.clone(),
                    failed_attempts: 0u32,
                };
                user.sudo.store(sudo).await?
            }
        };
        sudo.auth_mut().read = ReadOption::from_key(&super_super_key);
        sudo.auth_mut().write = WriteOption::Inherit;

        // Add the accepted terms and conditions to the datachain rrecord
        if let Some(accepted_terms) = request.accepted_terms.as_ref() {
            let mut user = user.as_mut();
            if let Some(mut terms) = user.accepted_terms.load_mut().await? {
                terms.as_mut().terms_and_conditions = accepted_terms.clone();
            } else {
                let mut terms = user
                    .accepted_terms
                    .store(AcceptedTerms {
                        terms_and_conditions: accepted_terms.clone(),
                    })
                    .await?;
                terms.auth_mut().read = ReadOption::Everyone(None);
                terms.auth_mut().write = WriteOption::Specific(master_write_key.hash());
            }
        }

        // Create the advert object and save it using public read
        let advert_key_entropy = format!("advert:{}", request.email.clone()).to_string();
        let advert_key = PrimaryKey::from(advert_key_entropy);
        let mut advert = match dio.load::<Advert>(&advert_key).await.ok() {
            Some(mut advert) => {
                let mut advert_mut = advert.as_mut();
                advert_mut.identity = request.email.clone();
                advert_mut.id = AdvertId::UID(uid);
                advert_mut.nominal_encrypt = private_read_key.as_public_key().clone();
                advert_mut.nominal_auth = write_key.as_public_key().clone();
                advert_mut.sudo_encrypt = sudo_private_read_key.as_public_key().clone();
                advert_mut.sudo_auth = sudo_write_key.as_public_key().clone();
                advert_mut.broker_encrypt = user.broker_read.as_public_key().clone();
                advert_mut.broker_auth = user.broker_write.as_public_key().clone();
                drop(advert_mut);
                advert
            }
            None => {
                let advert = Advert {
                    identity: request.email.clone(),
                    id: AdvertId::UID(uid),
                    nominal_encrypt: private_read_key.as_public_key().clone(),
                    nominal_auth: write_key.as_public_key().clone(),
                    sudo_encrypt: sudo_private_read_key.as_public_key().clone(),
                    sudo_auth: sudo_write_key.as_public_key().clone(),
                    broker_encrypt: user.broker_read.as_public_key().clone(),
                    broker_auth: user.broker_write.as_public_key().clone(),
                };
                user.as_mut()
                    .advert
                    .store_with_key(advert, advert_key.clone())
                    .await?
            }
        };
        advert.auth_mut().read = ReadOption::Everyone(None);
        advert.auth_mut().write = WriteOption::Inherit;

        // Save the data
        dio.commit().await?;

        // Create the authorizations and return them
        let mut session = compute_user_auth(user.deref());
        session.token = Some(token);

        // Return success to the caller
        Ok((
            CreateUserResponse {
                key: user.key().clone(),
                qr_code: qr_code,
                qr_secret: secret.clone(),
                recovery_code,
                authority: session,
                message_of_the_day: None,
            },
            user,
        ))
    }
}