div.example { background-color: rgba(229, 236, 243, 1); color: rgba(0, 0, 0, 1); padding: 0.5em; margin: 1em 2em 1em 1em }
div.warning { border: 1px solid rgba(255, 0, 0, 1) }

本文是《大型分布式网站架构与实践》3.2节 常用安全算法的 学习笔记。

数字摘要

数字摘要也成为消息摘要(message digest),它是一个唯一对应一个消息或者文本的固定长度的值,它由一个单向的Hash函数计算产生。假设Hash函数为f(x),那么理想情况是任意的x1和x2,f(x1) ≠f(x2); 任意的x1,f(x1)的值总是相同的。如果两个不同的值产生了相同的消息摘要,我们称之为Hash碰撞。发生碰撞的概率越小Hash函数越好,尽管完全不发生碰撞是不可能的。消息摘要是不可逆的,我们无法从摘要中复原原有信息。消息摘要的使用场景是 我们得到了一个文本和它的消息摘要,我们可以使用相同的Hash函数计算出自己的摘要,如果两个摘要是相同的,我们就可以判定文本没有被修改过。

常用的消息摘要算法有MD5(Message Digest Algorithm 5) 和 SHA-1(Secure Hash Algorithm 1)。

我们还需要将摘要算法得到的二进制编码为字符串,常用的编码有16进制编码和base64编码

  值得注意的是编码算法并不像Hash函数那样是不可逆的,如果我们知道了编码算法本身的原理,很容易就可以将其进行逆向解码。

下面代码演示了使用java(JDK1.8)来进行MD5和SHA-1计算摘要,以及使用16进制编码和base64编码为字符串。

