Кодирование / декодирование в Base58 без использования внешних компонент

14.11.19

Разработка - Защита ПО и шифрование

Функции кодирование / декодирование строки в Base58 без использования внешних компонент. Код алгоритмов доступен в полном описании статьи.

Скачать файлы

Наименование Файл Версия Размер
Исходный код расширения:
.zip 12,81Kb
1
.zip 0.2 12,81Kb 1 Скачать

В расширении представлены алгоритмы:

  • Кодирования / декодирования строки в base58.
  • Кодирование строк utf8 в массив байт и декодирование массива байт в строку uft8

Данные алгоритмы перенесены с JS. За основу взята библиотека ts-lib-crypto. Внешние компоненты в разработки не использовались. Для использования алгоритмов необходима платформа версии не ниже 8.3.11

Разработка распространяется под лицензией MIT.

Исходный код всех модулей представлен ниже:

 
 base58_encode
 
 base58_decode
 
 Crypt_ОбщегоНазначенияКлиентСевер
 
 СтрокаВUtf8МассивБайт
 
 МассивБайтВUtf8Строку

 

Разработка не предусматривает интерфейса и состоит только из общих модулей.

base58 encode decode

См. также

Запрет глобального поиска в конфигурации

Защита ПО и шифрование Платформа 1С v8.3 1С:Бухгалтерия 3.0 Абонемент ($m)

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

1 стартмани

09.02.2023    2225    9    aximo    4    

2

Как защитить pdf файл

Защита ПО и шифрование Абонемент ($m)

Для установки защиты pdf документа, полученного в 1С, написано консольное приложение на c#., использующее одну зависимость pdfSharp.dll. В результате работы приложения ограничены операции над документом и записаны метаданные. С помощью аргументов командной строки можно управлять работой приложения.

2 стартмани

30.01.2023    1662    1    olevlasam    3    

3

Универсальный синтаксический анализатор ASN.1 для декодирования .key, .cer, .der, .p7m, .p7s, .crt, .pem

Защита ПО и шифрование Платформа 1С v8.3 Конфигурации 1cv8 Абонемент ($m)

Универсальный синтаксический анализатор ASN.1, который может декодировать любую допустимую структуру ASN.1 DER или BER, независимо от того, закодирована ли она в кодировке Base64 (распознаются необработанные base64, защита PEM и begin-base64) или в шестнадцатеричном кодировании.

1 стартмани

