首页 > 代码库 > Java安全加密

Java安全加密

凯撒密码

第一个案例,对字符进行简单的加密。

  • 对字符数组中所有的字符做+1处理(后面可以转换成+num这个数由通讯双方来协定)

代码

        /**
         *对字符简单的加密
         */
        public class SimpleEncryptionDemo01 {

            public static void main(String[] args) {
                // 确定加密的内容
                String content = "i love you tonight 404 see you z";
                //加密
                String encryptData = http://www.mamicode.com/encrypt(content);"hljs-string">"加密后:" + encryptData);
                //解密
                String decryptData = http://www.mamicode.com/decrypt(encryptData);"hljs-string">"解密后:" + decryptData);
            }

            /**
             * 加密方法
             * 
             * @param content
             * @return
             */
            public static String encrypt(String content) {
                // 将内容转换成字符数组
                char[] charArray = content.toCharArray();
                // 遍历出所有字符
                for (int i = 0; i < charArray.length; i++) {
                    // 对所有字符进行 + 1操作
                    charArray[i] = (char) (charArray[i] + 1);
                }
                return new String(charArray);
            }

            /**
             * 解密方法
             * 
             * @param encryptData
             * @return
             */
            public static String decrypt(String encryptData) {
                // 将内容转换成字符数组
                char[] charArray = encryptData.toCharArray();
                System.out.println(new String(charArray));
                // 解密
                for (int i = 0; i < charArray.length; i++) {
                    charArray[i] = (char) (charArray[i] - 1);
                }
                return new String(charArray);
            }
        }

第二个案例,对字节进行简单的加密。

字节和字符的区别?
* UTF-8编码下,一个英文或数字字符就是一个字节
* UTF-8编码下,一个汉字代表三个字节。
* 为什么new String(bytes)的时候回发生乱码,就是因为加密后得到的字节找不到对应的字符。
* Base64的原理
技术分享

代码

    /**
     *对字节进行加密
     */
    public class SimpleEncryptionDemo2 {

        public static void main(String[] args) {
            // 测试字节字符
            String content = "中文";
            int key = 80;
            String encryptData = http://www.mamicode.com/encrypt(content,key);"hljs-string">"+密后:" + encryptData);
            String decryptData = http://www.mamicode.com/decrypt(encryptData,key);"hljs-string">"解密后:" + decryptData);
        }

        /**
         * 加密
         * 
         * @param content
         * @return
         */
        public static String encrypt(String content,int key) {
            // 第一步,转换成字节数组
            byte[] bytes = content.getBytes();
            System.out.println("打印未加密的字节数组:");
            Util.printBytes(bytes);
            System.out.println("_________________________________________");
            // 第二步,遍历出所有字节,并做+1处理
            for (int i = 0; i < bytes.length; i++) {
                bytes[i] = (byte) (bytes[i] + key);
            }
            System.out.println("打印加密的字节数组:");
            Util.printBytes(bytes);
            System.out.println("_________________________________________");
    //      return new String(bytes);
            return Base64.getEncoder().encodeToString(bytes);
        }

        /**
         * 解密
         * @param encryptData
         * @return
         */
        public static String decrypt(String encryptData,int key) {
            // 第一步,转换成字节数组,加密时使用Base64编码,那解密时也要使用Base64解码
            byte[] bytes = Base64.getDecoder().decode(encryptData);
    //      byte[] bytes = encryptData.getBytes();
            System.out.println("打印经过两次转换后的字节数组:");
            Util.printBytes(bytes);
            System.out.println("_________________________________________");
            // 解密
            for (int i = 0; i < bytes.length; i++) {
                bytes[i] = (byte) (bytes[i] - key);
            }
            System.out.println("打印解密后的字节数组:");
            Util.printBytes(bytes);
            System.out.println("_________________________________________");
            return new String(bytes);
        }
    }

对称加密与非对称加密

  • 对称加密与非对称加密的区别
    • 对称加密。同一把钥匙进行加密和解密
    • 非对称加密,公钥加密,私钥解密。

对称加密的案例

常见算法:AES、DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK
AES:高级加密标准(Advanced Encryption Standard)
DES:数据加密标准(Data Encryption Standard)