package test;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64; public class MeesageDigest {
/**
* 根据不同的算法计算消息摘要
* @param value 待计算的字符串
* @param algorithm 特定摘要算法
* @return
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static byte[] messageDigest(String value, String algorithm) throws NoSuchAlgorithmException, UnsupportedEncodingException{
MessageDigest md5 = MessageDigest.getInstance(algorithm);
return md5.digest(value.getBytes("UTF-8"));
} //16进制编码
public static String bytesToHexString(byte[] src){
StringBuilder stringBuilder = new StringBuilder("");
if (src == null || src.length <= 0) {
return null;
}
for (int i = 0; i < src.length; i++) {
int v = src[i] & 0xFF;
String hv = Integer.toHexString(v);
if (hv.length() < 2) {
stringBuilder.append(0);
}
stringBuilder.append(hv);
}
return stringBuilder.toString();
} //16进制解码
public static byte[] hexStringToBytes(String hexString) {
if (hexString == null || hexString.equals("")) {
return null;
}
hexString = hexString.toUpperCase();
int length = hexString.length() / 2;
char[] hexChars = hexString.toCharArray();
byte[] d = new byte[length];
for (int i = 0; i < length; i++) {
int pos = i * 2;
d[i] = (byte) ((byte) "0123456789ABCDEF".indexOf(hexChars[pos]) << 4 | (byte) "0123456789ABCDEF".indexOf(hexChars[pos + 1]));
}
return d;
} public static void main(String[] args) throws Exception {
byte[] md5_bytes = messageDigest("do what you love and keep going !", "MD5");
byte[] sha1_bytes = messageDigest("do what you love and keep going !", "SHA-1"); //16进制编码,我们利用网上的一些md5加密工具(32位小)得到的结果就是下面的这个md5_str。
String md5_str = bytesToHexString(md5_bytes);
System.out.println(md5_str); //base64编码
String base64_str = Base64.getEncoder().encodeToString(md5_bytes);
System.out.println(base64_str);
}
}

对称加密

对称加密(Symmetric Cryptography ):加密和解密必须使用同一个秘钥,对称加密加密效率高,但是由于通信双方都必须知道这个秘钥,所以对秘钥的保护就变得相当重要(在后面会讲到可以使用非对称加密来传递对称加密的秘钥)。

常用的对称加密算法有:DES 、 3DES、 AES。

下面java代码演示了如何使用AES算法(Advanced Encryption Standard)来加密和解密文本。

package test;

import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec; public class SymmetricCryptography {
//AES算法产生秘钥
public static SecretKey genKeyAES() throws Exception{
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128);//秘钥长度
SecretKey key = keyGen.generateKey();
return key;
} /**
* 我们这里定义了一个字符串类型的秘钥转换为java类型秘钥的方法
* 在真实的场景中,我们会将秘钥保存在一个文件中,当需要用它来加密解密时,从文件中读取出来,然后再转换为java的秘钥对象
*/
public static SecretKey loadKeyAES(String keyStr){
byte[] bytes = Base64.getDecoder().decode(keyStr);
SecretKey key = new SecretKeySpec(bytes, "AES");
return key;
} //下面两个方法分别使用AES算法加密和解密,他们都需要秘钥参与
public static byte[] encryptAES(byte[] source, String keyStr) throws Exception{
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, loadKeyAES(keyStr));
byte[] bytes = cipher.doFinal(source);
return bytes;
} public static byte[] decryptAES(byte[] source, String keyStr) throws Exception{
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, loadKeyAES(keyStr));
byte[] bytes = cipher.doFinal(source);
return bytes;
} public static void main(String[] args) throws Exception {
//加密和解密必须使用相同的key
SecretKey key = genKeyAES();
//产生字符串秘钥的key,可以保存在文件中以备使用
String keyStr = Base64.getEncoder().encodeToString(key.getEncoded());
//待加密的文本
String source = "do what you love and keep going !"; //将文本加密并按base64编码后输出
byte[] encrypt_bytes = encryptAES(source.getBytes("UTF-8"), keyStr);
System.out.println(Base64.getEncoder().encodeToString(encrypt_bytes)); //使用同样的key解码后输出
byte[] decrypt_bytes = decryptAES(encrypt_bytes, keyStr);
System.out.println(new String(decrypt_bytes, "UTF-8")); //使用不同的key解码然后输出
SecretKey another_key = genKeyAES();
String another_key_str = Base64.getEncoder().encodeToString(another_key.getEncoded());
//使用不同的秘钥将抛出javax.crypto.BadPaddingException,无法解密。
byte[] another_decrypt_bytes = decryptAES(encrypt_bytes, another_key_str);
System.out.println(new String(another_decrypt_bytes, "UTF-8"));
} }

非对称加密

非对称加密(asymmetric encryption)使用一对秘钥进行加密和解密:假设通信双方为Alice和Bob,Alice首先产生一对秘钥,并将其中一把公开出来,任何人都可以获得到,我们称之为 公钥(public key),而另一把则只有Alice自己知道,我们称之为 私钥(private key)。 非对称加密算法的特点是 对于使用私钥加密的密文可以使用公钥解密,而使用公钥加密的密文可以使用私钥解密。 现在Alice拥有了私钥,Bob拥有了公钥,他们之间便可以相互加密传输信息了。非对称加密只需要一对秘钥就够了。

Alice向Bob加密传输信息:

Bob向Alice加密传输信息:

非对称加密可以做到即使得到了公钥和加密算法源码也不能够解密密文,而实际上它们就是公开的。所以它不同于对称加密那样,一旦传输过程中将 加密秘钥 暴露,加密就不再安全了。 另一方面,非对称加密由于更加复杂,所以加密效率更低,不适于加密长文本。 在实际使用中,往往将对称加密和非对称加密混合使用:首先使用非对称加密来传输对称加密的秘钥,然后再使用对称加密传输真正的消息文本。这样就既保证了加密效率又不存在对称加密秘钥分发存在风险的问题了。最常用的非对称加密算法是RSA算法(其名称来自于三位作者的名字)

下面代码演示了RSA算法的java实现。

package test;