04.12.2022    2989    12    keyn5565`    0    

13

Шифрование строки на основе мастер-пароля в 1С Предприятие 8.3.19

Защита ПО и шифрование Платформа 1С v8.3 Абонемент ($m)

Демонстрация возможностей шифрования строки на основе мастер-пароля в 1С Предприятие 8.3.19. AES без zip файла, RSA, PKDF2. (c использованием библиотеки С# через com).

2 стартмани

31.08.2022    3833    7    vit59    2    

6

Обфускатор байт-кода

Защита ПО и шифрование Платформа 1С v8.3 Конфигурации 1cv8 Россия Абонемент ($m)

Обработка, позволяющая запутывать и шифровать байт-код, поставлять модули без исходных текстов и т.д. Протестировано на платформе 8.3.23.1739.

10 стартмани

16.06.2022    10313    80    ZhokhovM    12    

40

Как уберечь конструкторскую документацию от воровства конкурентами?

Защита ПО и шифрование Платформа 1С v7.7 Платформа 1С v8.3 Абонемент ($m)

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

2 стартмани

09.03.2022    5657    3    ge_ni    9    

2

Защита конфигураций, обработок, расширений 1С онлайн, управление версиями

Защита ПО и шифрование Платформа 1С v8.3 Конфигурации 1cv8 Абонемент ($m)

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

1 стартмани

27.12.2021    4556    2    idm80    11    

9
Отзывы
23. mrsmrv 125 14.05.20 10:48 Сейчас в теме
Вот что получилось в итоге:

Функция КодироватьHex(пСтрокаHex, пАлфавит) Экспорт
	Возврат Кодировать(ПолучитьДвоичныеДанныеИзHexСтроки(пСтрокаHex), пАлфавит);
КонецФункции

Функция Кодировать(пДвДанные, пАлфавит) Экспорт
	лБольшоеЧисло = 0;
	лБуферДвДанных = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(пДвДанные);
	лЧислоБайт = лБуферДвДанных.Размер;
	лРазмерАлфавита = СтрДлина(пАлфавит); // Алфавит односимвольный
	
	// Получаем большое число
	лСтрока1 = "";

	Для Каждого лТекБайт Из лБуферДвДанных Цикл
		лБольшоеЧисло = лБольшоеЧисло*256 + лТекБайт;
		Если лБольшоеЧисло=0 И лТекБайт=0 Тогда
			лСтрока1 = лСтрока1 + Сред(пАлфавит,1,1); // пишем ноль вначале, если лидирующие байты - нулевые
		КонецЕсли;
	КонецЦикла;
	
	лСтрока2 = "";
	// Разделяем на символы
	лОстаток = лБольшоеЧисло%лРазмерАлфавита;
	лБольшоеЧисло = Цел(лБольшоеЧисло/лРазмерАлфавита);
	Пока лОстаток <> 0 или лБольшоеЧисло > 0 Цикл
		лСтрока2 = Сред(пАлфавит,лОстаток+1,1) + лСтрока2;
		лОстаток = лБольшоеЧисло%лРазмерАлфавита;
		лБольшоеЧисло = Цел(лБольшоеЧисло/лРазмерАлфавита);
	КонецЦикла;
	Возврат лСтрока1+лСтрока2;
КонецФункции

Функция РаскодироватьHex(пСтрока, пАлфавит) Экспорт
	Возврат ПолучитьHexСтрокуИзДвоичныхДанных(Раскодировать(пСтрока, пАлфавит));
конецФункции

Функция Раскодировать(пСтрока, пАлфавит) Экспорт
	лБольшоеЧисло = 0;
	лРазмерАлфавита = СтрДлина(пАлфавит); // Алфавит односимвольный
	лДлинаСтроки = СтрДлина(пСтрока);
	
	лНулевойСимвол = Сред(пАлфавит,1,1); // определяем количество лидирующих "нулей"
	лКолвоНулевых = 0;
	Пока Сред(пСтрока,лКолвоНулевых+1,1) = лНулевойСимвол Цикл
		лКолвоНулевых = лКолвоНулевых + 1;
	КонецЦикла;
	
	// Лидирующие нули добавляют значащие символы в любой системе счисления 1 к 1, а не как a^x = b
	лДлинаБуфера = Окр((лДлинаСтроки-лКолвоНулевых)/ЛогарифмПоОснованию(лРазмерАлфавита,256))+лКолвоНулевых+1; // Создаём буфер с учётом лидирующих "нулей" и небольшим запасом
	лБуферДвДанных = Новый БуферДвоичныхДанных(лДлинаБуфера);
	
	// Получаем большое число
	лСтрока = "";
	нн=0;
	Для н=1 по лДлинаСтроки Цикл
		лТекЧисло = СтрНайти(пАлфавит,Сред(пСтрока,н,1))-1;
		лБольшоеЧисло = лБольшоеЧисло*лРазмерАлфавита + лТекЧисло;
		Если лБольшоеЧисло=0 И лТекЧисло=0 Тогда
			нн=н;
			// лБуферДвДанных.Установить(нн,0); // пишем нулевые байты вначале, если лидирующие символы - нулевые
			// писать нулевые байты смысла нет, они и так там нулевые. просто запоминаем сколько нулевых вначале и всё.
		КонецЕсли;
	КонецЦикла;
	
	// Разделяем на байты
	н = 0;
	лОстаток = лБольшоеЧисло%256;
	лБольшоеЧисло = Цел(лБольшоеЧисло/256);
	Пока лОстаток <> 0 или лБольшоеЧисло > 1 Цикл
		лБуферДвДанных.Установить(лДлинаБуфера-н-1,лОстаток);
		лОстаток = лБольшоеЧисло%256;
		лБольшоеЧисло = Цел(лБольшоеЧисло/256);
		н = н + 1;
	КонецЦикла;
	
	лРеальнаяДлинаБуфера = нн+н;
	
	лДвДанные = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(лБуферДвДанных.Прочитать(лДлинаБуфера-лРеальнаяДлинаБуфера,лРеальнаяДлинаБуфера));
	
	Возврат лДвДанные;
КонецФункции

Функция ЛогарифмПоОснованию(Основание, Показатель)
	Возврат Log10(Показатель)/Log10(Основание);
КонецФункции
Показать
ArtemSerov; +1 Ответить
Остальные комментарии
В избранное Подписаться на ответы Сортировка: Древо развёрнутое
Свернуть все
1. МихаилМ 15.11.19 20:58 Сейчас в теме
в 1с8 вычислительные операции долгие. замените их подстановкой из массива(соответствия). и не по байту , а по 2-3
2. dmbarchenkov 22.04.20 00:07 Сейчас в теме
МихаилМ дело говорит, можете поправить или вам не интересно обработку дорабатывать?
3. mrsmrv 125 09.05.20 17:35 Сейчас в теме
Извините, но:
Функция base58_decode(Знач тСтр) Экспорт
	
	ALPHABET = ПолучитьСтруктуруALPHABET();
	
	Если Не ЗначениеЗаполнено(тСтр) Тогда 
		
		Возврат Новый Массив; 
		
	КонецЕсли;
Показать


не следует ли сначала проверить входное значение и потом уже получать нужные данные.
Кроме того есть такая функция ПустаяСтрока() - она вроде как побыстрее работает, потому что не проверяет на типы данных.

Далее:
Если ТипЗнч(Буфер) <> ТипЗнч(Новый Массив) Тогда 
		
		Возврат "";
		
	КонецЕсли;



не лучше ли: Если ТипЗнч(Буфер) <> Тип("Массив") Тогда
Не создавать объект массив, а просто сравнивать с типом.
ArtemSerov; +1 Ответить
4. mrsmrv 125 09.05.20 17:59 Сейчас в теме
В функции base58_decode последний цикл:
Пока (Сред(тСтр, сч, 1) = "1" и сч < Байты.Количество()) Цикл

Байты.Добавить(0);

КонецЦикла;

бесконечный.
Я обработку не скачивал, только текст публикации анализирую.
Извините.
13. mrsmrv 125 09.05.20 20:58 Сейчас в теме
(4) в цикл попадаем если входная строка например "1234" что в байтах 0, 13, 155 (0x000d9b)
5. mrsmrv 125 09.05.20 18:04 Сейчас в теме
В скачанных файлах текст модуля тот-же. Как вы тестировали?
6. mrsmrv 125 09.05.20 18:10 Сейчас в теме
В тексте статьи вы указали что "платформа версии не ниже 8.3.11", но начиная с 8.3.10. есть функция ПолучитьДвоичныеДанныеИзСтроки и обратная функция ПолучитьСтрокуИзДвоичныхДанных. Стоило ли городить функции МассивБайтВUtf8Строку, СтрокаВUtf8МассивБайт ?
7. mrsmrv 125 09.05.20 18:25 Сейчас в теме
Алфавит. Если это просто массив, а только он и используется в приведённом решении, строка не используется. Зачем тогда структура. Кроме того заполнения массива зачем-то сделано с разделителями, когда строка с алфавитом же, и так ясно что в строке каждый символ это элемент алфавита. т.е. достаточно просто пройтись по строке, забрать по одному символу с каждой позиции строки и внести её в массив:
точн
ее, проще будет так:
	Результат = Новый Структура;
	
	Результат.Вставить("Строка", 
	"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"­);
	Результат.Вставить("Массив",Новый Массив);
	Для н = 1 Пл СтрДлина(Результат.Строка) Цикл
		Результат.Массив.Добавить(Сред(Результат.Строка,н,1));
	КонецЦикла;
Показать

Ну конечно структуру можно и неиспользовать. Но это в других местах тоже потребует изменения обращения к массиву алфавита.

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

Кроме того, алфавит вообще-то судя по википедии может быть разным, исходя из этого неплохо было бы его принимать в качестве параметра.
8. mrsmrv 125 09.05.20 18:30 Сейчас в теме
В функции РазложитьМассивВСтроку
СтрокаРезультат = СтрокаРезультат + СокрЛП(Строка(ЭлементМассива)) + Разделитель;
достаточно
СтрокаРезультат = СтрокаРезультат + СокрЛП(ЭлементМассива) + Разделитель;
А учитывая, что там и так элементы без пробелов (ведь у нас там лишь элементы алфавита), то
СтрокаРезультат = СтрокаРезультат + ЭлементМассива + Разделитель;

Впрочем она и не используется
9. mrsmrv 125 09.05.20 20:00 Сейчас в теме
Вот эта конструкция:
carry = Числа[к] / 58;
carry = Число(Лев(carry, СтрНайти(carry, ",") - 1));

Может быть заменена на 

carry = Цел(Числа[к] / 58);

Кровь из глаз, ведь вы только что явно присвоили Число, дальше то Зачем это?:

				Если ЗначениеЗаполнено(carry) тогда
					
					carry = Число(carry);
					
				Иначе
					
					carry = 0;
					
				КонецЕсли;
Показать
10. mrsmrv 125 09.05.20 20:09 Сейчас в теме
А в чём тайный смысл ПобитовоеИли((carry / 58), 0), Зачем побитовое или с нулём? или это у вас так "Цел" работает?
проверил, может я уже туплю, ан нет:
Прикрепленные файлы:
11. mrsmrv 125 09.05.20 20:13 Сейчас в теме
Вот эта конструкция
Если carry % 58 = 0 Тогда 
				
				carry = ПобитовоеИли((carry / 58), 0);
				
			Иначе
				
				carry = carry / 58;
				carry = Число(Лев(carry, СтрНайти(carry, ",") - 1));
				
				Если ЗначениеЗаполнено(carry) тогда
					
					carry = Число(carry);
					
				Иначе
					
					carry = 0;
					
				КонецЕсли;
Показать

превращается в
carry = Цел(carry / 58);
12. mrsmrv 125 09.05.20 20:35 Сейчас в теме
Переписал я вашу функцию base58_encode, она и быстрее стала, судите сами:

Текст функции:

Функция base58_encode2(Знач Буфер, Знач пАлфавит = "") Экспорт
	
	Если ТипЗнч(Буфер) <> Тип("Массив") Тогда 
		Возврат "";
	КонецЕсли;
	
	Если Буфер.Количество() = 0 Тогда 
		Возврат "";
	КонецЕсли;
	
	Если пАлфавит = "" Тогда
		пАлфавит = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"­;
	КонецЕсли;
	
	лМассивАлфавита = Новый Массив;
	Для н = 1 По СтрДлина(пАлфавит) Цикл
		лМассивАлфавита.Добавить(Сред(пАлфавит,н,1));
	КонецЦикла;
	
	Числа = Новый Массив;
	Числа.Добавить(0);
	сч = 0;
	
	Пока сч < Буфер.Количество() Цикл 
		счч = 0;
		
		Пока счч < Числа.Количество() Цикл
			Числа[счч] = Числа[счч]*256;
			счч = счч + 1;
		КонецЦикла;
		
		Числа[0] = Числа[0] + Буфер[сч];
		carry = 0;
		к = 0;
		
		Пока к < Числа.Количество() Цикл 
			Числа[к] = Числа[к] + carry;
			carry = Цел(Числа[к]/58);
			Числа[к] = Числа[к] % 58;
			к = к + 1;
		КонецЦикла;
		
		Пока carry <> 0 Цикл 
			Числа.Добавить(carry % 58);
			carry = Цел(carry / 58);
		КонецЦикла;
		сч = сч + 1;
	КонецЦикла;
	сч = 0;
	Пока (Буфер[сч] = "0" И сч < Буфер.Количество() - 1) Цикл
		Числа.Добавить(0);        
		сч = сч + 1;
	КонецЦикла;
	
	Результат = "";
	
	Для Каждого Индекс из Числа Цикл 
		
		Результат = лМассивАлфавита[Индекс] + Результат;
		
	КонецЦикла;
	
	Возврат Результат;
	
КонецФункции
Показать
Прикрепленные файлы:
14. mrsmrv 125 09.05.20 21:10 Сейчас в теме
хм, а как должны кодироваться нулевые байты, если они вначале идут? У вас они игнорируются:
например: 0, 0, 0, 255, 0, 34, 0 (0х000000FF002200) кодируются сервисом: https://www.appdevtools.com/base58-encoder-decoder в строку 1117X3tT1, а у вас в строку 7X3tT1, если брать обратно строку, то у вас строка 1117X3tT1 декодируется в байты: 0, 255, 0, 34, 0 (0x00FF002200), а в выше указанном сервисе всё корректно обратно преобразуется в байты 0, 0, 0, 255, 0, 34, 0 (0х000000FF002200).
Как быть? некоторые последовательности байтов у вас туда-обратно не восстанавливаются!
15. mrsmrv 125 10.05.20 10:19 Сейчас в теме
Открыл исходник в JS, в функции Decode в конце вы строки:

for (let i = 0; string[i] === '1' && i < string.length - 1; i++) {
      bytes.push(0)
    }


"превратили" в строки:

    сч = 1;
    
    Пока (Сред(тСтр, сч, 1) = "1" и сч < Байты.Количество()) Цикл 
        
        Байты.Добавить(0);    
        
    КонецЦикла;
Показать


Хорошо, что я уже сам там поставил увеличение счётчика, проверил, работает и своей догадке нашёл подтверждение в "оригинале" JS.

т.е. свою разработку вы не тестировали?
17. ArtemSerov 22 11.05.20 07:02 Сейчас в теме
(15) Оригинал алгоритма: https://github.com/wavesplatform/ts-lib-crypto

Разработка выполнялась под задачи интеграции, требовалось повторить логику оригинального алгоритма, оптимизаций алгоритма не производилось

(12) Спасибо, как протестирую добавлю ваш код в публикацию
20. mrsmrv 125 11.05.20 22:11 Сейчас в теме
(17)Так у вашего проекта исходник из waves был скопирован чтоли?
16. mrsmrv 125 10.05.20 19:01 Сейчас в теме
Поставил минус, за то, что вы были на сайте и не удосужились ответить ни на один вопрос.
19. mrsmrv 125 11.05.20 22:10 Сейчас в теме
(16)О, ответ, хоть на что-то. Минус отменил.
21. ArtemSerov 22 12.05.20 06:20 Сейчас в теме
(19) Мои комментарии модерируются, не могу отвечать так оперативно
(20) Да, Проект Acryl это fork waves
18. пользователь 11.05.20 08:21
Сообщение было скрыто модератором.
...
22. mrsmrv 125 13.05.20 22:29 Сейчас в теме
Ну и теперь самая важная информация. Попробуйте в 1С набрать код: Сообщить(pow(2,1024*1024)); и выполнить его. То-то удивитесь.
23. mrsmrv 125 14.05.20 10:48 Сейчас в теме
Вот что получилось в итоге:

Функция КодироватьHex(пСтрокаHex, пАлфавит) Экспорт
	Возврат Кодировать(ПолучитьДвоичныеДанныеИзHexСтроки(пСтрокаHex), пАлфавит);
КонецФункции

Функция Кодировать(пДвДанные, пАлфавит) Экспорт
	лБольшоеЧисло = 0;
	лБуферДвДанных = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(пДвДанные);
	лЧислоБайт = лБуферДвДанных.Размер;
	лРазмерАлфавита = СтрДлина(пАлфавит); // Алфавит односимвольный
	
	// Получаем большое число
	лСтрока1 = "";

	Для Каждого лТекБайт Из лБуферДвДанных Цикл
		лБольшоеЧисло = лБольшоеЧисло*256 + лТекБайт;
		Если лБольшоеЧисло=0 И лТекБайт=0 Тогда
			лСтрока1 = лСтрока1 + Сред(пАлфавит,1,1); // пишем ноль вначале, если лидирующие байты - нулевые
		КонецЕсли;
	КонецЦикла;
	
	лСтрока2 = "";
	// Разделяем на символы
	лОстаток = лБольшоеЧисло%лРазмерАлфавита;
	лБольшоеЧисло = Цел(лБольшоеЧисло/лРазмерАлфавита);
	Пока лОстаток <> 0 или лБольшоеЧисло > 0 Цикл
		лСтрока2 = Сред(пАлфавит,лОстаток+1,1) + лСтрока2;
		лОстаток = лБольшоеЧисло%лРазмерАлфавита;
		лБольшоеЧисло = Цел(лБольшоеЧисло/лРазмерАлфавита);
	КонецЦикла;
	Возврат лСтрока1+лСтрока2;
КонецФункции

Функция РаскодироватьHex(пСтрока, пАлфавит) Экспорт
	Возврат ПолучитьHexСтрокуИзДвоичныхДанных(Раскодировать(пСтрока, пАлфавит));
конецФункции

Функция Раскодировать(пСтрока, пАлфавит) Экспорт
	лБольшоеЧисло = 0;
	лРазмерАлфавита = СтрДлина(пАлфавит); // Алфавит односимвольный
	лДлинаСтроки = СтрДлина(пСтрока);
	
	лНулевойСимвол = Сред(пАлфавит,1,1); // определяем количество лидирующих "нулей"
	лКолвоНулевых = 0;
	Пока Сред(пСтрока,лКолвоНулевых+1,1) = лНулевойСимвол Цикл
		лКолвоНулевых = лКолвоНулевых + 1;
	КонецЦикла;
	
	// Лидирующие нули добавляют значащие символы в любой системе счисления 1 к 1, а не как a^x = b
	лДлинаБуфера = Окр((лДлинаСтроки-лКолвоНулевых)/ЛогарифмПоОснованию(лРазмерАлфавита,256))+лКолвоНулевых+1; // Создаём буфер с учётом лидирующих "нулей" и небольшим запасом
	лБуферДвДанных = Новый БуферДвоичныхДанных(лДлинаБуфера);
	
	// Получаем большое число
	лСтрока = "";
	нн=0;
	Для н=1 по лДлинаСтроки Цикл
		лТекЧисло = СтрНайти(пАлфавит,Сред(пСтрока,н,1))-1;
		лБольшоеЧисло = лБольшоеЧисло*лРазмерАлфавита + лТекЧисло;
		Если лБольшоеЧисло=0 И лТекЧисло=0 Тогда
			нн=н;
			// лБуферДвДанных.Установить(нн,0); // пишем нулевые байты вначале, если лидирующие символы - нулевые
			// писать нулевые байты смысла нет, они и так там нулевые. просто запоминаем сколько нулевых вначале и всё.
		КонецЕсли;
	КонецЦикла;
	
	// Разделяем на байты
	н = 0;
	лОстаток = лБольшоеЧисло%256;
	лБольшоеЧисло = Цел(лБольшоеЧисло/256);
	Пока лОстаток <> 0 или лБольшоеЧисло > 1 Цикл
		лБуферДвДанных.Установить(лДлинаБуфера-н-1,лОстаток);
		лОстаток = лБольшоеЧисло%256;
		лБольшоеЧисло = Цел(лБольшоеЧисло/256);
		н = н + 1;
	КонецЦикла;
	
	лРеальнаяДлинаБуфера = нн+н;
	
	лДвДанные = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(лБуферДвДанных.Прочитать(лДлинаБуфера-лРеальнаяДлинаБуфера,лРеальнаяДлинаБуфера));
	
	Возврат лДвДанные;
КонецФункции

Функция ЛогарифмПоОснованию(Основание, Показатель)
	Возврат Log10(Показатель)/Log10(Основание);
КонецФункции
Показать
ArtemSerov; +1 Ответить
25. siamagic 18.01.23 12:29 Сейчас в теме
В любом кодере Когда делаешь в бейс64 - то по мере добавляения символов первые не меняются.
У бейс58 не так - почему они же одно и тоже
Оставьте свое сообщение