Простое и понятное симметричное шифрование и принципы асимметричного шифрования

Java Безопасность
Простое и понятное симметричное шифрование и принципы асимметричного шифрования

Учебная комната Cabbage Java охватывает основные знания

Иногда я слышал от своих коллег (программистов): base64 и md5 используются для шифрования. У них нет понятия о шифровании, они тоже думают, что сжатие и есть шифрование. Итак, сегодня я пришел сюда, чтобы объяснить, что такое шифрование, принцип и использование шифрования.

1. Концепции шифрования

Криптография — это техническая наука о составлении и расшифровке шифров. Основанный на математике, он развивается поочередно в процессе противостояния шифрования и расшифровки, атаки и защиты, копья и щита. С точки зрения математических алгоритмов он включает симметричные криптографические алгоритмы, асимметричные криптографические алгоритмы и алгоритмы хеширования.

Давайте взглянем на некоторые понятия, которые часто упоминаются в шифровании!

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

2. Симметричное шифрование

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

Процесс шифрования выглядит следующим образом: открытый текст + алгоритм шифрования + закрытый ключ => зашифрованный текст

Процесс расшифровки выглядит следующим образом: зашифрованный текст + алгоритм дешифрования + закрытый ключ => открытый текст

对称加密.png

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

Если вы не очень хорошо понимаете, просто посмотрите на этот простой для понимания пример:

А сказал Б: «У меня здесь есть замок. В будущем, когда мы будем отправлять новости друг другу, мы будем помещать новости в ящик, а затем использовать этот замок, чтобы заблокировать их перед отправкой. Этот замок имеет два одинаковых ключи, по одному на каждого из нас. А передал ключ Б.

3. Асимметричное шифрование

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

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

Открытый текст + алгоритм шифрования + открытый ключ => зашифрованный текст, зашифрованный текст + алгоритм дешифрования + закрытый ключ => открытый текст

非对称加密.png

Поскольку для шифрования и дешифрования используются два разных ключа, асимметричное шифрование является «асимметричным». Недостатком асимметричного шифрования является то, что шифрование и дешифрование занимают много времени, а скорость низкая, и оно подходит только для шифрования небольшого объема данных.

Если вы не очень хорошо понимаете, просто посмотрите на этот простой для понимания пример:

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

B сказал A, у меня здесь есть замок типа B, соответствующий ключу B, я дам вам большую коробку с замком B, но ключ B вам не дадут, после того, как вы отправите мне сообщение, заприте его в ящик с замком Б и отдайте мне, и тогда я смогу открыть его сам ключом Б.

4. Сравнение распространенных алгоритмов шифрования

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

Общие алгоритмы симметричного шифрования в основном включаютДЕС, 3ДЭС, АЕСи т. д., общие асимметричные алгоритмы в основном включаютРСА, ДСАи т. д., алгоритм хеширования в основном имеетША-1, МД5Ждать.

4.1 Сравнение алгоритмов хеширования

имя безопасность скорость
MD5 середина быстро
SHA-1 высоко медленный

4.2 Сравнение алгоритмов симметричного шифрования

имя имя ключа скорость бега безопасность НЧ
DES 56 бит Быстрее Низкий середина
3DES 112-битный или 168-битный медленный середина высоко
AES 128, 192, 256 бит быстро высоко Низкий

4.3 Сравнение алгоритмов асимметричного шифрования

имя зрелость скорость бега безопасность НЧ
RSA высоко середина высоко середина
ECC высоко медленный высоко высоко

