[原]VC++网络安全编程范例(6)-OPENSSL创建文件保险箱
SSL是Secure Socket Layer(安全套接层协议)的缩写,可以在Internet上提供秘密性传输。Netscape公司在推出第一个Web浏览器的同时,提出了SSL协议标准,目前已有3.0版本。SSL采用公开密钥技术。
其目标是保证两个应用间通信的保密性和可靠性,可在服务器端和用户端同时实现支持。目前,利用公开密钥技术的SSL协议,已成为Internet上保密通讯的工业标准。安全套接层协议能使用户/服务器应用之间的通信不被攻击者窃听,并且始终对服务器进行认证,还可选择对用户进行认证。
SSL协议要求建立在可靠的传输层协议(TCP)之上。SSL协议的优势在于它是与应用层协议独立无关的,高层的应用层协议(例如:HTTP,FTP,TELNET等)能透明地建立于SSL协议之上。SSL协议在应用层协议通信之前就已经完成加密算法、通信密钥的协商及服务器认证工作。在此之后应用层协议所传送的数据都会被加密,从而保证通信的私密性。
通过以上叙述,SSL协议提供的安全信道有以下三个特性:
1、数据的保密性 信息加密就是把明码的输入文件用加密算法转换成加密的文件以实现数据的保密。加密的过程需要用到密钥来加密数据然后再解密。没有了密钥,就无法解开加密的数据。数据加密之后,只有密钥要用一个安全的方法传送。加密过的数据可以公开地传送。
2、数据的一致性 加密也能保证数据的一致性。例如:消息验证码(MAC),能够校验用户提供的加密信息,接收者可以用MAC来校验加密数据,保证数据在传输过程中没有被篡改过。
3、安全验证 加密的另外一个用途是用来作为个人的标识,用户的密钥可以作为他的安全验证的标识。SSL是利用公开密钥的加密技术(RSA)来作为用户端与服务器端在传送机密资料时的加密通讯协定。。
首先,应该感谢Eric A. Young和Tim J. Hudson,他们自1995年开始编写后来具有巨大影响的OpenSSL软件包,更令我们高兴的是,这是一个没有太多限制的开放源代码的软件包,这使得我们可以利用这个软件包做很多事情。Eric A. Young 和Tim J. Hudson是加拿大人,后来由于写OpenSSL功成名就之后就到大公司里赚大钱去了。1998年,OpenSSL项目组接管了OpenSSL的开发工作,并推出了OpenSSL的0.9.1版,到目前为止,OpenSSL的算法已经非常完善,对SSL2.0、SSL3.0以及TLS1.0都支持。 OpenSSL采用C语言作为开发语言,这使得OpenSSL具有优秀的跨平台性能,这对于广大技术人员来说是一件非常美妙的事情,可以在不同的平台使用同样熟悉的东西。OpenSSL支持Linux、Windows、BSD、Mac、VMS等平台,这使得OpenSSL具有广泛的适用性。不过,对于目前新成长起来的C++程序员,可能对于C语言的代码不是很习惯,但习惯C语言总比使用C++重新写一个跟OpenSSL相同功能的软件包轻松不少。OpenSSL的应用程序已经成为了OpenSSL重要的一个组成部分,其重要性恐怕是OpenSSL的开发者开始没有想到的。现在OpenSSL的应用中,很多都是基于OpenSSL的应用程序而不是其API的,如OpenCA,就是完全使用OpenSSL的应用程序实现的。OpenSSL的应用程序是基于OpenSSL的密码算法库和SSL协议库写成的,所以也是一些非常好的OpenSSL的API使用范例,读懂所有这些范例,你对OpenSSL的API使用了解就比较全面了,当然,这也是一项锻炼你的意志力的工作。
OpenSSL的应用程序提供了相对全面的功能,在相当多的人看来,OpenSSL已经为自己做好了一切,不需要再做更多的开发工作了,所以,他们也把这些应用程序成为OpenSSL的指令。OpenSSL的应用程序主要包括密钥生成、证书管理、格式转换、数据加密和签名、SSL测试以及其它辅助配置功能。
Engine机制 Engine机制的出现是在OpenSSL的0.9.6版的事情,开始的时候是将普通版本跟支持Engine的版本分开的,到了OpenSSL的0.9.7版,Engine机制集成到了OpenSSL的内核中,成为了OpenSSL不可缺少的一部分。 Engine机制目的是为了使OpenSSL能够透明地使用第三方提供的软件加密库或者硬件加密设备进行加密。OpenSSL的Engine机制成功地达到了这个目的,这使得OpenSSL已经不仅仅使一个加密库,而是提供了一个通用地加密接口,能够与绝大部分加密库或者加密设备协调工作。当然,要使特定加密库或加密设备更OpenSSL协调工作,需要写少量的接口代码,但是这样的工作量并不大,虽然还是需要一点密码学的知识。Engine机制的功能跟Windows提供的CSP功能目标是基本相同的。目前,OpenSSL的0.9.7版本支持的内嵌第三方加密设备有8种,包括:CryptoSwift、nCipher、Atalla、Nuron、UBSEC、Aep、SureWare以及IBM 4758 CCA的硬件加密设备。现在还出现了支持PKCS#11接口的Engine接口,支持微软CryptoAPI的接口也有人进行开发。当然,所有上述Engine接口支持不一定很全面,比如,可能支持其中一两种公开密钥算法。
密钥和证书管理是PKI的一个重要组成部分,OpenSSL为之提供了丰富的功能,支持多种标准。
首先,OpenSSL实现了ASN.1的证书和密钥相关标准,提供了对证书、公钥、私钥、证书请求以及CRL等数据对象的DER、PEM和BASE64的编解码功能。OpenSSL提供了产生各种公开密钥对和对称密钥的方法、函数和应用程序,同时提供了对公钥和私钥的DER编解码功能。并实现了私钥的PKCS#12和PKCS#8的编解码功能。OpenSSL在标准中提供了对私钥的加密保护功能,使得密钥可以安全地进行存储和分发。
在此基础上,OpenSSL实现了对证书的X.509标准编解码、PKCS#12格式的编解码以及PKCS#7的编解码功能。并提供了一种文本数据库,支持证书的管理功能,包括证书密钥产生、请求产生、证书签发、吊销和验证等功能。
事实上,OpenSSL提供的CA应用程序就是一个小型的证书管理中心(CA),实现了证书签发的整个流程和证书管理的大部分机制。
5.SSL和TLS协议
OpenSSL实现了SSL协议的SSLv2和SSLv3,支持了其中绝大部分算法协议。OpenSSL也实现了TLSv1.0,TLS是SSLv3的标准化版,虽然区别不大,但毕竟有很多细节不尽相同。
虽然已经有众多的软件实现了OpenSSL的功能,但是OpenSSL里面实现的SSL协议能够让我们对SSL协议有一个更加清楚的认识,因为至少存在两点:一是OpenSSL实现的SSL协议是开放源代码的,我们可以追究SSL协议实现的每一个细节;二是OpenSSL实现的SSL协议是纯粹的SSL协议,没有跟其它协议(如HTTP)协议结合在一起,澄清了SSL协议的本来面目。
具体见代码分析
#include <windows.h>
#include <openssl/evp.h>
#include <openssl/x509.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
ALG_LIST g_Alg_List[]=
{
{"ECB模式的DES算法",NID_des_ecb},
{"CBC模式的DES算法",NID_des_cbc},
{"CBC模式3DES算法",NID_des_ede3_cbc},
{"ECB模式3DES算法",NID_des_ede3_ecb},
{"CBC模式的IDEA算法",NID_idea_cbc},
{"ECB模式的IDEA算法",NID_idea_ecb},
{"128位的ECB模式的AES算法",NID_aes_128_ecb},
{"128位的CBC模式的AES算法",NID_aes_128_cbc},
{"192位的ECB模式的AES算法",NID_aes_192_ecb},
{"192位的CBC模式的AES算法",NID_aes_192_cbc},
{"256位的ECB模式的AES算法",NID_aes_256_ecb},
{"256位的CBC模式的AES算法",NID_aes_256_cbc},
{NULL,0}
};
/////////////////////////////////////////////////////////////////////////////
// CAboutDlg dialog used for App About
class CAboutDlg : public CDialog
{
public:
CAboutDlg();
// Dialog Data
//{{AFX_DATA(CAboutDlg)
enum { IDD = IDD_ABOUTBOX };
//}}AFX_DATA
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CAboutDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
//{{AFX_MSG(CAboutDlg)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
//{{AFX_DATA_INIT(CAboutDlg)
//}}AFX_DATA_INIT
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CAboutDlg)
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
//{{AFX_MSG_MAP(CAboutDlg)
// No message handlers
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMyDlg dialog
CMyDlg::CMyDlg(CWnd* pParent /*=NULL*/)
: CDialog(CMyDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CMyDlg)
// NOTE: the ClassWizard will add member initialization here
//}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CMyDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMyDlg)
DDX_Control(pDX, IDC_COMBO1, m_ALGLIST);
//}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
//{{AFX_MSG_MAP(CMyDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON1, OnButton1)
ON_BN_CLICKED(IDC_BUTTON2, OnButton2)
ON_BN_CLICKED(IDC_BUTTON4, OnButton4)
ON_BN_CLICKED(IDC_BUTTON5, OnButton5)
ON_BN_CLICKED(IDC_BUTTON3, OnButton3)
ON_BN_CLICKED(IDC_BUTTON6, OnButton6)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CMyDlg message handlers
BOOL CMyDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog.The framework does this automatically
//when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
m_ALGLIST.Clear();
for(int i=0;;i++)
{
if(g_Alg_List.nAlgID ==0)
{
break;
}
m_ALGLIST.InsertString(i,g_Alg_List.strAlgName);
}
m_ALGLIST.SetCurSel(i-1);
OpenSSL_add_all_algorithms();
return TRUE;// return TRUEunless you set the focus to a control
}
void CMyDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialog::OnSysCommand(nID, lParam);
}
}
// If you add a minimize button to your dialog, you will need the code below
//to draw the icon.For MFC applications using the document/view model,
//this is automatically done for you by the framework.
void CMyDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialog::OnPaint();
}
}
// The system calls this to obtain the cursor to display while the user drags
//the minimized window.
HCURSOR CMyDlg::OnQueryDragIcon()
{
return (HCURSOR) m_hIcon;
}
//设置待加密的原文文件路径
void CMyDlg::OnButton1()
{
static char BASED_CODE szFilter[] = "全部文件 (*.*)|*.*||";
CFileDialog filedlg(
TRUE,NULL,NULL,
OFN_EXPLORER,
szFilter);
if(filedlg.DoModal()==IDOK)
{
SetDlgItemText(IDC_EDIT1,filedlg.GetPathName());
}
}
//设置加密后的密文文件路径
void CMyDlg::OnButton2()
{
static char BASED_CODE szFilter[] = "全部文件 (*.*)|*.*||";
CFileDialog filedlg(
TRUE,NULL,NULL,
OFN_EXPLORER,
szFilter);
if(filedlg.DoModal()==IDOK)
{
SetDlgItemText(IDC_EDIT2,filedlg.GetPathName());
}
}
//设置待解密的密文文件路径
void CMyDlg::OnButton4()
{
static char BASED_CODE szFilter[] = "全部文件 (*.*)|*.*||";
CFileDialog filedlg(
TRUE,NULL,NULL,
OFN_EXPLORER,
szFilter);
if(filedlg.DoModal()==IDOK)
{
SetDlgItemText(IDC_EDIT3,filedlg.GetPathName());
}
}
//设置解密后的原文文件路径
void CMyDlg::OnButton5()
{
static char BASED_CODE szFilter[] = "全部文件 (*.*)|*.*||";
CFileDialog filedlg(
TRUE,NULL,NULL,
OFN_EXPLORER,
szFilter);
if(filedlg.DoModal()==IDOK)
{
SetDlgItemText(IDC_EDIT4,filedlg.GetPathName());
}
}
//加密
void CMyDlg::OnButton3()
{
CString strPlainFilePath;
CString strCipherFilePath;
int nAlg_ID;
CString strPass;
GetPassDlg passdDlg;
//获得原文和密文文件的路径
GetDlgItemText(IDC_EDIT1,strPlainFilePath);
GetDlgItemText(IDC_EDIT2,strCipherFilePath);
if(strCipherFilePath.IsEmpty()||strPlainFilePath.IsEmpty())
{
AfxMessageBox("请输入正确的文件路径!");
return;
}
if(strCipherFilePath==strPlainFilePath)
{
AfxMessageBox("密文文件和原文文件不能为同一个文件!");
return;
}
//获得密码算法
nAlg_ID = g_Alg_List.nAlgID;
#ifdef _DEBUG
AfxMessageBox(strPlainFilePath);
AfxMessageBox(strCipherFilePath);
AfxMessageBox(g_Alg_List.strAlgName);
#endif
//获得文件保护密码
if(passdDlg.DoModal()==IDOK)
{
strPass = passdDlg.m_Pass;
}
else
{
AfxMessageBox("请输入加密文件的密码");
return;
}
#ifdef _DEBUG
AfxMessageBox(strPass);
#endif
if(Encrypt_File(strPlainFilePath,strCipherFilePath,nAlg_ID,strPass))
{
AfxMessageBox("加密文件成功");
}
else
{
AfxMessageBox("加密文件失败");
}
}
//解密
void CMyDlg::OnButton6()
{
CString strPlainFilePath;
CString strCipherFilePath;
CString strPass;
GetPassDlg passdDlg;
//获得原文和密文文件的路径
GetDlgItemText(IDC_EDIT4,strPlainFilePath);
GetDlgItemText(IDC_EDIT3,strCipherFilePath);
if(strCipherFilePath.IsEmpty()||strPlainFilePath.IsEmpty())
{
AfxMessageBox("请输入正确的文件路径!");
return;
}
if(strCipherFilePath==strPlainFilePath)
{
AfxMessageBox("密文文件和原文文件不能为同一个文件!");
return;
}
#ifdef _DEBUG
AfxMessageBox(strPlainFilePath);
AfxMessageBox(strCipherFilePath);
#endif
//获得文件保护密码
if(passdDlg.DoModal()==IDOK)
{
strPass = passdDlg.m_Pass;
}
else
{
AfxMessageBox("请输入加密文件的密码");
return;
}
#ifdef _DEBUG
AfxMessageBox(strPass);
#endif
if(Decrypt_File(strCipherFilePath,strPlainFilePath,strPass))
{
AfxMessageBox("解密文件成功");
}
else
{
AfxMessageBox("解密文件失败");
}
}
/**********************************************************************
函数名称:Encrypt_File
函数功能:加密文件
处理过程:
1.根据选择的密码算法以及口令,生成key和iv。
2.把文件头写入密文文件
3.循环读取原文文件数据加密后保存到密文文件路径中。
参数说明:
strPstrPlainFilePath: CString,待加密的原文文件路径
strCipherFilePath: CString,加密后的密文文件保存路径
nAlg_ID: int 密码算法ID
strPass: CString 口令
返回值:成功返回TRUE,否则返回FALSE
备注说明:密文文件由文件头和密文数据组成,文件头里记录和加密算法信息。
************************************************************************/
BOOL CMyDlg::Encrypt_File(CString strPlainFilePath, CString strCipherFilePath, int nAlg_ID, CString strPass)
{
unsigned char key; //保存密钥的数组
unsigned char iv; ////保存初始化向量的数组
EVP_CIPHER_CTX ctx; //EVP加密上下文环境
unsigned char out; //保存密文的缓冲区
int outl;
unsigned char in; //保存原文的缓冲区
int inl;
const EVP_CIPHER * cipher; //加密算法
int rv;
FILE *fpIn; //输入文件句柄
FILE *fpOut; //输出文件句柄
char enchead={0}; //保存密文文件头的数组
//根据算法ID获得EVP_CIPHER算法
cipher = EVP_get_cipherbynid(nAlg_ID);
if(cipher==NULL)
{
return FALSE;
}
//打开待加密的原文文件
fpIn = fopen(strPlainFilePath.GetBuffer(0),"rb");
if(fpIn==NULL)
{
return FALSE;
}
//打开保存密文的文件
fpOut = fopen(strCipherFilePath.GetBuffer(0),"wb");
if(fpOut==NULL)
{
fclose(fpIn);
return FALSE;
}
strPlainFilePath.ReleaseBuffer();
strCipherFilePath.ReleaseBuffer();
//文件头,保存算法信息
sprintf(enchead,"ALGID:%d\n",nAlg_ID);
fwrite(enchead,1,128,fpOut);
//根据口令、密码算法生成key和iv
EVP_BytesToKey(cipher,EVP_md5(),NULL,(const unsigned char *)strPass.GetBuffer(0),strPass.GetLength(),1,key,iv);
//初始化ctx
EVP_CIPHER_CTX_init(&ctx);
//设置密码算法、key和iv
rv = EVP_EncryptInit_ex(&ctx,cipher,NULL,key,iv);
if(rv != 1)
{
EVP_CIPHER_CTX_cleanup(&ctx);
return FALSE;
}
//以1K为单位,循环读取原文,加密后后保存到密文文件。
for(;;)
{
inl = fread(in,1,1024,fpIn);
if(inl <= 0)//读取原文结束
break;
rv = EVP_EncryptUpdate(&ctx,out,&outl,in,inl);//加密
if(rv != 1)
{
fclose(fpIn);
fclose(fpOut);
EVP_CIPHER_CTX_cleanup(&ctx);
return FALSE;
}
fwrite(out,1,outl,fpOut);//保存密文到文件
}
//加密结束
rv = EVP_EncryptFinal_ex(&ctx,out,&outl);
if(rv != 1)
{
fclose(fpIn);
fclose(fpOut);
EVP_CIPHER_CTX_cleanup(&ctx);
return FALSE;
}
fwrite(out,1,outl,fpOut);//保密密文到文件
fclose(fpIn);
fclose(fpOut);
EVP_CIPHER_CTX_cleanup(&ctx);//清除EVP加密上下文环境
return TRUE;
}
/**********************************************************************
函数名称:Decrypt_File
函数功能:对加密文件解密
处理过程:
1.读取文件头获得加密算法。
2.根据算法和口令生成key和iv
3.循环读取原文文件数据解密,并保存在原文文件。
参数说明:
strCipherFilePath: CString,密文文件路径
strPstrPlainFilePath: CString,解密后的原文文件保存路径。
strPass: CString 口令
返回值:成功返回TRUE,否则返回FALSE
************************************************************************/
BOOL CMyDlg::Decrypt_File(CString strCipherFilePath, CString strPlainFilePath, CString strPass)
{
unsigned char key; //保存密钥的数组
unsigned char iv; //保存初始化向量的数组
EVP_CIPHER_CTX ctx; //EVP加密上下文环境
unsigned char out; //保存解密后明文的缓冲区数组
int outl;
unsigned char in; //保存密文数据的数组
int inl;
const EVP_CIPHER * cipher; //加密算法
int rv;
FILE *fpIn; //输入文件句柄
FILE *fpOut; //输出文件句柄
char enchead={0}; //保存密文文件头的数组
int nAlg_ID=0; //加密算法
//打开待解密的密文文件
fpIn = fopen(strCipherFilePath.GetBuffer(0),"rb");
if(fpIn==NULL)
{
return FALSE;
}
//打开保存明文的文件
fpOut = fopen(strPlainFilePath.GetBuffer(0),"wb");
if(fpOut==NULL)
{
fclose(fpIn);
return FALSE;
}
strPlainFilePath.ReleaseBuffer();
strCipherFilePath.ReleaseBuffer();
//读取密文文件头,获取加密算法
fread(enchead,1,128,fpIn);
sscanf(enchead,"ALGID:%d\n",&nAlg_ID);
if(nAlg_ID==0)
{
return FALSE;
}
//根据算法ID获得EVP_CIPHER算法
cipher = EVP_get_cipherbynid(nAlg_ID);
if(cipher==NULL)
{
return FALSE;
}
//根据口令、密码算法生成key和iv
EVP_BytesToKey(cipher,EVP_md5(),NULL,(const unsigned char *)strPass.GetBuffer(0),strPass.GetLength(),1,key,iv);
//初始化ctx
EVP_CIPHER_CTX_init(&ctx);
//设置解密的算法、key和iv
rv = EVP_DecryptInit_ex(&ctx,cipher,NULL,key,iv);
if(rv != 1)
{
EVP_CIPHER_CTX_cleanup(&ctx);
return FALSE;
}
//以1K为单位,循环读取原文,解密后后保存到明文文件。
for(;;)
{
inl = fread(in,1,1024,fpIn);
if(inl <= 0)
break;
rv = EVP_DecryptUpdate(&ctx,out,&outl,in,inl);//解密
if(rv != 1)
{
fclose(fpIn);
fclose(fpOut);
EVP_CIPHER_CTX_cleanup(&ctx);
return FALSE;
}
fwrite(out,1,outl,fpOut);//保存明文到文件
}
//解密结束
rv = EVP_DecryptFinal_ex(&ctx,out,&outl);
if(rv != 1)
{
fclose(fpIn);
fclose(fpOut);
EVP_CIPHER_CTX_cleanup(&ctx);
return FALSE;
}
fwrite(out,1,outl,fpOut);//保存明文到文件
fclose(fpIn);
fclose(fpOut);
EVP_CIPHER_CTX_cleanup(&ctx);//清除EVP加密上下文环境
return TRUE;
}
作者:yincheng01 发表于2011-12-14 1:49:15 原文链接
页:
[1]