import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64; import javax.crypto.Cipher; public class AsymmetricEncryption {
//生成非对称加密的一对秘钥
public static KeyPair getKeyPair() throws Exception{
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
//初始化KeyPairGenerator的值决定了最大可以加密的字节数,例如512最大加密53字节,1024最大加密117字节....,而且随着加密字节的增多,速度会越来越慢
keyPairGenerator.initialize(512);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
return keyPair;
} /*
* 下面两个方法用于将字符串转换为秘钥对象
* 在真实的场景中,往往是将秘钥转换为字符串保存在文件中以备后面使用。
* 当使用的时候,从文件中读取字符串然后再转换为java的秘钥对象。
*/
//将String类型(base64)的公钥转换为PublicKey对象。
public static PublicKey string2PublicKey(String pubStr) throws Exception{
byte[] keyBytes = Base64.getDecoder().decode(pubStr);
//需要先将公钥转换为X509EncodedKeySpec
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
} //将String类型(base64)的公钥转换为PrivateKey对象。
public static PrivateKey string2PrivateKey(String priStr) throws Exception{
byte[] keyBytes = Base64.getDecoder().decode(priStr);
//需要先将私钥转换为PKCS8EncodedKeySpec
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
} //下面两个方法分别用于加密和解密,如果使用公钥加密则使用私钥解密,如果使用私钥加密则使用公钥解密
public static byte[] RSAEncrypt(byte[] content, Key key) throws Exception{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] bytes = cipher.doFinal(content);
return bytes;
} public static byte[] RSADecrypt(byte[] content, Key key) throws Exception{
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] bytes = cipher.doFinal(content);
return bytes;
} public static void main(String[] args) throws Exception {
//生成非对称加密的一对秘钥,按base64编码
KeyPair keyPair = getKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate(); String public_str = Base64.getEncoder().encodeToString(publicKey.getEncoded());
String private_str = Base64.getEncoder().encodeToString(privateKey.getEncoded()); //在真实的场景中,会将生成的字符串类型的公钥发送给通信的另一方,让其保存在文件中。 而字符串类型的私钥则自己保存在文件中。
System.out.println("public key: " + private_str);
System.out.println("private key: " + public_str); //真正通信的时候,再将秘钥的字符串表示转换为java秘钥对象
PublicKey publicKey2 = string2PublicKey(public_str);
PrivateKey privateKey2 = string2PrivateKey(private_str); String content = "do what you love and keep going !"; //在这里加密的内容不能大约53个字节
assert content.getBytes("UTF-8").length <= 53 : "Data must not be longer than 53 bytes "; //使用公钥加密,私钥解密
byte[] public_encrypt_bytes = RSAEncrypt(content.getBytes("UTF-8"), publicKey2);
byte[] private_decrypt_bytes = RSADecrypt(public_encrypt_bytes, privateKey2); System.out.println(new String(private_decrypt_bytes, "UTF-8")); //使用私钥加密,公钥解密
byte[] private_encrypt_bytes = RSAEncrypt(content.getBytes("UTF-8"), privateKey2);
byte[] public_decrypt_bytes = RSADecrypt(private_encrypt_bytes, publicKey2); System.out.println(new String(public_decrypt_bytes, "UTF-8"));
}
}

数字签名

数字签名是非对称加密技术消息摘要技术的综合运用。 消息发送方将消息正文通过摘要算法产生消息摘要,然后通过私钥加密,便得到了 数字签名。 数字签名需要和消息正文一同发送给接受者,接受者首先对接受到的消息正文采用相同的摘要算法产生消息摘要,然后使用发送者的公钥来将数字签名解密,得到加密前的消息摘要,通过这两个消息摘要进行对比,发送者就可以确定 消息是否被篡改过,以及消息是否来自于期待的发送者。签名的含义就是发送方使用只有自己知道的私钥来对消息摘要进行加密,这样别人就无法冒充发送方。