Управление ключами при симметричном шифровании более сложное и не подходит для Интернета.Оно обычно используется во внутренних системах.Защищенность можно считать лишь умеренной, но скорость шифрования на несколько порядков выше (скорость программного шифрования и дешифрования выше по крайней мере в 100 раз быстрее, а число шифровальных и дешифровальных операций может быть зашифровано в секунду (бит данных), подходит для шифрования и дешифрования обработки больших объемов данных. Ключ асимметричного шифрования прост в управлении и обладает высокой безопасностью, но скорость шифрования относительно низкая, что подходит для шифрования и расшифровки небольших данных или подписи данных.

5. Используемые общие алгоритмы шифрования

5.1 Алгоритм MD5

MD5 использует хеш-функцию, и ее типичное применение — генерировать дайджест сообщения для части информации, чтобы предотвратить подделку. Строго говоря, MD5 — это не алгоритм шифрования, а алгоритм дайджеста. Независимо от длины ввода, MD5 выведет строку длиной 128 бит (обычно 32 символа в шестнадцатеричном формате).

Пример использования Java:

public static final byte[] computeMD5(byte[] content) {
    try {
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        return md5.digest(content);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

5.2 Алгоритм SHA1

SHA1 — это тот же популярный алгоритм дайджеста сообщений, что и MD5, однако SHA1 более безопасен, чем MD5. Для сообщений длиной менее 2^64 бит SHA1 создает 160-битный дайджест сообщения. Основываясь на характеристиках дайджеста информации MD5, SHA1 и необратимости (в целом), его можно использовать в таких сценариях, как проверка целостности файлов и цифровых подписей.

Пример использования Java:

public static byte[] computeSHA1(byte[] content) {
    try {
        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        return sha1.digest(content);
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

5.3 Алгоритм HMAC

MAC — это связанный с ключом код аутентификации сообщения операции хеширования (Hash-based Message Authentication Code).Операция HMAC использует хеш-алгоритм (MD5, SHA1 и т. д.), принимает ключ и сообщение в качестве входных данных и генерирует дайджест сообщения как выход . Отправитель и получатель HMAC имеют ключ для расчета, и третья сторона без этого ключа не может вычислить правильное значение хеш-функции, что может предотвратить подделку данных.

Пример использования Java:

package net.pocrd.util;
import net.pocrd.annotation.NotThreadSafe;
import net.pocrd.define.ConstField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;


@NotThreadSafe
public class HMacHelper {

    private static final Logger logger = LoggerFactory.getLogger(HMacHelper.class);
    private Mac mac;

    /**
     * MAC算法可选以下多种算法
     * HmacMD5/HmacSHA1/HmacSHA256/HmacSHA384/HmacSHA512
     */
    private static final String KEY_MAC = "HmacMD5";
    public HMacHelper(String key) {
        try {
            SecretKey secretKey = new SecretKeySpec(key.getBytes(ConstField.UTF8), KEY_MAC);
            mac = Mac.getInstance(secretKey.getAlgorithm());
            mac.init(secretKey);
        } catch (Exception e) {
            logger.error("create hmac helper failed.", e);
        }
    }
    public byte[] sign(byte[] content) {
        return mac.doFinal(content);
    }

    public boolean verify(byte[] signature, byte[] content) {
        try {
            byte[] result = mac.doFinal(content);
            return Arrays.equals(signature, result);
        } catch (Exception e) {
            logger.error("verify sig failed.", e);
        }
        return false;
    }
    
}

Уведомление: Экземпляры алгоритма HMAC небезопасны в многопоточной среде. Однако, когда требуется многопоточный доступ, требуется вспомогательный класс для синхронизации.Использование ThreadLocal для кэширования экземпляра для каждого потока позволяет избежать операций блокировки.

5.4 Алгоритм AES

ES, DES и 3DES — это алгоритмы симметричного блочного шифрования, а процесс шифрования и дешифрования является обратимым. Обычно используются AES128, AES192, AES256 (установленный по умолчанию JDK еще не поддерживает AES256, вам необходимо установить соответствующий патч jce для обновления jce1.7, jce1.8).

Алгоритм шифрования DES представляет собой блочный шифр, который шифрует данные 64-битными блоками, длина его ключа составляет 56 бит, и для шифрования и дешифрования используется один и тот же алгоритм. Алгоритм шифрования DES заключается в том, чтобы держать ключ в секрете, в то время как общедоступный алгоритм включает в себя алгоритмы шифрования и дешифрования. Таким образом, только люди, имеющие тот же ключ, что и отправитель, могут интерпретировать данные зашифрованного текста, зашифрованные с помощью алгоритма шифрования DES. Следовательно, расшифровка алгоритма шифрования DES фактически является кодировкой ключа поиска. Для 56-битного ключа количество операций составляет 2^56, если для поиска используется исчерпывающий метод. 3DES — это симметричный алгоритм, основанный на DES, который шифрует часть данных три раза тремя разными ключами с более высокой степенью надежности.

Алгоритм шифрования AES — это передовой стандарт шифрования в криптографии. Алгоритм шифрования использует симметричную блочную систему шифрования. Минимальная поддерживаемая длина ключа — 128 бит, 192 бит, 256 бит, а длина блока — 128 бит. Алгоритм должен быть простым. для различного аппаратного и программного обеспечения. Этот алгоритм шифрования является стандартом блочного шифрования, принятым федеральным правительством США. Сам AES предназначен для замены DES, AES обладает большей безопасностью, эффективностью и гибкостью.

Пример использования Java:

import net.pocrd.annotation.NotThreadSafe;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;

@NotThreadSafe
public class AesHelper {

    private SecretKeySpec keySpec;
    private IvParameterSpec iv;

    public AesHelper(byte[] aesKey, byte[] iv) {
        if (aesKey == null || aesKey.length < 16 || (iv != null && iv.length < 16)) {
            throw new RuntimeException("错误的初始密钥");
        }
        if (iv == null) {
            iv = Md5Util.compute(aesKey);
        }
        keySpec = new SecretKeySpec(aesKey, "AES");
        this.iv = new IvParameterSpec(iv);
    }

    public AesHelper(byte[] aesKey) {
        if (aesKey == null || aesKey.length < 16) {
            throw new RuntimeException("错误的初始密钥");
        }
        keySpec = new SecretKeySpec(aesKey, "AES");
        this.iv = new IvParameterSpec(Md5Util.compute(aesKey));
    }

    public byte[] encrypt(byte[] data) {
        byte[] result = null;
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance("AES/CFB/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);
            result = cipher.doFinal(data);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    public byte[] decrypt(byte[] secret) {
        byte[] result = null;
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance("AES/CFB/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);
            result = cipher.doFinal(secret);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    public static byte[] randomKey(int size) {
        byte[] result = null;
        try {
            KeyGenerator gen = KeyGenerator.getInstance("AES");
            gen.init(size, new SecureRandom());
            result = gen.generateKey().getEncoded();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }
    
}

5.5 Алгоритм RSA

Алгоритм шифрования RSA в настоящее время является наиболее влиятельным алгоритмом шифрования с открытым ключом и обычно считается одной из лучших схем с открытым ключом в настоящее время. RSA — это первый алгоритм, который можно использовать как для шифрования, так и для цифровой подписи, он устойчив ко всем известным до сих пор криптографическим атакам и рекомендован ISO в качестве стандарта для шифрования данных с открытым ключом.

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

Пример использования Java:

import net.pocrd.annotation.NotThreadSafe;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.Security;
import java.security.Signature;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

@NotThreadSafe
public class RsaHelper {

    private static final Logger logger = LoggerFactory.getLogger(RsaHelper.class);
    private RSAPublicKey publicKey;
    private RSAPrivateCrtKey privateKey;

    static {
        // 使用bouncycastle作为加密算法实现
        Security.addProvider(new BouncyCastleProvider()); 
    }

    public RsaHelper(String publicKey, String privateKey) {
        this(Base64Util.decode(publicKey), Base64Util.decode(privateKey));
    }

    public RsaHelper(byte[] publicKey, byte[] privateKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (RSAPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
            if (privateKey != null && privateKey.length > 0) {
                this.privateKey = (RSAPrivateCrtKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public RsaHelper(String publicKey) {
        this(Base64Util.decode(publicKey));
    }

    public RsaHelper(byte[] publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (RSAPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] encrypt(byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }

        if (content == null) {
            return null;
        }

        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            int size = publicKey.getModulus().bitLength() / 8 - 11;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((content.length + size - 1) / size * (size + 11));
            int left = 0;
            for (int i = 0; i < content.length; ) {
                left = content.length - i;
                if (left > size) {
                    cipher.update(content, i, size);
                    i += size;
                } else {
                    cipher.update(content, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] decrypt(byte[] secret) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }

        if (secret == null) {
            return null;
        }

        try {
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            int size = privateKey.getModulus().bitLength() / 8;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((secret.length + size - 12) / (size - 11) * size);
            int left = 0;
            for (int i = 0; i < secret.length; ) {
                left = secret.length - i;
                if (left > size) {
                    cipher.update(secret, i, size);
                    i += size;
                } else {
                    cipher.update(secret, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            logger.error("rsa decrypt failed.", e);
        }
        return null;
    }

    public byte[] sign(byte[] content) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }
        if (content == null) {
            return null;
        }
        try {
            Signature signature = Signature.getInstance("SHA1WithRSA");
            signature.initSign(privateKey);
            signature.update(content);
            return signature.sign();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public boolean verify(byte[] sign, byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }
        if (sign == null || content == null) {
            return false;
        }
        try {
            Signature signature = Signature.getInstance("SHA1WithRSA");
            signature.initVerify(publicKey);
            signature.update(content);
            return signature.verify(sign);
        } catch (Exception e) {
            logger.error("rsa verify failed.", e);
        }
        return false;
    }
}

5.6 Алгоритм ЕСС

ECC также является алгоритмом асимметричного шифрования, и его главное преимущество заключается в том, что в некоторых случаях он использует меньший ключ, чем другие методы, такие как алгоритм шифрования RSA, обеспечивающий сравнимый или более высокий уровень безопасности. Однако одним недостатком является то, что реализация операций шифрования и дешифрования занимает больше времени, чем другие механизмы (по сравнению с алгоритмом RSA, который интенсивно использует ЦП).

Пример использования Java:

import net.pocrd.annotation.NotThreadSafe;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.KeyFactory;
import java.security.Security;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

@NotThreadSafe
public class EccHelper {

    private static final Logger logger = LoggerFactory.getLogger(EccHelper.class);
    private static final int SIZE = 4096;
    private BCECPublicKey  publicKey;
    private BCECPrivateKey privateKey;

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    public EccHelper(String publicKey, String privateKey) {
        this(Base64Util.decode(publicKey), Base64Util.decode(privateKey));
    }

    public EccHelper(byte[] publicKey, byte[] privateKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
            if (privateKey != null && privateKey.length > 0) {
                this.privateKey = (BCECPrivateKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKey));
            }
        } catch (ClassCastException e) {
            throw new RuntimeException("", e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public EccHelper(String publicKey) {
        this(Base64Util.decode(publicKey));
    }

    public EccHelper(byte[] publicKey) {
        try {
            KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
            if (publicKey != null && publicKey.length > 0) {
                this.publicKey = (BCECPublicKey)keyFactory.generatePublic(new X509EncodedKeySpec(publicKey));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] encrypt(byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }
        try {
            Cipher cipher = Cipher.getInstance("ECIES", "BC");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            int size = SIZE;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((content.length + size - 1) / size * (size + 45));
            int left = 0;
            for (int i = 0; i < content.length; ) {
                left = content.length - i;
                if (left > size) {
                    cipher.update(content, i, size);
                    i += size;
                } else {
                    cipher.update(content, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public byte[] decrypt(byte[] secret) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }
        try {
            Cipher cipher = Cipher.getInstance("ECIES", "BC");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            int size = SIZE + 45;
            ByteArrayOutputStream baos = new ByteArrayOutputStream((secret.length + size + 44) / (size + 45) * size);
            int left = 0;
            for (int i = 0; i < secret.length; ) {
                left = secret.length - i;
                if (left > size) {
                    cipher.update(secret, i, size);
                    i += size;
                } else {
                    cipher.update(secret, i, left);
                    i += left;
                }
                baos.write(cipher.doFinal());
            }
            return baos.toByteArray();
        } catch (Exception e) {
            logger.error("ecc decrypt failed.", e);
        }
        return null;
    }

    public byte[] sign(byte[] content) {
        if (privateKey == null) {
            throw new RuntimeException("private key is null.");
        }
        try {
            Signature signature = Signature.getInstance("SHA1withECDSA", "BC");
            signature.initSign(privateKey);
            signature.update(content);
            return signature.sign();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public boolean verify(byte[] sign, byte[] content) {
        if (publicKey == null) {
            throw new RuntimeException("public key is null.");
        }
        try {
            Signature signature = Signature.getInstance("SHA1withECDSA", "BC");
            signature.initVerify(publicKey);
            signature.update(content);
            return signature.verify(sign);
        } catch (Exception e) {
            logger.error("ecc verify failed.", e);
        }
        return false;
    }
    
}