对称加密第一个案例

    public class SymmetricalEncryptionDemo01 {

        public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
                IllegalBlockSizeException, BadPaddingException {
            // 声明要加密的内容
            String content = "今晚404,不见不散";
            //生成key
            SecretKey key = createKey();
            //加密
            String encryptData = http://www.mamicode.com/encrypt(content, key);"hljs-comment">//解密
            String decryptData = http://www.mamicode.com/decrypt(encryptData, key);"hljs-javadoc">/**
         * 生成key
         * @return
         * @throws NoSuchAlgorithmException
         */
        public static SecretKey createKey() throws NoSuchAlgorithmException {
            // 生成key
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            SecretKey secretKey = keyGenerator.generateKey();
            return secretKey;
        }

        /**
         * 加密方法
         * 
         * @param content
         * @return
         * @throws InvalidKeyException
         * @throws NoSuchPaddingException
         * @throws NoSuchAlgorithmException
         * @throws BadPaddingException
         * @throws IllegalBlockSizeException
         */
        public static String encrypt(String content, SecretKey key) throws InvalidKeyException, NoSuchAlgorithmException,
                NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
            // 获取cipher单例对象
            Cipher cipher = Cipher.getInstance("AES");
            // 初始化cipher,设置为加密模式
            cipher.init(Cipher.ENCRYPT_MODE, key);
            // 开始加密
            byte[] encryptBytes = cipher.doFinal(content.getBytes());
            return Base64.getEncoder().encodeToString(encryptBytes);
        }

        /**
         * @param encryptData
         * @return
         */
        public static String decrypt(String encryptData, SecretKey key) throws InvalidKeyException,
                NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
            byte[] encryptBytes = Base64.getDecoder().decode(encryptData);
            // 获取cipher单例对象
            Cipher cipher = Cipher.getInstance("AES");
            // 解密
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decryptBytes = cipher.doFinal(encryptBytes);
            return new String(decryptBytes);
        }
    }

对称加密的第二个案例

将生成的key保存到本地,然后解密时和下一次加密时就只需要到本地读取即可。。

    /**
     *保存key到本地
     */
    public class SymmetricalEncryptionDemo02 {

        public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
                IllegalBlockSizeException, BadPaddingException, IOException, ClassNotFoundException {
            // 声明要加密的内容
            String content = "今晚404,不见不散";
            //第一次生成key,并序列化到本地
    //      SecretKey key = createKey();
    //      SerializableUtil.saveObject2File("heima.key", key);
            //从文件中读取key
            SecretKey key = (SecretKey) SerializableUtil.readObjectFromFile("heima.key");
            System.out.println(key);
            //加密
            String encryptData = http://www.mamicode.com/encrypt(content, key);"hljs-comment">//解密
            String decryptData = http://www.mamicode.com/decrypt(encryptData, key);"hljs-javadoc">/**
         * 生成key
         * @return
         * @throws NoSuchAlgorithmException
         */
        public static SecretKey createKey() throws NoSuchAlgorithmException {
            // 生成key
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            SecretKey secretKey = keyGenerator.generateKey();
            return secretKey;
        }

        /**
         * 加密方法
         * 
         * @param content
         * @return
         * @throws InvalidKeyException
         * @throws NoSuchPaddingException
         * @throws NoSuchAlgorithmException
         * @throws BadPaddingException
         * @throws IllegalBlockSizeException
         */
        public static String encrypt(String content, SecretKey key) throws InvalidKeyException, NoSuchAlgorithmException,
                NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
            // 获取cipher单例对象
            Cipher cipher = Cipher.getInstance("AES");
            // 初始化cipher,设置为加密模式
            cipher.init(Cipher.ENCRYPT_MODE, key);
            // 开始加密
            byte[] encryptBytes = cipher.doFinal(content.getBytes());
            return Base64.getEncoder().encodeToString(encryptBytes);
        }

        /**
         * @param encryptData
         * @return
         */
        public static String decrypt(String encryptData, SecretKey key) throws InvalidKeyException,
                NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
            byte[] encryptBytes = Base64.getDecoder().decode(encryptData);
            // 获取cipher单例对象
            Cipher cipher = Cipher.getInstance("AES");
            // 解密
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] decryptBytes = cipher.doFinal(encryptBytes);
            return new String(decryptBytes);
        }
    }

第三个案例——自定义秘钥,代码

        /**
         * 生成key
         * @return
         * @throws NoSuchAlgorithmException
         */
        public static SecretKey createKey(String keyword) throws NoSuchAlgorithmException {
            // 生成key
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(new SecureRandom(keyword.getBytes()));
            SecretKey secretKey = keyGenerator.generateKey();
            return secretKey;
        }

优点

  • 使用简单,方便
  • 效率高,速度快

缺点

  • 密钥传输过程中容易被截获

应用场景

  • 登录,post请求{username=lisi,pw=加密}

非对称加密的案例

常见算法:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)等
应用场景:银行和电商网站,他就采用非对称加密,将公钥给所有人,你们就用这个公钥加密,私钥我自己留着,谁也不知道,所以除了我,谁也解密不了。

第一个案例

(数据量不大的情况下可用,加密数据小于117个字节,解密数据小于128个字节)

    public class AysmmetricEncryptionDemo {

        public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
                IllegalBlockSizeException, BadPaddingException {
            String content = "今晚小树林,等你哦";
            //生成密钥对
            KeyPair keyPair = createKeyypair();
            //获取公钥私钥
            PublicKey publicKey = keyPair.getPublic();
            PrivateKey privateKey = keyPair.getPrivate();
            //加密
            String encryptData = http://www.mamicode.com/encrypt(content, publicKey);"hljs-string">"加密后:" + encryptData);
            //解密
            String decryptData = http://www.mamicode.com/decrypt(encryptData, privateKey);"hljs-string">"解密后:" + decryptData);
        }
        /**
         * 生成秘钥对
         * @return
         * @throws NoSuchAlgorithmException
         */
        public static KeyPair createKeyypair() throws NoSuchAlgorithmException{
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            KeyPair keyPair = keyPairGenerator.generateKeyPair();
            return keyPair;
        }
        /**
         * 加密
         * 
         * @param content
         * @return
         * @throws NoSuchPaddingException
         * @throws NoSuchAlgorithmException
         * @throws BadPaddingException
         * @throws IllegalBlockSizeException
         * @throws InvalidKeyException
         */
        public static String encrypt(String content, PublicKey publicKey) throws NoSuchAlgorithmException,
                NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {
            // 获取cipher对象
            Cipher cipher = Cipher.getInstance("RSA");
            // 初始化cipher,指定模式为加密,传入公钥
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] encryptBytes = cipher.doFinal(content.getBytes());
            return Base64.getEncoder().encodeToString(encryptBytes);
        }

        /**
         * 解密方法
         * 
         * @param encryptData
         * @param privateKey
         * @return
         * @throws NoSuchPaddingException 
         * @throws NoSuchAlgorithmException 
         * @throws InvalidKeyException 
         * @throws BadPaddingException 
         * @throws IllegalBlockSizeException 
         * @throws Exception
         */
        public static String decrypt(String encryptData, PrivateKey privateKey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException{
            byte[] encryptBytes = Base64.getDecoder().decode(encryptData);
            // 获取cipher对象
            Cipher cipher = Cipher.getInstance("RSA");
            // 小芝解密
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] decryptBytes = cipher.doFinal(encryptBytes);
            return new String(decryptBytes);
        }
    }

如果数据量大的话,则要对要加密、解密的数据进行分块处理

            /**
             * 处理大量数据时,分块加密/解密
             * @param content
             * @param cipher
             * @param max
             * @return
             * @throws IllegalBlockSizeException
             * @throws BadPaddingException
             * @throws IOException
             */
            private static byte[] doFinalWithBlock(byte[] bytes, Cipher cipher,int max)
                    throws IllegalBlockSizeException, BadPaddingException, IOException {
                int len = bytes.length;//3000
                //加密数据长度不能超过117个byte
                int inputOffset = 0;//2700
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                while (len > inputOffset) {
                    //特殊情况,最后一次剩下的数据不足117
                    if ((len - inputOffset) >= max) {
                        byte[] encryptBytes = cipher.doFinal(bytes, inputOffset, max);
                        baos.write(encryptBytes);
                        inputOffset += max;
                    }else {
                        byte[] encryptBytes = cipher.doFinal(bytes, inputOffset, len - inputOffset);
                        baos.write(encryptBytes);
                        inputOffset = len;
                    }
                }
                byte[] byteArray = baos.toByteArray();
                return byteArray;
            }