从上面的讨论中我们不难猜测出常用的数字签名算法其实就是 数字摘要算法和非对称加密算法的组合,比如 MD5withRSA 、 SHA1withRSA。

下面使用Java自身的签名API实现了MD5withRSA算法,其实我们通过组合之前对数字签名和非对称加密的讨论,自己实现起来也很容易。

package test;

import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature; public class SignatureWithJavaApi {
/**
* 发送发对消息文本签名
* @param content 消息文本
* @param privateKey 发送发私钥
* @return
* @throws Exception
*/
public static byte[] sign(String content, PrivateKey privateKey) throws Exception{
Signature signature = Signature.getInstance("MD5withRSA");
signature.initSign(privateKey);
signature.update(content.getBytes("UTF-8"));
return signature.sign();
} /**
* 接收方认证数字签名
* @param content 消息文本
* @param sign 发送发数字签名
* @param publicKey 发送发公钥
* @return
* @throws Exception
*/
public static boolean verify(String content, byte[] sign, PublicKey publicKey) throws Exception{
Signature signature = Signature.getInstance("MD5withRSA");
signature.initVerify(publicKey);
signature.update(content.getBytes("UTF-8"));
return signature.verify(sign);
} public static void main(String[] args) throws Exception {
String content = "do what you love and keep going !";
KeyPair pair = AsymmetricEncryption.getKeyPair(); byte[] sign = sign(content, pair.getPrivate());
boolean isVerify = verify(content, sign, pair.getPublic());
System.out.println(isVerify);
}
}

数字证书

数字证书是网络中用户身份的证明。例如我们访问https://www.baidu.com时就会从baidu接受到它的数字证书。在调试工具中 security -> View certificate 查看数字证书如下:

从这个数字证书的详情中我们可以看到证书的颁发机构(CA)、证书使用者、公钥等信息,如下图

客户端可以从以下几个方面对服务器证书进行验证:

1. 日期检测 就是证书中的有效期

2.颁发者可信度检测 在证书中我们可以看到颁发者(CA)一栏,颁发者多是一些权威机构,操作系统中预安装了很多权威CA的根证书。 当然我们可以自签发一些证书,为了能让客户端通过认证,我们需要手动导入自己的根证书。

3. 证书数字签名认证 在证书中除了我们所看到的信息外,它还携带了发证机构的数字签名,当客户端接收到这个数字证书后,就可以使用发证机构的公钥(来自CA根证书)来对这个签名进行认证,从而确定这个证书的确是由相应的CA签发的。

4.站点身份认证 简单的说就是检验服务器的域名与证书中使用者的域名是否匹配,这可以防止服务器复用其他站点的证书。

在企业内部,常常会搭建自己的https服务器,当我们访问它们时也会得到一个数字证书,但是这个证书往往默认是通不过浏览器认证的,如下图:

上面我们访问企业内部https服务器得到了一个数字证书,很明显这个证书仅仅通过了日期检测,其他三个都没有通过。 在使用中,我们会让浏览器忽略这些警告继续访问,这是因为在企业内部我们自己肯定是信任所访问的站点的,而这个证书中的公钥信息可以在后续通信中实现加密传输,这个往往才是我们真正需要的。

下面我们通过openssl来生成根证书和服务器端证书。

实验环境: centos 7

centos中默认已经安装了openssl,安装过程就不再赘述了。

1. 修改/etc/pki/tls/openssl.cnf,设定自己的工作目录

#dir = /etc/pki/CA # Where everything is kept
dir = /home/massclouds/CA #指定为自己的路径
certs = $dir/certs # Where the issued certs are kept
crl_dir = $dir/crl # Where the issued crl are kept
database = $dir/index.txt # database index file.

2.在上面设定的工作目录下面新建一些目录和文件

# mkdir certs newcerts private crl
# touch index.txt
# echo 01>serial

3.openssl生成随机数

openssl rand -out private/.rand 1000

根证书:

1.openssl生成CA私钥

openssl genrsa -aes256 -out private/cakey.pem 1024

2.生成根证书的签发申请CSR(certificate sign request)

 openssl req -new -key private/cakey.pem -out private/ca.csr -subj "/C=CN/ST=shandong/L=jinan/O=Shandong Massclouds Technology Co., Ltd/OU=ca department/CN=CA"

也可以不指定-subj参数,通过交互方式输入证书使用者的信息。

3.签发根证书

openssl x509 -req -days 365 -sha256 -extensions v3_ca -signkey private/cakey.pem -in private/ca.csr -out certs/ca.cer

服务器证书(由根证书签发)

1.生成服务器私钥

openssl genrsa -aes256 -out private/server-key.pem 1024

2.生成服务器CSR文件

openssl req -new -key private/server-key.pem -out private/server.csr -subj "/C=CN/ST=shandong/L=jinan/O=Shandong Massclouds Technology Co., Ltd/OU=server department/CN=www.server.com"

3.使用根证书签发服务端证书

openssl x509 -req -days 365 -sha256 -extensions v3_req -CA certs/ca.cer -CAkey private/cakey.pem -CAserial ca.srl -CAcreateserial -in private/server.csr -out certs/server.cer

客户端证书(由根证书签发)

1.生成客户端私钥

openssl genrsa -aes256 -out private/client-key.pem 1024

2.生成客户端CSR文件

openssl req -new -key private/client-key.pem -out private/client.csr -subj "/C=CN/ST=shandong/L=jinan/O=Shandong Massclouds Technology Co., Ltd/OU=lisi/CN=lisi"

3.使用根证书签发客户端证书

openssl x509 -req -days 365 -sha256 -CA certs/ca.cer -CAkey private/cakey.pem -CAserial ca.srl -in private/client.csr -out certs/client.cer

以上这些步骤在《大型分布式网站架构与实践》P175页有更加详细的介绍。

至此我们就得到了根证书和由这个根证书签发的服务端证书,我们现在可以将根证书导入到浏览器中看看是什么样子的。

导入后的结果:

至此我们就讨论完了https的所有准备知识,下一篇我们将进入https的讨论。

do what you love and keep going !

