Русский
Русский
English
Статистика
Реклама

Token

Windows Tokens

15.12.2020 16:09:41 | Автор: admin

Эксперт OTUS Александр Колесников поделился с нами полезной статьёй, которую написал специально для студентов курса "Пентест. Практика тестирования на проникновение".

Мы же в свою очередь приглашаем всех желающих принять участие в бесплатном демо-уроке на тему: "Windows AD сбор информации, эскалация привилегий. Эксплойты и уязвимости последних 5ти лет."

Предыдущая статья рассказывала о том, как эксплойты используют технику копирования токена для повышения привилегий. В этой статье мы попытаемся рассмотреть еще одну технику модификациитокенов, а также постараемся узнать, как задаются привилегии, которые помещаются в Primary Token.

Все примеры в статье имеют ознакомительный характер, и если вы захотите повторить эксперименты, то используйте виртуальную машину, так как некоторые из действий могут вывести вашу рабочую машину из строя. Все эксперименты выполняются только для исследовательских задач.

Настройка тестового стенда

Для экспериментов подойдет пара виртуальных машин, которые работают по NAT сети. Две потому что нет другого способа проводить ядерную отладку операционной системы. А NAT сеть, чтобы были отладочные символы, с ними проще и быстрее искать данные без исходного кода. Настройка отладчика есть на просторах сети, но все же ниже будет пару строк как это делается для операционной системы Windows 10 1909.

1. Устанавливаем отладчик. Для операционной системы Windows есть только Windbg Preview, установим его:

2. Переводим целевую операционную систему в отладочный режим:

Перезагружаем систему. И открываем вторую машину, которая будет содержать отладчик:

После перезагрузки системы:

Так как это Windbg Preview все настройки по загрузке и хранению отладочных символов, были выполнены автоматически. Нам остается только дождаться когда они будут полностью загружены. После этого начинаем наше исследование.

Token

Официальная документация гласит, что основные данные, которые существуют в операционной системе касательно привилегий пользователя хранятся в специальной структуре, которая называется Primary Token. В прошлой статье мы не останавливались подробно на части, которая описывается Se* привилегиями. Наиболее интересные привилегии, которые могут быть использованы для эскалации привилегий, вывода из строя ОС:

  • SeAssignPrimaryToken

  • SeAudit

  • SeBackup

  • SeChangeNotify

  • SeCreateToken

  • SeDebug

  • SeLoadDriver

  • SeLockMemory

  • SeManageVolume

  • SeRestore

  • SeSecurity

  • SeTakeOwnership

Каждая строка это константа, которая определяет, какие привилегии существуют в токене. В операционной системе данный из токена включены в специальную структуру, которая называется Security Descriptor:

В качестве подопытного процесса возьмём процесс System. Для получения адреса токена можно ввести следующую команду dx @$cursession.Processes[4].KernelObject.Token

Токен хранится с использованием EXFAST_REF это означает, что нужно занулить последний байт, чтобы вычислить адрес токена:

Посмотрим полный список групп и привилегий у пользователя System:

Стоит обратить внимание на тот факт, что найти Security Descriptor можно за несколько шагов, которые представлены выше на снимке. Полный список информации о привилегиях и группах представлен ниже:

Исследуем, что будет, если мы модифицируем ссылку на токен из отладчика:

Из-за изменения токена на нулевой, произошёл BSOD. В Windows была возможность использовать нулевой токен, но всё закончилось на Windows 10 1607. Был имплементирован механизм, который вызывает BSOD, если ссылка на токен в Security Descriptor модифицируется. Анализ дампа показывает, что проблема в испорченном объекте:

А можем ли мы все таки произвести модификацию без BSOD? Попробуем модифицировать так, чтобы осталась возможность работать с ОС. Для проверки попробуем создать условия, при которых токен обычного пользователя включал максимальное количество привилегий:

1.Найдем системный процесс и выясним, какие привилегии у него включены:

2.Пройдем в токен cmd.exe просмотрим текущее значение на включенные права:

3.Модифицируем права. Список прав после модификации:

Модификация прошла успешно, но все равно пользователь не может аттачиться к ряду системных приложений. Из-за того, что у пользователя еще остались ограничения из-за Integrity Levels и дополнительных битов, которые запрещают взаимодействие с процессами.

А можно ли провести выставление конкретных прав у пользователя в дескрипторе? Попробуем определить. В официальной документации указываются только строковые приставления констант, попробуем переписать токен значениями: 0x1

BSOD не произошло, похоже что механизм защиты просматривает только случаи, когда испорчен заголовок объекта. Это хорошо, значит, можно брутить. Первое значение, которое показало какой-то вменяемый результат 0x100000.

В итоге перебора значений были получены следующие данные о константах в токене:

  • SeDebugPrivilege - 0x100000

  • SeAuditPrivilege - 0x200000

  • SeSystemEnvironmentPrivilege - 0x400000

  • SeCreatePermanentPrivilege - 0x010000

  • SeSystemtimePrivilege - 0x001000

  • SeSecurityPrivilege - 0x000100

  • SeLockMemoryPrivilege - 0x000010

Получается, что хранимые привилегии это всего лишь байт в поле из 6 байт. И все действия, которые были проведены в отладчике, могут быть выполнены через shellcode, только нужно учитывать, что в эксперименте мы просто проставляли права как существующие, но для их полноценного использования их нужно еще включить. То есть записать привилегии по адресу SEPTOKEN_PRIVILEGES и _SEPTOKENPRIVILEGES+0x8.

Проведем следующий эксперимент отнимем привилегии у процесса System и посмотрим, сможем ли мы выполнить операции, которые требуют расширенных привилегий.

В качестве теста была запущена команда netstat -ab, команда успешно отработала. Почему так произошло? Ведь все данные из токена о привилегиях были удалены. Здесь стоит вспомнить, что в ОС Windows привилегии наследуются от группы к пользователю, поэтому пока пользователь System принадлежит группам из списка ниже, он сможет продолжать использовать свои привилегии:

Ну и напоследок, шелкод, который можно использовать в качестве payload для эксплойтов из прошлой статьи:

[BITS 64]start:mov r9, [gs:0x188]                ;KPROCESS/currentThreadmov r9, [r9+0x220]                ;EPROCESS смещение к KTHREADmov r8, [r9+0x3e8]                ;InheritedFromUniqueProcessId (cmd.exe PID)mov rax, r9                           loop1:  mov rax, [rax + 0x2f0]         sub rax, 0x2f0                    ;KPROCESS  cmp [rax + 0x2e8],r8              ;сравнить ProcessId  jne loop1                           mov rcx, rax                        ;если найден нужный PID EPROCESSadd rcx, 0x360                        mov rax, [rcx]                           and rax, 0xFFFFFFFFFFFFFFF0mov r8,  0x1e73deff20               ;System набор привилегийmov [rax+0x48],r8                   ;Перезапись привилегийret