消息摘要

消息摘要是一个不可逆的过程。常用来防篡改。

常见算法:MD5、SHA、CRC等

byte数组和16进制字符串的互转

        /**
         * byte数组转16进制字符串
         * @param bytes
         * @return
         */
        public static String bytes2Hex(byte[] bytes){
            StringBuffer sBuffer = new StringBuffer();
            for (int i = 0; i < bytes.length; i++) {
                //取高位
                int high = (bytes[i] & 0xf0) >> 4;
                //取低位
                int low = bytes[i] & 0x0f;
                sBuffer.append(HEXSTR[high]).append(HEXSTR[low]);
            }
            return sBuffer.toString();
        }

    /**
         * 16进制字符串转字节数组
         * @param hex
         * @return
         */
        public static byte[] hex2Bytes(String hex){
            int len = hex.length()/2;
            //声明一个字节数组用于接收转换后的字节
            byte[] bytes = new byte[len];
            for (int i = 0; i < len; i++) {
                //首先取高位,取偶数位
                String highStr = hex.substring(2 * i, 2 * i + 1);
                String lowStr = hex.substring(2 * i + 1, 2 * i + 2);
                int high = Integer.parseInt(highStr, 16) << 4;
                int low = Integer.parseInt(lowStr, 16);
                bytes[i] = (byte) (high + low);
            }
            return bytes;
        }

数字签名

  • 首先防止偷看消息,消息内容是要公钥加密——为了防止别人截获消息之后,阅读消息。
  • 防止别人改消息,1.对消息进行MD5加密——MD5值,2.对MD5用张三的私钥进行加密!3.将加密后的MD5值和消息一同发给服务器。
  • 服务器用张三的公钥解密MD5值,然后将消息用MD5加密——得到MD5值,对比两个MD5值,如果相等,消息没被篡改。

Signature

public class DigitalSignatureDemo {

    public static void main(String[] args) throws Exception {
        String content = "今晚六点半,不见不散!!!";
        byte[] bytes = content.getBytes();
        //获取数字签名的单例对象
        Signature signature = Signature.getInstance("MD5withRSA");
        //从文件中读取私钥
        PrivateKey privateKey = (PrivateKey) SerializableUtil.readObjectFromFile("heima.privateKey");
        //私钥跟公钥不是一对,那么认证失败!
    //      KeyPairGenerator pairGenerator = KeyPairGenerator.getInstance("RSA");
    /       KeyPair keyPair = pairGenerator.generateKeyPair();
    //      PrivateKey privateKey = keyPair.getPrivate();
        //初始化签名
        signature.initSign(privateKey);
        //加载内容
        signature.update(bytes);
        //进行签名
        byte[] signBytes = signature.sign();//得到签名后的byte数组,就相当于私钥加密后的MD5值

        //获取公钥
        PublicKey publicKey = (PublicKey) SerializableUtil.readObjectFromFile("heima.publicKey");
        //认证流程
        //初始化认证
        signature.initVerify(publicKey);
    //      String content2 = "今晚八点,606嘿嘿嘿!";
        //加载内容
        signature.update(bytes);
        //认证
        boolean verify = signature.verify(signBytes);//认证加密后的MD5值
        System.out.println(verify);
    }
}

keytool的使用。

  • 生成keyPair keytool -genkeypair
  • 修改别名 keytool -changealias -alias mykey -destalias heima1
  • 导出证书 keytool -exportcert
  • 导入证书 keytool -importcert

获取文件中的证书

            // 获取证书工厂的实例
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                FileInputStream inStream = new FileInputStream(new File("heima.cer"));
                X509Certificate certificate = (X509Certificate) cf.generateCertificate(inStream);

SSl

单向/双向认证的原理

为了便于更好的认识和理解 SSL 协议,这里着重介绍 SSL 协议的握手协议。SSL 协议既用到了公钥加密技术又用到了对称加密技术,对称加密技术虽然比公钥加密技术的速度快,可是公钥加密技术提供了更好的身份认证技术。SSL 的握手协议非常有效的让客户和服务器之间完成相互之间的身份认证,其主要过程如下:

  • ① 客户端的浏览器向服务器传送客户端 SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。
  • ② 服务器向客户端传送 SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。
  • ③ 客户利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书的 CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过, 通讯将断开;如果合法性验证通过,将继续进行第四步。
  • ④ 用户端随机产生一个用于后面通讯的“对称密码”,然后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中获得)对其加密,然后将加密后的“预主密码”传给服务器。
  • ⑤ 如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的“预主密码”一起传给服务器。(数字签名)
  • ⑥ 如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的 CA 是否可靠,发行 CA 的公钥能否正确解开客户证书的发行 CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的“预主密 码”,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。
  • ⑦ 服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于 SSL 协议的安全数据通讯的加解密通讯。同时在 SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。
  • ⑧ 客户端向服务器端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
  • ⑨ 服务器向客户端发出信息,指明后面的数据通讯将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
  • ⑩ SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。

双向认证 SSL 协议的具体过程

  ① 浏览器发送一个连接请求给安全服务器。
  ② 服务器将自己的证书,以及同证书相关的信息发送给客户浏览器。
  ③ 客户浏览器检查服务器送过来的证书是否是由自己信赖的 CA 中心所签发的。如果是,就继续执行协议;如果不是,客户浏览器就给客户一个警告消息:警告客户这个证书不是可以信赖的,询问客户是否需要继续。
  ④ 接着客户浏览器比较证书里的消息,例如域名和公钥,与服务器刚刚发送的相关消息是否一致,如果是一致的,客户浏览器认可这个服务器的合法身份。
  ⑤ 服务器要求客户发送客户自己的证书。收到后,服务器验证客户的证书,如果没有通过验证,拒绝连接;如果通过验证,服务器获得用户的公钥。
  ⑥ 客户浏览器告诉服务器自己所能够支持的通讯对称密码方案。
  ⑦ 服务器从客户发送过来的密码方案中,选择一种加密程度最高的密码方案,用客户的公钥加过密后通知浏览器。
  ⑧ 浏览器针对这个密码方案,选择一个通话密钥,接着用服务器的公钥加过密后发送给服务器。
  ⑨ 服务器接收到浏览器送过来的消息,用自己的私钥解密,获得通话密钥。
  ⑩ 服务器、浏览器接下来的通讯都是用对称密码方案,对称密钥是加过密的。

代码访问

public class HttpsDemo {

            public static void main(String[] args) throws IOException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException {
                //第一种方式,我所有的证书都不检查
                SSLContext sslContext = SSLContext.getInstance("TLS");//TLS 是transport Layer safe
                //获取信任管理者工厂
                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                //获取keyStore
                KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
                ks.load(null);
                //从文件中读取证书
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                FileInputStream inStream = new FileInputStream(new File("srca.cer"));
                X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);
                //给keyStore设置证书
                ks.setCertificateEntry("12306", cert);
                trustManagerFactory.init(ks);
                TrustManager[] tm = trustManagerFactory.getTrustManagers();
                sslContext.init(null, tm, null);
                SSLSocketFactory socketFactory = sslContext.getSocketFactory();
                HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory);
                // TODO Auto-generated method stub
                URL url = new URL("https://kyfw.12306.cn/otn/");
                //第二步,获取coon
                HttpsURLConnection coon = (HttpsURLConnection) url.openConnection();
                InputStream inputStream = coon.getInputStream();
                String response = Util.inputStream2String(inputStream);
                System.out.println(response);
            }
        }

Tomcat配置https

1. 在server节点下配置如下的内容

    <Connector  port="8443" protocol="HTTP/1.1" SSLEnabled="true"
          maxThreads="150" scheme="https" secure="true"
          clientAuth="false" sslProtocol="TLS"
          keystoreFile="证书的全路径名称" keystorePass="证书的密码"/>

2. 创建证书

keytool -genkeypair -alias "itheima" -keyalg "RSA" -keystore "haha.keystore"    

3. 找到server .xml的这一句话

 <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" /> 把他注释掉。

4. 启动tomcat,加密的https的服务器就配置好了

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Java安全加密