HTTPS学习(一):准备知识的更多相关文章

  1. 关于图计算&图学习的基础知识概览:前置知识点学习(Paddle Graph Learning (PGL))

    关于图计算&图学习的基础知识概览:前置知识点学习(Paddle Graph Learning (PGL)) 欢迎fork本项目原始链接:关于图计算&图学习的基础知识概览:前置知识点学习 ...

  2. HTTPS学习总结

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 21.0px Verdana; color: #393939 } span.s1 { } HTTPS学习总结 ...

  3. jQuery学习笔记 - 基础知识扫盲入门篇

    jQuery学习笔记 - 基础知识扫盲入门篇 2013-06-16 18:42 by 全新时代, 11 阅读, 0 评论, 收藏, 编辑 1.为什么要使用jQuery? 提供了强大的功能函数解决浏览器 ...

  4. Ant学习-001-ant 基础知识及windows环境配置

    一.Ant 概要基础知识 Apache Ant 是一个将软件编译.测试.部署等步骤联系在一起加以自动化的一个工具,大多用于Java环境中的软件开发,用以构建应用,或结合其他开源测试工具例如 git.T ...

  5. 学习javascript基础知识系列第二节 - this用法

    通过一段代码学习javascript基础知识系列 第二节 - this用法 this是面向对象语言中的一个重要概念,在JAVA,C#等大型语言中,this固定指向运行时的当前对象.但是在javascr ...

  6. 学习javascript基础知识系列第三节 - ()()用法

    总目录:通过一段代码学习javascript基础知识系列 注意: 为了便于执行和演示,建议使用chrome浏览器,按F12,然后按Esc(或手动选择)打开console,在console进行执行和演示 ...

  7. (1)写给Web初学者的教案-----学习Web的知识架构

    1:学习Web的知识架构 前文中我们简单的介绍了一些关于Web的基本知识,这里任老师再次强调一下凡是用浏览器打开的网站我们就称之为Web应用程序(B/S结构).除此之外其它需要下载安装的软件或是手机  ...

  8. 学习java接口知识

    学习java接口知识 //一个类最多只能有一个直接的父类.但是有多个间接的父类. java是单继承. class ye_01{ String name; } class fu_01 extends y ...

  9. 软件测试为什么需要学习Linux的知识?Linux学到什么程度?-log5

    ​软件测试为什么需要学习Linux的知识?学到什么程度?-log5 Dotest软件测试学堂-董浩 公司目前90%的服务器操作系统不是Windows,而是Linux(RedHat.Debian.Cen ...

随机推荐

  1. 要多用Java帮助文档

    从第一次接触Java到现在,大概两年了吧,间断断续续的学习.毕竟还在上课,其他课程也挺耗时间,但更多的还是自己不自律,很多时间都在玩. 平时用的有eclipse和IDEA,使用快捷方式有时看看源码,也 ...

  2. [LeetCode]547. Friend Circles朋友圈数量--不相邻子图问题

    /* 思路就是遍历所有人,对于每一个人,寻找他的好友,找到好友后再找这个好友的好友 ,这样深度优先遍历下去,设置一个flag记录是否已经遍历了这个人. 其实dfs真正有用的是flag这个变量,因为如果 ...

  3. Java源码系列4——HashMap扩容时究竟对链表和红黑树做了什么?

    我们知道 HashMap 的底层是由数组,链表,红黑树组成的,在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随 ...

  4. CF Grakn Forces 2020 1408E Avoid Rainbow Cycles(最小生成树)

    1408E Avoid Rainbow Cycles 概述 非常有趣的题目(指解法,不难,但很难想) 非常崇拜300iq,今天想做一套div1时看见了他出的这套题Grakn Forces 2020,就 ...

  5. 第13章节 BJROBOT 雷达跟随【ROS全开源阿克曼转向智能网联无人驾驶车】

    雷达跟随说明:注意深度摄像头的 USB 延长线,可能会对雷达扫描造成影响, 所以在雷达跟随前,把深度摄像头的 USB 延长线取下.另外雷达跟随范围大概是前方 50cm 和 120°内扫描到的物体都可以 ...

  6. OpenWRT19.07_命令行_重拨wan_重启路由

    OpenWRT19.07_命令行_重拨wan_重启路由 转载注明来源: 本文链接 来自osnosn的博客,写于 2020-10-19. 写OpenWRT的脚本时,需要用到一些重启命令 以下的命令中的参 ...

  7. Promise入门到精通(初级篇)-附代码详细讲解

    Promise入门到精通(初级篇)-附代码详细讲解 ​     Promise,中文翻译为承诺,约定,契约,从字面意思来看,这应该是类似某种协议,规定了什么事件发生的条件和触发方法. ​     Pr ...

  8. CentOS7 普通用户绕过root登录

      正常环境中我们的服务器都会使用一个普通用户跳转到root进行操作,如果root用户的密码不记得只知道普通用户密码,设备又不方便进行开关机破密码时,我们就可以用到以下方法登陆设备. pkexec : ...

  9. Docker-Docker部署SpringBoot项目

    1.手工方式 1.1.准备Springboot jar项目 将项目打包成jar 1.2.编写Dockerfile FROM java:8 VOLUME /tmp ADD elk-web-1.0-SNA ...

  10. [从源码学设计]蚂蚁金服SOFARegistry 之 ChangeNotifier

    [从源码学设计]蚂蚁金服SOFARegistry 之 ChangeNotifier 目录 [从源码学设计]蚂蚁金服SOFARegistry 之 ChangeNotifier 0x00 摘要 0x01 ...