Узнать подробнее о курсе "Пентест. Практика тестирования на проникновение".

Смотреть открытый вебинар на тему
"Windows AD сбор информации, эскалация привилегий. Эксплойты и уязвимости последних 5ти лет."


ЗАБРАТЬ СКИДКУ

Подробнее..

Поддержка токенов PKCS11 с ГОСТ-криптографией в Python. Часть II Объекты класса Token

18.03.2021 18:13:59 | Автор: admin
imageВ предыдущей статье был представлен модуль pyp11, написанный на языке Си и обеспечивающий поддержку токенов PKCS#11 с российской криптографией. В этой статье будет рассмотрен класс Token, который позволит упростить использование функционала модуля pyp11 в скриптах, написанных на Python-е. Отметим, что в качестве прототипа этого класса был взят класс token, написанный на TclOO и который используется в утилите cryptoarmpkcs:


Прототип класса Token
oo::class create token {  variable libp11  variable handle  variable infotok  variable pintok  variable nodet#Конструктор  constructor {handlelp11 labtoken slottoken} {    global pass    global yespas    set handle $handlelp11    set slots [pki::pkcs11::listslots $handle]    array set infotok []    foreach slotinfo $slots {      set slotflags [lindex $slotinfo 2]      if {[lsearch -exact $slotflags TOKEN_PRESENT] != -1} {        if {[string first $labtoken [lindex $slotinfo 1]] != -1} {          set infotok(slotlabel) [lindex $slotinfo 1]          set infotok(slotid) [lindex $slotinfo 0]          set infotok(slotflags) [lindex $slotinfo 2]          set infotok(token) [lindex $slotinfo 3]          #Берется наш токен          break        }      }    }    #Список найденных токенов в слотах    if {[llength [array names infotok]] == 0 } {      error "Constructor: Token not present for library   : $handle"    }    #Объект какого токена    set nodet [dict create pkcs11_handle $handle]    dict set nodet pkcs11_slotid $infotok(slotid)    set tit "Введите PIN-код для токене $infotok(slotlabel)"    set xa [my read_password $tit]    if {$xa == "no"} {      error "Вы передумали вводить PIN для токена $infotok(slotlabel)"    }    set pintok $pass    set pass ""    set rr [my login ]    if { $rr == 0 } {      unset pintok      error "Проверьте PIN-код токена $infotok(slotlabel)."    } elseif {$rr == -1} {      unset pintok      error "Отсутствует токен."    }    my logout  }#Методы класса  method infoslot {} {    return [array get infotok]  }  method listcerts {} {    array set lcerts []    set certsder [pki::pkcs11::listcertsder $handle $infotok(slotid)]    #Перебираем сертификаты    foreach lc $certsder {      array set derc $lc      set lcerts($derc(pkcs11_label)) [list $derc(cert_der) $derc(pkcs11_id)]      #parray derc    }    return [array get lcerts]  }  method read_password {tit} {    global yespas    global pass    set tit_orig "$::labpas"    if {$tit != ""} {      set ::labpas "$tit"    }    tk busy hold ".st.fr1"    tk busy hold ".st.fr3"    #place .topPinPw -in .st.fr1.fr2_certs.labCert  -relx 1.0 -rely 3.0 -relwidth 3.5    place .topPinPw -in .st.labMain  -relx 0.35 -rely 5.0 -relwidth 0.30    set yespas ""    focus .topPinPw.labFrPw.entryPw    vwait yespas    catch {tk busy forget ".st.fr1"}    catch {tk busy forget ".st.fr3"}    if {$tit != ""} {      set ::labpas "$tit_orig"    }    place forget .topPinPw    return $yespas  }  unexport read_password  method rename {type ckaid newlab} {    if {$type != "cert" && $type != "key" && $type != "all"} {      error "Bad type for rename "    }    set uu $nodet    lappend uu "pkcs11_id"    lappend uu $ckaid    lappend uu "pkcs11_label"    lappend uu $newlab    if { [my login ] == 0 } {      unset uu      return 0    }    pki::pkcs11::rename $type $uu    my logout    return 1  }  method changeid {type ckaid newid} {    if {$type != "cert" && $type != "key" && $type != "all"} {      error "Bad type for changeid "    }    set uu $nodet    lappend uu "pkcs11_id"    lappend uu $ckaid    lappend uu "pkcs11_id_new"    lappend uu $newid    if { [my login ] == 0 } {      unset uu      return 0    }    pki::pkcs11::rename $type $uu    my logout    return 1  }  method delete {type ckaid} {    if {$type != "cert" && $type != "key" && $type != "all" && $type != "obj"} {      error "Bad type for delete"    }    set uu $nodet    lappend uu "pkcs11_id"    lappend uu $ckaid    my login    ::pki::pkcs11::delete $type $uu    my logout    return 1  }  method deleteobj {hobj} {    set uu $nodet    lappend uu "hobj"    lappend uu $hobj#tk_messageBox -title "class deleteobj" -icon info -message "hobj: $hobj\n" -detail "$uu"    return [::pki::pkcs11::delete obj $uu ]  }  method listmechs {} {    set llmech [pki::pkcs11::listmechs $handle $infotok(slotid)]    return $llmech  }  method pubkeyinfo {cert_der_hex} {    array set linfopk [pki::pkcs11::pubkeyinfo $cert_der_hex $nodet]    return [array get linfopk]  }  method listobjects {type} {    if {$type != "cert" && $type != "pubkey" && $type != "privkey" && $type != "all" && $type != "data"} {      error "Bad type for listobjects "    }    set allobjs [::pki::pkcs11::listobjects $handle $infotok(slotid) $type]    return $allobjs  }  method importcert {cert_der_hex cka_label} {    set uu $nodet    dict set uu pkcs11_label $cka_label    if {[catch {set pkcs11id [pki::pkcs11::importcert $cert_der_hex $uu]} res] } {      error "Cannot import this certificate:$res"      #          return 0    }    return $pkcs11id  }  method login {} {    set wh 1    set rl -1    while {$wh == 1} {      if {[catch {set rl [pki::pkcs11::login $handle $infotok(slotid) $pintok]} res]} {        if {[string first "SESSION_HANDLE_INVALID" $res] != -1} {          pki::pkcs11::closesession $handle          continue        } elseif {[string first "TOKEN_NOT_PRESENT" $res] != -1} {          set wh 0          continue        }      }      break    }    if {$wh == 0} {      return -1    }    return $rl  }  method logout {} {    return [pki::pkcs11::logout $handle $infotok(slotid)]  }  method keypair {typegost parkey} {    my login    set skey [pki::pkcs11::keypair $typegost $parkey $nodet]    my logout    return $skey  }  method digest {typehash source} {    return [pki::pkcs11::digest $typehash $source $nodet]  }  method signkey {ckm digest hobj_priv} {    set uu $nodet    dict set uu hobj_privkey $hobj_priv    my login    set ss [pki::pkcs11::sign $ckm $digest $uu]    my logout    return $ss  }  method signcert {ckm digest pkcs11_id} {    set uu $nodet    dict set uu pkcs11_id $pkcs11_id    my login    set ss  [pki::pkcs11::sign $ckm $digest $uu]    my logout    return $ss  }  method verify {digest signature asn1pubkey} {    set uu $nodet    dict set uu pubkeyinfo $asn1pubkey    return [pki::pkcs11::verify $digest $signature $uu]  }  method tokenpresent {} {    set slots [pki::pkcs11::listslots $handle]    foreach slotinfo $slots {      set slotid [lindex $slotinfo 0]      set slotlabel [lindex $slotinfo 1]      set slotflags [lindex $slotinfo 2]      if {[lsearch -exact $slotflags TOKEN_PRESENT] != -1} {        if {infotok(slotlabel) == $slotlabel && $slotid == $infotok(slotid)} {          return 1        }      }    }    return 0  }  method setpin {type tpin newpin} {    if {$type != "user" && $type != "so"} {      return 0    }    if {$type == "user"} {      if {$tpin != $pintok} {        return 0      }    }    set ret [::pki::pkcs11::setpin  $handle $infotok(slotid) $type $tpin $newpin]    if {$type == "user"} {      if {$ret} {        set pitok $newpin      }    }    return $ret  }  method inituserpin {sopin upin} {    set ret [::pki::pkcs11::inituserpin $handle $infotok(slotid) $sopin $upin]    return $ret  }  method importkey {uukey} {    set uu $nodet    append uu " $uukey"    my login    if {[catch {set impkey [pki::pkcs11::importkey $uu ]} res] } {        set impkey 0    }    my logout    return $impkey  }#Деструктор  destructor {    variable handle    if {[info exists pintok]} {      my login    }    #    ::pki::pkcs11::unloadmodule  $handle  }}

Класс Token, как и любой класс в Python, включает конструктор, методы и деструктор. Конструктор и деструктор это те же методы, только с предопределёнными именами. Конструктор имеет имя __init__, а деструктор имя __del__. Объявление конструктора и деструктора можно опускать. И в классе Token мы опустим объявление деструктора, а вот конструктор будет необходим. Конструктор будет создавать экземпляр класса Token для конкретного токена с конкретными атрибутами.

I. Конструктор класса Token


Итак, конструктор класса Token выглядит следующим образом:
import sysimport pyp11class Token:  def __init__ (self, handlelp11, slottoken, serialnum):    flags = ''    self.pyver = sys.version[0]    if (self.pyver == '2'):        print ('Только для python3')        quit()#Сохраняем handle библиотеки PKCS#11    self.handle = handlelp11#Сохраняем номер слота с токеном    self.slotid = slottoken#Сохраняем серийный номер токена    self.sn = serialnum#Проверяем наличие в указанном слоте токена с заданным серийным номером    ret, stat = self.tokinfo()#Проверяем код возврата    if (stat != ''):#Возвращаем информацию об ошибке        self.returncode = stat        return#Экземпляр класса (объект) успешно создан


Параметрами конструктора (метода __init__) являются (помимо обязательного self) handle библиотеки токена (handlelp11), номер слота (slottoken), в котором должен находиться токен, и серийный номер токена (serialnum).
Для получения handle библиотеки pkcs#11, номеров слотов и информации о находящихся в них токенах можно использовать следующий скрипт:
#!/usr/bin/python3import sysimport pyp11from Token import Tokendef listslots (handle):    slots = pyp11.listslots(aa)    i = 0    lslots = []    for v in slots:        for f in v[2]:        if (f == 'TOKEN_PRESENT'):                i = 1                lslots.append(v)                break    i += 1    return (lslots)#Библиотеки для Linux#Программный токенlib = '/usr/local/lib64/libls11sw2016.so'#Облачный токен#lib = '/usr/local/lib64/libls11cloud.so'#Аппаратный токен#lib = '/usr/local/lib64/librtpkcs11ecp_2.0.so'#Библиотеки для Windows#lib='C:\Temp\ls11sw2016.dll'try:#Вызываем команду загрузки библиотеки и получаем её handle (дескриптор библиотеки)    aa = pyp11.loadmodule(lib)    print('Handle библиотеки ' + lib + ': ' + aa)except:    print('Except load lib: ')    e = sys.exc_info()[1]    e1 = e.args[0]#Печать ошибки    print (e1)    quit()#Список слотовslots = listslots(aa)i = 0for v in slots:    for f in v[2]:        if (f == 'TOKEN_PRESENT'):        if (i == 0):            print ('\nИнформация о токенах в слотах\n')        it = v[3]        print ('slotid=' + str(v[0]))        print ('\tFlags=' + str(v[2]))        print ('\tLabel="' + it[0].strip() + '"')        print ('\tManufacturer="' + it[1].strip() + '"')        print ('\tModel="' + it[2].strip() + '"')        print ('\tSerialNumber="' + it[3].strip() + '"')        i = 1        break    i += 1pyp11.unloadmodule(aa)if (i == 0):    print ('Нет ни одного подключенного токена. Вставьте токен и повторите операцию')quit()

Если с библиотекой и слотом всё ясно, то с серийным номером токена может возникнуть вопрос а зачем этот параметр нужен и почему именно он, а, например, не метка токена. Сразу оговоримся, что это принципиально для извлекаемых токенов, когда злоумышленником (или случайно) один токен в слоте будет заменён другим токеном. Более того, различные экземпляры токена могут иметь одинаковые метки. И наконец, токен может быть еще не проинициализирован или владелец будет его переинициализировать, в частности, сменит метку токена. Теоретически даже серийный номер не гарантирует его идентичность, оптимально учитывать всю информацию о токене (серийный номер, модель, производитель). В задачи конструктора и входит сохранение в переменных создаваемого экземпляра класса аргументов объекта токен:
...#Сохраняем handle библиотеки PKCS#11    self.handle = handlelp11#Сохраняем номер слота с токеном    self.slotid = slottoken#Сохраняем серийный номер токена    self.sn = serialnum...

Проверкой наличия указанного токена в указанном слоте занимается метод tokinfo(), определенный в данном классе.
Метод tokinfo возвращает два значения (см. выше в конструкторе):
#Проверяем наличие в указанном слоте токена с заданным серийным номером    ret, stat = self.tokinfo()

В первой переменной (ret) содержится результат выполнения метода, а во второй (stat) информация о том, как завершилось выполнение метода. Если вторая переменная пуста, то метод tokinfo успешно выполнился. Если вторая переменная не пуста, то выполнение метода завершилось с ошибкой. Информация об ошибке будет находиться в этой переменной. При обнаружении ошибки выполнения метода self.tokinfo конструктор записывает её в переменную returncode:
#Проверяем наличие в указанном слоте токена с заданным серийным номером    ret, stat = self.tokinfo()#Проверяем код возврата    if (stat != ''):#Возвращаем информацию об ошибке в переменной returncode        self.returncode = stat        return

После создания объекта (экземпляра класса) необходимо проверить значение переменной returncode, чтобы быть уверенным в том, что объект для указанного токена создан:
#!/usr/bin/python3import sysimport pyp11from Token import Token#Выбираем библиотеку#Аппаратный токенlib = '/usr/local/lib64/librtpkcs11ecp_2.0.so'try:    aa = pyp11.loadmodule(lib)except:    e = sys.exc_info()[1]    e1 = e.args[0]    print (e1)    quit()#Серийный номер токенаsn = '9999999999999999'slot = 110#Создаем объект токенаt1 = Token(aa, slot, sn)#Проверка переменной returncodeif (t1.returncode != ''):#Объект создан с ошибкой    print (t1.returncode)#Уничтожение объекта    del t1#Завершение скрипта    quit()#объект успешно создан. . .

Если обнаружена ошибка при создании объекта, то целесообразно этот объект уничтожить:
del <идентификатор объекта>

II. Архитектура методов в классе Token


Главным принципом при написании методов было то, чтобы обработка исключений была внутри методов, а информация об исключениях (ошибках) возвращалась в текстовом виде. Исходя из этого все методы возвращают два значения: собственно результат выполнения и информацию об ошибке. Если ошибок нет, то второе значение пустое. Мы это уже видели на примере использования метода tokinfo в конструкторе. А вот и сам код метода tokinfo:
  def tokinfo(self):    status = ''#Получаем список слотов    try:        slots = pyp11.listslots(self.handle)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        dd = ''        status = e1        return (dd, status)    status = ''#Ищем заданный слот с указанным  токеном#Перебираем слоты    for v in slots:#Ищем заданный слот            if (v[0] != self.slotid):                status = "Ошибочный слот"                continue            self.returncode = ''#Список флагов текущего слота            self.flags = v[2]#Проверяем наличие в стоке токена            if (self.flags.count('TOKEN_PRESENT') !=0):#Проверяем серийный номер токена                tokinf = v[3]                sn = tokinf[3].strip()                if (self.sn != sn):                    status = 'Серийный номер токена=\"' + sn + '\" не совпадает с заданным \"' + self.sn + '\"'                    dd = ''                    return (dd, status)                status = ''                break            else:                dd = ''                status = "В слоте нет токена"                return (dd, status)    tt = tokinf    dd = dict(Label=tt[0].strip())    dd.update(Manufacturer=tt[1].strip())    dd.update(Model=tt[2].strip())    dd.update(SerialNumber=tt[3].strip())    self.infotok = dd#Возвращаемые значения    return (dd, status)

Полное описание класса Token находится здесь.
import sysimport pyp11class Token:  def __init__ (self, handlelp11, slottoken, serialnum):    flags = ''    self.pyver = sys.version[0]    if (self.pyver == '2'):        print ('Только для python3')        quit()#Сохраняем handle библиотеки PKCS#11    self.handle = handlelp11#Сохраняем номер слота с токеном    self.slotid = slottoken#Сохраняем серийный номер токена    self.sn = serialnum#Проверяем наличие в указанном слоте с токена с заданным серийным номером    ret, stat = self.tokinfo()#Проверяем код возврата    if (stat != ''):#Возвращаем информацию об ошибке        self.returncode = stat        return#Экземпляр класса (объект) успешно создан  def tokinfo(self):    status = ''#Получаем список слотов    try:        slots = pyp11.listslots(self.handle)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        dd = ''        status = e1        return (dd, status)    status = ''#Ищем заданный слот с указанным  токеном#Перебираем слоты    for v in slots:#Ищем заданный слот            if (v[0] != self.slotid):                status = "Ошибочный слот"                continue            self.returncode = ''#Список флагов текущего слота            self.flags = v[2]#Проверяем наличие в стоке токена            if (self.flags.count('TOKEN_PRESENT') !=0):#Проверяем серийный номер токена                tokinf = v[3]                sn = tokinf[3].strip()                if (self.sn != sn):                    status = 'Серийный номер токена=\"' + sn + '\" не совпадает с заданным \"' + self.sn + '\"'                    dd = ''                    return (dd, status)                status = ''                break            else:                dd = ''                status = "В слоте нет токена"                return (dd, status)    tt = tokinf    dd = dict(Label=tt[0].strip())    dd.update(Manufacturer=tt[1].strip())    dd.update(Model=tt[2].strip())    dd.update(SerialNumber=tt[3].strip())    self.infotok = dd    return (dd, status)  def listcerts(self):    try:        status = ''        lcerts = pyp11.listcerts(self.handle, self.slotid)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        lcerts = ''        status = e1    return (lcerts, status)  def listobjects(self, type1, value = '' ):    try:        status = ''        if (value == ''):        lobjs = pyp11.listobjects(self.handle, self.slotid, type1)        else:        lobjs = pyp11.listobjects(self.handle, self.slotid, type1, value)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        lobjs = ''        status = e1    return (lobjs, status)  def rename(self, type, pkcs11id, label):    try:        status = ''        dd = dict(pkcs11_id=pkcs11id, pkcs11_label=label)        ret = pyp11.rename(self.handle, self.slotid, type, dd)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        ret = ''        status = e1    return (ret, status)  def changeckaid(self, type, pkcs11id, pkcs11idnew):    try:        status = ''        dd = dict(pkcs11_id=pkcs11id, pkcs11_id_new=pkcs11idnew)        ret = pyp11.rename(self.handle, self.slotid, type, dd)    except:#Проблемы с библиотекой токена        e = sys.exc_info()[1]        e1 = e.args[0]        ret = ''        status = e1    return (ret, status)  def login(self, userpin):    try:        status = ''        bb = pyp11.login (self.handle, self.slotid, userpin)    except:        e = sys.exc_info()[1]        e1 = e.args[0]        bb = 0        status = e1    return (bb, status)  def logout(self):    try:        status = ''        bb = pyp11.logout (self.handle, self.slotid)    except:        e = sys.exc_info()[1]        e1 = e.args[0]        bb = 0        status = e1    return (bb, status)  def keypair(self, typek, paramk, labkey):#Параметры для ключей    gost2012_512 = ['1.2.643.7.1.2.1.2.1', '1.2.643.7.1.2.1.2.2', '1.2.643.7.1.2.1.2.3']    gost2012_256 = ['1.2.643.2.2.35.1', '1.2.643.2.2.35.2',  '1.2.643.2.2.35.3',  '1.2.643.2.2.36.0', '1.2.643.2.2.36.1', '1.2.643.7.1.2.1.1.1', '1.2.643.7.1.2.1.1.2', '1.2.643.7.1.2.1.1.3', '1.2.643.7.1.2.1.1.4']    gost2001 = ['1.2.643.2.2.35.1', '1.2.643.2.2.35.2',  '1.2.643.2.2.35.3',  '1.2.643.2.2.36.0', '1.2.643.2.2.36.1']#Тип ключа    typekey = ['g12_256', 'g12_512', 'gost2001']    genkey = ''    if (typek == typekey[0]):    gost = gost2012_256    elif (typek == typekey[1]):    gost = gost2012_512    elif (typek == typekey[2]):    gost = gost2001    else:    status = 'Неподдерживаемый тип ключа'    return (genkey, status)    if (gost.count(paramk) == 0) :    status = 'Неподдерживаемые параметры ключа'    return (genkey, status)    try:#Ошибок нет, есть ключевая пара    status = ''    genkey = pyp11.keypair(self.handle, self.slotid, typek, paramk, labkey)    except:#Не удалось создать ключевую пару    e = sys.exc_info()[1]    e1 = e.args[0]    print (e1)#Возвращаеи текст ошибки в словаре    status = e1    return (genkey, status)     def digest(self, typehash, source):#Считаем хэш    try:        status = ''        digest_hex = pyp11.digest (self.handle, self.slotid, typehash, source)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в словаре    status = e1    digest_hex = ''    return (digest_hex, status)#Формирование подписи  def sign(self, ckmpair, digest_hex, idorhandle):#Для подписи можно использовать CKA_ID или handle закрытого ключа    try:        status = ''        sign_hex = pyp11.sign(self.handle, self.slotid, ckmpair, digest_hex, idorhandle)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в словаре    status = e1    sign_hex = ''    return (sign_hex, status)#Проверка подписи  def verify(self, digest_hex, sign_hex, pubkeyinfo):#Для подписи можно использовать CKA_ID или handle закрытого ключа    try:        status = ''        verify = pyp11.verify(self.handle, self.slotid, digest_hex, sign_hex, pubkeyinfo)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    verify = 0    status = e1    return (verify, status)#Инициализировать токен  def inittoken(self, sopin, labtoken):    try:        status = ''        dd = pyp11.inittoken (self.handle, self.slotid, sopin, labtoken)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    return (dd, status)#Инициализировать пользовательский PIN-код  def inituserpin(self, sopin, userpin):    try:        status = ''        dd = pyp11.inituserpin (self.handle, self.slotid, sopin, userpin)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    return (dd, status)#Сменить пользовательский PIN-код  def changeuserpin(self, oldpin, newpin):    try:        status = ''        dd = pyp11.setpin (self.handle, self.slotid, 'user', oldpin, newpin)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    self.closesession ()    return (dd, status)  def closesession(self):    try:        status = ''        dd = pyp11.closesession (self.handle)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = 0    status = e1    return (dd, status)  def parsecert(self, cert_der_hex):    try:        status = ''        dd = pyp11.parsecert (self.handle, self.slotid, cert_der_hex)    except:#Не удалось разобрать сертификат    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def importcert(self, cert_der_hex, labcert):    try:        status = ''        dd = pyp11.importcert (self.handle, self.slotid, cert_der_hex, labcert)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def delobject(self, hobject):    try:        status = ''        hobjc = dict(hobj=hobject)        dd = pyp11.delete(self.handle, self.slotid, 'obj', hobjc)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def delete(self, type, pkcs11id):    if (type == 'obj'):        dd = ''        status = 'delete for type obj use nethod delobject'        return (dd, status)    try:        status = ''        idobj = dict(pkcs11_id=pkcs11id)        dd = pyp11.delete(self.handle, self.slotid, type, idobj)    except:    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)  def listmechs(self):    try:        status = ''        dd = pyp11.listmechs (self.handle, self.slotid)    except:#Не удалось получить список механизмов токена    e = sys.exc_info()[1]    e1 = e.args[0]#Возвращаеи текст ошибки в status    dd = ''    status = e1    return (dd, status)


Рассмотрим импользование функционала модуля pyp11 и аналогичных операторов с использованием класса Token.
В последнем случае необходимо будет создать и объект токена:
<дескриптор объекта> = Token(<дескриптор библиоткети>, <номер слота>, <серийный номер>)if (<дескриптор объекта>.returncode != ''):   print('Ошибка при создании объекта:')#Печать ошибки   print(<дескриптор объекта>.returncode)#Уничтожение объекта   del <дескриптор объекта>   quit()

Начнем с инициализации токена:
try:    ret = pyp11.inittoken (<дескриптор библиоткети>, <номер слота>, <SO-PIN>, <метка токена>)except:#Не удалось проинициализировать токен    e = sys.exc_info()[1]    e1 = e.args[0]    print (e1)    quit()

Аналогичный код при использовании класса Token выглядит так (идентификатор объекта t1):
ret, stat = t1.inittoken(<SO-PIN>, <метка токена>)#Проверка корретности инициализацииif (stat != ''):   print('Ошибка при инициализации токена:')#Печать ошибки   print(stat)   quit()  

Далее мы просто дадим соответствие основных операторов модуля pyp11 и методов класса Token без обработки исключений и ошибок:
<handle> := <дескриптор библиотеки pkcs11><slot> := <дескриптор слота с токеном><error> := <переменная с текстом ошибки><ret> := <результат выполнения оператора><cert_der_hex> := <сертификат в DER-формате в HEX-кодировке>=================================================#Инициализация пользовательского PIN-кода<ret> = pyp11.inituserpin (<handle>, <slot>, <SO-PIN>, <USER-PIN>)<ret>, <error> = <идентификатор объекта>.inituserpin (<SO-PIN>, <USER-PIN>)#Смена USER-PIN кода<ret> = pyp11.setpin (<handle>, <slot>, 'user', <USER-PIN старый>, <USER-PIN новый>)<ret>, <error> = t1.changeuserpin (<USER-PIN старый>, <USER-PIN новый>)#Смена SO-PIN кода<ret> = pyp11.setpin (<handle>, <slot>, 'so', <SO-PIN старый>, <SO-PIN новый>)<ret>, <error> = t1.changesopin (<SO-PIN старый>, <SO-PIN новый>)#Login<ret> = pyp11.login (<handle>, <slot>, <USER-PIN>)<ret>, <error> = t1.login (<USER-PIN>)#Logout<ret> = pyp11.logout (<handle>, <slot>)<ret>, <error> = t1.logout ()#Закрытие сессии<ret> = pyp11.closesession (<handle>)<ret>, <error> = t1.closesession ()#Список сертификатов на токене<ret> = pyp11.listcerts (<handle>, <slot>)<ret>, <error> = t1.listcerts ()#Список объектов на токене<ret> = pyp11.listobjects (<handle>, <slot>, <'cert' | 'pubkey' | 'privkey' | 'data' | 'all'> [, 'value'])<ret>, <error> = t1.listobjects (<'cert' | 'pubkey' | 'privkey' | 'data' | 'all'> [, 'value'])#Разбор сертификата<ret> = pyp11.parsecert (<handle>, <slot>, <cert_der_hex>)<ret>, <error> = t1.parsecert(<cert_der_hex>)#Импорт сертификата<ret> = pyp11.importcert (<handle>, <slot>, <cert_der_hex>, <Метка сертификата>)<ret>, <error> = t1.importcert(<cert_der_hex>, <Метка сертификата>)#Вычисление хэша<ret> = pyp11.digest (<handle>, <slot>, <тип алгоритма>, <контент>)<ret>, <error> = t1.digest(<тип алгоритма>, <контент>)#Вычисление электронной подписи<ret> = pyp11.digest (<handle>, <slot>, <механизм подписи>, <хэш от контента>, <CKA_ID | handle закрытого ключа>)<ret>, <error> = t1.digest(<механизм подписи>, <хэш от контента>, <CKA_ID | handle закрытого ключа>)#Проверка электронной подписи<ret> = pyp11.verify (<handle>, <slot>, <хэш от контента>, <подпись>, <asn1-структура subjectpublickeyinfo в hex>)<ret>, <error> = t1.verify(<хэш от контента>, <подпись>, <asn1-структура subjectpublickeyinfo в hex>)#Генерация ключевой пары<ret> = pyp11.keypair (<handle>, <slot>, <тип ключа>, <OID криптопараметра>, <CKA_LABEL>)<ret>, <error> = t1.keypair(<тип ключа>, <OID криптопараметра>, <CKA_LABEL>)

III. Сборка и установка модуля pyp11 с классом Token


Сборка и установка модуля pyp11 с классом Token ничем не отличается от описанной в первой части.
Итак, скачиваем архив и распаковываем его. Заходим в папку PythonPKCS11 и выполняем команду установки:
python3 setup.py install

После установки модуля переходим в папку tests и запускаем тесты для модуля pyp11.
Для тестирования класса Token переходим в папку test/classtoken.
Для подключения модуля pyp11 и класса Token в скрипты достаточно добавить следующие операторы:
import pyp11from Token import Token


IV. Заключение


В ближайшее время должна появиться и третья часть статьи, в которой будет рассказано, как добавить поддержку российской криптографии в проект PyKCS11.

P.S. Хочу сказать спасибо svyatikov за то, что помог протестировать проект на платформе Windows.
Подробнее..

USB over IP удалённое администрирование

21.06.2021 16:11:25 | Автор: admin

Большую часть своей жизни я жил и работал в Москве. Занимался тем, что менял здоровье на деньги. Правда, на очень хорошие, поэтому под занавес пятого десятка купил квартиру в тихом приморском городке, далеко от столичной суеты и ежедневной нервотрёпки.

Особой необходимости в работе у меня не было. Дети уже стали самостоятельными настолько, что запросто могут содержать нас с женой. Да и скопить удалось прилично. Однако, были три проблемы.

Работа в тихом городке

Во-первых до пенсии неплохо хоть где-то формально числиться. Во-вторых, без работы скучно. В-третьих, даже небольшие деньги лишними никогда не бывают.

Однако, в маленьком приморском городке работы для айтишника не было никакой, а ездить в областной центр мне совсем не хотелось. Так я стал ИП и занялся дистанционным обслуживанием компаний, которым было накладно содержать постоянного админа.

Буквально за пару недель я обзавёлся первыми клиентами небольшими компаниями-мелкооптовиками. В штате 10-50 работников, главбух на аутсорсе, сервер с 1С и одним общим для всех каталогом. Почту используют бесплатную, вопросами ИБ заморачиваются по минимуму. В-общем, достаточно типичный для нашей страны мелкий бизнес.

Иногда приходилось выезжать на место, но не часто. Большинство задач вполне решалось дистанционно либо через TeamViewer, либо по телефону. Меня это устраивало, клиентов тоже.

Токены головная боль

Например, надо срочно отправить счёт. Сотрудник вводит пароль, входит в Диадок, загружает документ Но подписать его не может его коллега вчера сунул ключ себе в карман, а сегодня не пришёл на работу, поскольку с утра поехал утрясать какие-то вопросы с партнёром. Приходится отгружать товар без оплаты. Это означает, что деньги придут с большой задержкой как известно, уже оказанная услуга услугой не считается.

Разумеется, меня в этом ни один заказчик не упрекал. Но почти все постоянно намекали, что было бы неплохо как-то раз и навсегда решить проблему с токенами техническими средствами, поскольку административными уже пытались, но безрезультатно.

Всё действительно сложно

Сначала задача показалась мне простой. Технология USB over IP существует достаточно давно все токены подключены к одному устройству, а сотрудники работают с ними, как с локальными. Ничего не теряется, поскольку не покидает пределов офиса.

Осталось полчасика погуглить, найти решение подешевле и предложить заказчикам. Но ни тут-то было. Поразмыслив, я пришёл к выводу, что такой проект значительно сложнее, чем казался.

Прежде всего я выяснил, сколько токенов используют мои заказчики. Оказалось, от двух до одиннадцати. Стало быть, нужно такое решение одного производителя, чтобы удовлетворить всех, причём с небольшим запасом.

Почему одного производителя? Потому что мудрый Оккам умножения сущностей не приветствовал от этого одни проблемы. Да и дешевле так выходит, как правило.

Поскольку айтишников на фултайме ни в одной конторе не было, самодельные и софтовые решения исключались. Делать, настраивать и чинить некому.

По масштабированию прекрасно подходят облака. Но цены у них откровенно кусаются. Например, сервис FlexiHub обойдётся заказчику более 10 тыс. рублей в месяц за 5 ключей, не считая стоимости хаба. И это без гарантий совместимости.

Но самое главное сервисы требуют регулярной своевременной оплаты. У небольшой компании с этим могут быть проблемы. У неё не всегда есть возможность даже зарплаты сотрудникам заплатить вовремя. Задержка в неделю-другую дело обыкновенное.

В-общем, нужна железка. Точнее, набор устройств с разным количеством портов. Как я писал выше: от двух до одиннадцати с небольшим запасом. Чтобы один раз купил и навсегда твоё.

Если ориентироваться на мировые бренды, то надо брать Digi AnywhereUSB. За 2 USB-порта больше 20 тыс. рублей, за 14 портов в районе 150 тыс. рублей. Для небольших компаний это явно дорого.

Конечная остановка USB over IP концентратор

Перебирая варианты я пришёл к отечественному аппаратно-программному решению DistKontrolUSB. На нём и остановился. Причин этому несколько.

Прежде всего цена. За концентратор на 4 порта мой мелкий клиент заплатит 26 тыс. рублей с хвостиком, а более крупным придётся выложить около 69 тыс. рублей, но за 16 портов. Дешевле западного аналога в два раза.

Второе достоинство отечественного разработчика знание российской специфики. Младшая версия концентратора на 4 порта имеет ограниченную функциональность. В ней отсутствует авторизации для подключения к токенам, нет ограничений доступа и управления питанием портов.

Такой подход разумен, поскольку в небольших организациях все ключи по сути являются общими достаточно одной авторизации для всего. На практике это означает, что даже если реализовать отдельный вход для каждого токена, то получится всего лишь замедление работы сотрудников на время дополнительной аутентификации. Да ещё и возникнет путаница в паролях, что рано или поздно всё равно закончится уменьшением количества пар логин-пароль до одной.

Что касается управления питанием портов, то при их небольшом количестве функция лишена смысла. Проще перезагрузить концентратор полностью. Или руками перевоткнуть зависший USB-ключ.

Таким образом, большинство моих клиентов, обходящихся 2-4 ключами, за относительно небольшую сумму получат надёжное устройство для дистанционной работы с токенами. Причём, без заведомо избыточной функциональности, за которую надо платить.

Третье достоинство удобное удалённое управление концентратором через веб-интерфейс. Это особенно важно для более крупных организаций, которым подойдёт концентратор на 16 портов. Сотрудников там больше и без каких-то элементов ИБ не обойтись.

Допустим, по соображением безопасности требуется ограничить доступ к порту по IP-адресу, а к ключу по логину и паролю. Через панель управления это делается мгновенно, причём в одном модуле. Надо всего два раза сдвинуть соответствующие ползунки и нажать на кнопу Сохранить. Изменения сразу вступят в силу.

Проблема нахождения концов актуальна не только для очень крупных организаций, но и для моих клиентов. При помощи системного журнала в WEB-интерфейсе Управляемого USB over IP концентратора решение этой задачи облегчается. В журнале хранится вся информация о подключениях и отключениях, включая логин пользователя и IP-адрес, с которого было осуществлено подключение. Кстати, пару раз пригодилось.

Раз уж дело дошло до практики, то вот ещё один пример. Хоть правило уходя гасите свет существует, но выполняется не всегда. Один мой заказчик опасался, что в ночное время какой-нибудь хакер взломает концентратор и наделает делов.

В настройках USB over IP концентратора было сформировано автоматическое задание, по которому устройство отключается в заданное время. А чтобы директор не волновался и спал спокойно он получает уведомление о выполнении на электронную почту.

Итоги

Проект по оптимизации работы с токенами я начал в январе этого года. Из семи предприятий, с которыми я работаю, три уже приобрели и успешно используют USB over IP концентраторы. В одной организации это запланировано на лето. Думаю, что и оставшиеся три до конца года примут положительное решение.

Тем более, что моя аргументация теперь основана не только на умозрительных аргументах, но и на практике эксплуатации. Поскольку теперь я что-то смыслю в торговле, то могу оперировать цифрами.

Например, дневная задержка в оплате счёта на один миллион стоит от 100 до 1000 рублей в зависимости от договора. С отчётностью ещё хуже 5% от суммы налога. А это уже несколько тысяч даже для небольшой фирмочки. Причём, только прямого ущерба о каких-то сорвавшихся сделках я даже не говорю.

Сумму убытков из-за подобных задержек владелец компании знает. Стоимость USB over IP концентратора тоже. Срок окупаемости вычисляется без всякой высшей математики.

Насколько я могу судить, первопроходцы уже ушли в плюс. Даже те, кто приобрёл устройство на 16 портов.

Внимательный читатель заметит, что я ничего не написал про стоимость обслуживания. Исправляюсь оно ничего не стоит. По крайней мере для моих клиентов, поскольку работы у меня не прибавилось.

Впрочем, вру. Мои клиенты уже начали рекомендовать меня своим знакомым. Я думаю, что по уровню заработка я тут потихоньку достигну своего бывшего московского уровня. Жалею, что не уехал из Москвы раньше.

Подробнее..

Глобальные объекты в Angular

29.03.2021 16:17:23 | Автор: admin

В JavaScript мы часто используем сущности вроде window, navigator, requestAnimationFrame или location. Некоторые из этих объектов существуют испокон веков, некоторые часть вечно растущего набора Web API. Возможно, вы встречали класс Location или токен DOCUMENT в Angular. Давайте обсудим, для чего они нужны и чему мы можем у них научиться, чтобы сделать наш код чище и более гибким.

DOCUMENT

DOCUMENT это токен из общего пакета Angular. Вот как можно им воспользоваться. Вместо этого:

constructor(private readonly elementRef: ElementRef) {}get isFocused(): boolean {return document.activeElement === this.elementRef.nativeElement;}

Можно написать так:

constructor(@Inject(ElementRef) private readonly elementRef: ElementRef,@Inject(DOCUMENT) private readonly documentRef: Document,) {}get isFocused(): boolean {return this.documentRef.activeElement === this.elementRef.nativeElement;}

В первом примере мы обратились к глобальной переменной напрямую. Во втором внедрили ее как зависимость в конструктор.

Я не собираюсь бросаться в вас модными акронимами или утверждать, что первый пример нарушает тот или иной принцип программирования. По правде сказать, я в этой теории не сильно подкован.

Вместо этого я покажу, почему подход с токеном лучше. Начнем с того, что посмотрим, откуда берется этот токен. В его объявлении в @angular/common нет ничего особенного:

export const DOCUMENT = new InjectionToken<Document>('DocumentToken');

Внутри @angular/platform-browser, однако, мы видим, как он получает свое значение (пример упрощенный):

{provide: DOCUMENT, useValue: document}

Когда вы добавляете BrowserModule в app.browser.module.ts, вы регистрируете целую кучу имплементаций для токенов, таких как RendererFactory2, Sanitizer, EventManager и наш DOCUMENT. Почему так? Потому что Angular это кросс-платформенный фреймворк. Он оперирует абстракциями и активно использует механизм внедрения зависимостей для того, чтобы работать в браузере, на сервере и на мобильных устройствах. Чтобы разобраться, давайте заглянем в ServerModule ещё одну платформу, доступную из коробки (пример упрощенный):

{provide: DOCUMENT, useFactory: _document, deps: [Injector]},// ...function _document(injector: Injector) {const config = injector.get(INITIAL_CONFIG);const window = domino.createWindow(config.document, config.url);return window.document;}

Мы видим, что там используется domino для создания имитации документа на основе конфига, взятого из DI. Именно это вы получите, если запустите Angular Universal для рендеринга приложения на сервере. Мы уже видим первый и самый важный плюс данного подхода. Работа с DOCUMENT возможна в SSR-окружении, в то время как глобальный document в нем отсутствует.

Другие глобальные сущности

Что ж, команда Angular позаботилась о document для нас, это здорово. Но что, если мы хотим, например, проверить браузер через строку userAgent? Для этого мы обычно обращаемся к navigator.userAgent. На самом же деле это означает, что сначала мы запрашиваем глобальный объект, window в случае браузера, и потом берем его поле navigator. Так что давайте начнем с токена WINDOW. Его довольно просто реализовать благодаря фабрике, которую можно добавить к созданию токена:

export const WINDOW = new InjectionToken<Window>('An abstraction over global window object',{factory: () => inject(DOCUMENT).defaultView!},);

Этого достаточно, чтобы начать использовать WINDOW по аналогии с DOCUMENT. Теперь используем тот же подход для создания NAVIGATOR:

export const NAVIGATOR = new InjectionToken<Navigator>('An abstraction over window.navigator object',{factory: () => inject(WINDOW).navigator,},);

Мы шагнем дальше и сделаем отдельный токен под USER_AGENT тем же способом. Зачем? Увидим позже!

Иногда одного токена недостаточно. Location из Angular это, по сути, обертка над location для упрощения работы с ним. Поскольку мы привыкли к RxJS, давайте заменим requestAnimationFrame на реализацию в виде Observable:

export const ANIMATION_FRAME = new InjectionToken<Observable<DOMHighResTimeStamp>>('Shared Observable based on `window.requestAnimationFrame`',{factory: () => {const performanceRef = inject(PERFORMANCE);      return interval(0, animationFrameScheduler).pipe(map(() => performanceRef.now()),share(),);},},);

Мы пропустили создание PERFORMANCE, потому что оно следует той же модели. Теперь у нас есть один общий стрим, основанный на requestAnimationFrame, который можно использовать по всему приложению. После того как мы заменили всё на токены, наши компоненты больше не полагаются на волшебным образом доступные сущности и получают всё, от чего они зависят, из DI. Классно!

Server Side Rendering

Теперь, допустим, мы хотим написать window.matchMedia('(prefers-color-scheme: dark)').

Конечно, на сервере в нашем WINDOW что-то да есть, но оно, безусловно, не поддерживает весь API объекта Window. Если мы попробуем сделать данный вызов в SSR, скорее всего, получим ошибку undefined is not a function. Один способ решить проблему обернуть все подобные вызовы в проверку isPlatformBrowser, но это скукота. Преимущество DI в том, что значения можно переопределять. Так что вместо особой обработки таких случаев мы можем предоставить безопасный муляж WINDOW в app.server.module.ts, который защитит нас от несуществующих свойств.

Это демонстрирует еще одно достоинство данного подхода: значение токена можно менять. Благодаря этому тестировать код, зависящий от браузерного API, становится очень просто. Особенно если вы используете Jest, в котором нативный API по большей части отсутствует. Но муляж это просто заглушка. Иногда мы можем подложить что-то осмысленное. В SSR-окружении у нас есть объект запроса, который содержит данные о user agent. Для этого мы и вынесли его в отдельный токен иногда его можно заполучить отдельно. Вот как мы можем превратить запрос в провайдер:

function provideUserAgent(req: Request): ValueProvider {return {provide: USER_AGENT,useValue: req.headers['user-agent'],};}

А затем добавим его в server.ts, когда будем настраивать Angular Universal:

server.get('*', (req, res) => {res.render(indexHtml, {req,providers: [{provide: APP_BASE_HREF, useValue: req.baseUrl},provideUserAgent(req),],});});

Node.js так же имеет собственную имплементацию Performance, которую можно использовать на сервере:

{provide: PERFORMANCE,useFactory: performanceFactory,}// ...export function performanceFactory(): Performance {return require('perf_hooks').performance;}

Однако в случае requestAnimationFrame он нам не понадобится. Скорее всего, мы не хотим гонять наши Observable-цепочки впустую на сервере, так что просто подложим в DI EMPTY:

{provide: ANIMATION_FRAME,useValue: EMPTY,}

Следуя данной схеме, мы можем токенизировать все глобальные объекты в нашем приложении и предоставить замены для различных платформ, на которых мы его запускаем.

Подытожим

С таким подходом ваш код становится хорошо абстрагированным. Даже если вы не используете серверный рендеринг сейчас, возможно, он понадобится в будущем и вы будете к этому готовы. Кроме того, тестировать код гораздо легче, если все зависимости можно подменить. Мы создали крошечную библиотеку с токенами, которые используем сами:

Если вам требуется что-то, чего в ней еще нет, не стесняйтесь заводить issue на Гитхабе. Также у пакета есть напарник с версиями этих токенов под SSR:

Вы можете изучить этот проект по вселенной Rick and Morty от моего коллеги Игоря, чтобы увидеть это все в действии. Если вам в особенности интересен Angular Universal, прочитайте его статью о проблемах, с которыми он столкнулся, и как их решить.

Благодаря данному подходу наша библиотека Angular-компонентов Taiga UI без труда запустилась и под Angular Universal, и под Ionic. Надеюсь, что эти знания помогут и вам!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru