#!/bin/bash

### Cmf Auth
### todo0 проанализировать алгорим коля1 сергей3
### предварительно рассказав про scram digest ocra totp chap
### авторские права принадлежат ОСВ необходимо до публикации подать на патент метода
### Проблемы и решения2
Проблема всех стандартных протоколов аутентификации scram digest ocra totp chap
Особенности:
    1. Протоколы рассчитывают, что транспортный уровень секурный ssl и тп.
    2. Протоколы рассчитывают, что пароль достаточно длинный и поэтому хеш сложно перебрать.
Проблемы:
    1. Все эти протоколы уязвимы к прослушиванию и офлайн перебору паролей.
       Берем любой пакет и окружение его,
       и методом перебора пароля находим неслучайные байты в рашифрованном пакете.
       Когда нашли ascii и тп, то значит пароль подобрали.
       Кража пароля гораздо опасней, чем кража токена, тк пароль может много куда еще подойти.
    2. Почти все они уязвимы к mitm и подстановке сервера.
    3. Админ может незаметно подслушивать и офлайн перебирать
Решение: протокол аутентификации на основе RSA + HELLO.
    ОСВ назвал его RSA-HELLO-CRAM

### RSA-HELLO-CRAM
Вводные:
    1. Нас прослушивают.
    2. Сервер можно подменить.
    3. Тербуется защита от mitm(CDN и admin).
    4. Защита от офлайн перебора пароля.
    4.1 Пароль можно перебрать даже с PBKDF2_100000 на многовидяшных компах.
    4.2 Нельзя шифровать хешем PBKDF2 нерандомные данные, тк это позволит перебирать пароль.
Примечание:
    Фундаментальная проблема аутентификации, что сам алгоритм аутентификации позволяет
    перебирать хеш офлайн. Но есть компромисс - вероятностная аутентификация.
    Идеального решения найти неудалось, но с учетом, что количество попыток ограничено,
    найдено достаточное решение.
    Решение использует предварительную взаимную аутентификацию с вероятностью 1/(255*255),
    что раскрывает хеш пароля в очень малой степени, недостаточной для перебора.
    При этом этой вероятности достаточно при ограниченном колве попыток,
    чтобы установить взаимное доверие.
Диалог:
C: generate одноразовый priv.key,pub.key
   login, pub.key, clientTime
   # clientTime используем для защиты от повторов и от взлома rsa
S: if abs(clientTime-serverTime) > 30 then FAIL
   SEND: saltMsg := enc_rsa(salt,pub.key)
   # отправка salt через pub защищает от прослушки полностью, тк перебор salt нереален
   # но это не защищает от mitm
C: salt := dec_rsa(saltMsg,priv.key)
   passHash := PBKDF2_SHA256_100000(pass,salt)
   SEND: clientHello := SHA256(pub.key+clientTime,passHash)[0:1]
   # таким образом мы задним числом подписали pub_key
   # можно использовать любой другой вариант подписи хоть xor хоть сумму хоть генератор
   # clientHello это частичка хеша 2 байта, позволяет с вероятностью 1/65536
   # предварительно аутентифицировать клиента.
   # Использование 2 байт не дает возможность подобрать пароль.
   # тк коллизий будет много, и все варианты перебрать нереально сервер нас забанит.
   # mitm не сможет подобрать свой hello к подставному pub.key тк не знает пароль.
   # mitm может только подставить случайное число, но сервер не даст много раз перебрать.
S: if ClientHello != SHA256(pub.key+clientTime,passHash)[0:1] then FAIL
   # теперь с вероятностью 1/65536 мы знаем что это наш клиент и можем ему
   # тоже дать 2 байта общего секрета.
   # здесь давать больше нет смысла, но вопрос открытый.
   # mitm не сможет сообщить клиенту 2 верных серверных байта тк не знает пароль
   serverHello := SHA256(pub.key+clientTime,passHash)[2:3]
   serverKey := rand(32)
   SEND: serverAuthMsg := enc_rsa(serverHello + serverKey,pub.key)
C: serverHello, serverKey := dec_rsa(serverAuthMsg,priv.key)
   if serverHello != SHA256(pub.key+clientTime,passHash)[2:3] then FAIL
   # теперь мы с большой вероятностью знаем, что это наш сервер
   # имеет ли смысл здесь передать полный дайджет имхо нет
   # тк клиент может угадать 1/65536 и узнает пароль перебором.
   SEND: clientAuthMsg := SHA256(serverKey,passHash)
S: SEND: enc_rsa(JWS,pub.key)
C: JWS := enc_rsa(JWS,priv.key)

## Проблемы и решения

### Проблема 1: пароль передается в ssl в открытом виде.

Недобросовестный админ сервера может включить логгирование POST-запросов, украсть пароль.
Этот пароль часто совпадает с паролем к банковским кабинетам и онлайн-банкам.

Решение: использовать digest-подобную схему авторизации и не передавать пароль в открытом виде.

### Проблема 2: хэш можно перебрать.

8-10 символов взламываются несколько секунд, 12 символов - год.
Для увеличения стоикости к перебору хэши подвергают итерациям в духе PBKDF2.
Например, джанго использует 20000 итериций sha256 по дефолту
Т.К. мы считаем хэш на клиенте (в браузере), то есть ограничения по скорости выполнения. 
В современных браузерах есть поддержка crypto АПИ (на си). В старых браузерах типо IE ее нет. 
В случае IE, нужно либо требовать 12-символьные пароли при 10_000 итерациях, либо при 8 при 100_000. 
Но 100_000 считаются на голом JS оч долго.

Решение: Используем 100_000 итераций, 8 символов. В Случае IE мы будем считать хэш не в браузере, а на бэкэнде.


Заметка: После проверки пароля будет формироваться JWT-токен вида:

```json
{
    "login": "qwe",
    "expires": "2020-09-09",
    "groups": "crm:manager,crm:admin,cloud-disk:guest"
}
```
А если точнее JWS
Этот JSON будет подписываться на стороне сервера авторизации закрытым ключом.
Этот же JSON будет проверяться на стороне сервера ресурсов (crm, cloud-disk, ...)
с помощью открытого ключа. Эти ключи не передаются в браузер на сторону клиента.
При усилении защиты можно еще шифровать, пока нет необходимости.

### Проблема 3: написать безопасное приложение сложно

Чтобы нельзя было анонимно взламывать наши сервисы, мы разрешаем доступы к нашим сервисам только пройдя nginx-firewall.

nginx-firewall это: клиенту, помимо JWT, выдается еще и nginx_digest для этого токена. И проверяем эту подпись средствами nginx. Если подпись в пределах 1 дня верна - пользователя пропускаем. Если день закончился - отправляем на url получения новой подписи.



## Примеры кода

### Пример 1. Создание пользователя с ролями

```python
def test():
    make_models.make_models()
    start_views()
    init_ds()

    import random

    ri = random.randint(1, 10000)
    user = models.CmfPerson()
    user.login = f"nikita+{ri}@carbonsoft.ru"
    user.email = f"nikita+{ri}@carbonsoft.ru"
    user.last_name = "Кулаков"
    user.first_name = "Никита"
    user.second_name = "Юрьевич"

    user.save()
    user.dp.commit()

    print(models.CmfPerson.get(user.id))

    role = models.CmfRole()
    role.name = "MSK Manager"
    role.save()
    role.dp.commit()

    role_perms = models.CmfRolePerms()
    role_perms.role = role
    role_perms.obj = "module:helpdesk"
    role_perms.obj = "object:CmfPerson:1"
    role_perms.obj = "class:CmfPerson"
    role_perms.perms = ["read", "create", "delete"]
    role_perms.save()
    role_perms.dp.commit()

    print(models.CmfRolePerms.get(role_perms.id))
```
