最近一个项目“无线网接入动态密码验证” ,使用思科的ACS作为Radius客户端,自己实现Radius服务端进行密码验证,步骤如下:(握手过程Radius已经封装,无须管它)
1, 客户端:发送报文,接受返回报文并解析,然后进行相应的处理(客户端由思科的ACS处理);
2, 服务端:接受报文,解析报文并验证(密码之类),然后响应相应的结果(需要java实现)
使用MS-CHAP-V1协议通讯过程简介:
1,客户端和服务端需要一个共享密钥,这个可以自己约定并随意设置,只要保持两边一致就行;
2,客户端传过来的报文包括用户名和加密密码,密码是通过挑战数并利用DES算法和MD4算法进行加密的;
3,服务端收到报文后,需要解析报文,并取出用户名和加密密码;由于用户传过来的加密密码是通过不可逆算法加密的,所以,如果利用保存在服务器或动态生成的明文密码来进行验证的话,需要使用与客户端同样的步骤同样的算法来对明文密码进行加密,然后才能与传递过来的加密密码进行匹配验证(重点就在这里了);
4,如果验证失败,只需要写回RadiusPacket.ACCESS_REJECT 状态字即可,否则,需要写回RadiusPacket.ACCESS_ACCEPT,并返回通过算法计算生成的报文(这一块也比较烦琐)
我使用tinyradius 开源框架做为服务端框架,由于tinyradius 只处理了pap、chap协议进行了处理,没有特别针对微软的ms-chap-v1和ms-chap-v2进行处理,所以我稍微改装了一下tinyradius,修改了org.tinyradius.packet.AccessRequest.java,在其中的153行插入了RadiusAttribute msChapSpecial = getAttribute(VENDOR_ID,VENDOR_TYPE); 这样,我就取出了ms-chap-v1的报文属性, 其中VENDOR_ID=311代表微软的协议,VENDOR_TYPE = 25 是ms-chap-v1协议类型。当然,在163行也加如了else if(msChapSpecial != null){}的处理,要不它会报 "Access-Request: User-Password or CHAP-Password/CHAP-Challenge missing" 错误,整个tinyradius修改就这么简单,下面我会附上修改了的AccessRequest.java;
接下来分三步介绍处理过程:
1,接收报文
服务端接收客户端传过来的报文格式如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Vendor-Type | Vendor-Length | Ident | Flags |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| LM-Response
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LM-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LM-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LM-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LM-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LM-Response(cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NT-Response
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Response (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Response (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
由以上报文分析可得到:
Vendor-Type (一个8位字节)
Vendor协议类型
1 for MS-CHAP-Response.
Vendor-Length (一个8位字节)
报文长度
52
Ident (一个8位字节)
唯一标志位
Identical to the PPP CHAP Identifier.
Flags (一个8位字节)
如果flags为0x01,则要优先使用NT-Response来进行验证,如果flags为0,则NT-Response必须被忽略掉
The Flags field is one octet in length. If the Flags field is one
(0x01), the NT-Response field is to be used in preference to the
LM-Response field for authentication. The LM-Response field MAY
still be used (if non-empty), but the NT-Response SHOULD be tried
first. If it is zero, the NT-Response field MUST be ignored and
the LM-Response field used.
LM-Response
(LM-Response)域是长度为24位并由密码和挑战数通过函数LmChallengeResponse()加密后的,如果这个域是空的,那它肯定是被0填充的
The LM-Response field is 24 octets in length and holds an encoded
function LmChallengeResponse() of the password and the received challenge. If this field is empty, it SHOULD be zero-filled.
2,加密本地明文密码,并验证密码的正确性
以上是对接收报文的分析,具体使用tinnyRadius接收代码如下:
byte[] authenticatorChallenge = accessRequest.getAttribute(311, 11).getAttributeData();
byte[] vendorSpecial = accessRequest.getAttribute(211,1).getAttributeData();
int flag = vendorSpecial.length < 2 ? 0 : vendorSpecial[1];
authenticatorChallenge 是验证挑战数,vendorSpecial包含了以上报文中从indent开始到最后的数据
然后从vendorSpecial中取出
byte[] lmPassword = RadiusServerHelper .getSubbytes(vendorSpecial, 2, 24);
byte[] ntPassword = RadiusServerHelper.getSubbytes(vendorSpecial, 26,24);
byte[] username = accessRequest.getUserName();
byte[] localPassword = “”;//本地密码,从数据库中或者文件或者其他地方取出或生成,用来验证客户端传过来的密码是否正确。
好了,所有的数据准备好了,接下来就是认证了,认证需要将本地密码使用相应的算法加密后与客户端传过来的密码进行比较。
具体算法如下:
byte[] encryPassword = new byte[24];
if (flag == 0) {
encryPassword = MSCHAP.LmChallengeResponse(authenticatorChallenge,
localPassword);
//如果flag=0,则使用lmPasswor
if (RadiusServerHelper.byteEquels(encryPassword, lmPassword)) {
System.out.println("--success--");
return encryPassword;
}
//否则,如果falg=1则先使用ntPassword,如果没通过,再使用lmPassword
} else if (flag == 1) {
encryPassword = MSCHAP.NtChallengeResponse(authenticatorChallenge,
localPassword);
if (RadiusServerHelper.byteEquels(encryPassword, ntPassword)) {
System.out.println("--success--");
return encryPassword;
}else{
encryPassword = MSCHAP.LmChallengeResponse(
authenticatorChallenge, localPassword);
if(RadiusServerHelper.byteEquels(encryPassword,lmPassword)){
System.out.println("--success--");
return encryPassword;
}
}
}
使用函数如下:
public static byte[] LmChallengeResponse(byte[] Challenge, byte[] Password) {
byte[] PasswordHash = LmPasswordHash(Password);
return ChallengeResponse(Challenge, PasswordHash);
}
public static byte[] NtChallengeResponse(byte[] Challenge, byte[] Password) {
byte[] PasswordHash = NtPasswordHash(Password);
return ChallengeResponse(Challenge, PasswordHash);
}
//使用DES算法对本地密码加密
private static byte[] LmPasswordHash(byte[] Password) {
String pString = (new String(Password)).toUpperCase();
byte[] PasswordHash = new byte[16];
byte[] pByte = new byte[14];
for (int i = 0; i < 14; i++)
pByte[i] = 0;
Password = pString.getBytes();
for (int i = 0; i < 14 && i < Password.length; i++)
pByte[i] = Password[i];
DesHash(pByte, 0, PasswordHash, 0);
DesHash(pByte, 7, PasswordHash, 8);
return PasswordHash;
}
//使用MD4算法对本地密码进行加密
private static byte[] NtPasswordHash(byte[] Password) {
byte PasswordHash[] = new byte[16];
byte uniPassword[] = unicode(Password);
IMessageDigest md = HashFactory.getInstance("MD4");
md.update(uniPassword, 0, uniPassword.length);
System.arraycopy(md.digest(), 0, PasswordHash, 0, 16);
return PasswordHash;
}
其中DES算法可以通过“gnu-crypto-2.0.1.jar” 包中的
IBlockCipher cipher = CipherFactory.getInstance("DES");进行实现
具体算法见 http://www.ietf.org/rfc/rfc2433.txt PAGE 10
3,返回报文
如果验证失败,只需要返回 RadiusPacket.ACCESS_ACCEPT 即可,如果验证成功,则需要返回使用算法计算出来的响应报文
返回的Access-Accept报文里需要包含MS-CHAP-MPPE-Keys属性
The MS-CHAP-MPPE-Keys Attribute contains two session keys for use
by the Microsoft Point-to-Point Encryption Protocol (MPPE). This
Attribute is only included in Access-Accept packets.
A summary of the MS-CHAP-MPPE-Keys Attribute format is given below.
The fields are transmitted left to right.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Vendor-Type | Vendor-Length | Keys
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Keys (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Vendor-Type
12 for MS-CHAP-MPPE-Keys.
Vendor-Length
34
Keys
The Keys field consists of two logical sub-fields: the LM-Key and
the NT-Key. The LM-Key is eight octets in length and contains the
first eight bytes of the output of the function LmPasswordHash(P,
This hash is constructed as follows: let the plain-text password
be represented by P.
The NT-Key sub-field is sixteen octets in length and contains the
first sixteen octets of the hashed Windows NT password. The
format of the plaintext Keys field is illustrated in the following
diagram:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| LM-Key
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LM-Key (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NT-Key
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Key (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Key (cont)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NT-Key (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Padding
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Padding (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The Keys field MUST be encrypted by the RADIUS server using the
same method defined for the User-Password Attribute [3]. Padding
is required because the method referenced above requires the field
to be encrypted to be a multiple of sixteen octets in length.
Implementation Note
This attribute should only be returned in response to an
Access-Request packet containing MS-CHAP attributes.
返回报文需要将明文密码进行LmPasswordHash 加密成LM-key,将明文密码使用2次MD4加密成NT-Key,然后将LM-key + NT-key + 8位padding组成32位key,
再将key 通过客户端传来的16位加密挑战数(SA)进行加密(即接收整体报文的前16位),这个加密算法为:
1, 将key 分成两个16位P1,P2
2, 将共享密钥(S)与SA进行MD5加密得到B1
3, 将B1与P1进行异或操作得到C1
4, 将C1与S进行MD5加密得到B2
5, 将B2与P2进行异或操作得到C2
6, 将C1+C2得到返回报文
具体参见http://www.ietf.org/rfc/rfc2548.txt
返回报文具体代码如下:
byte[] lmPass = MSCHAP.LmPasswordHash(generatePassword);
byte[] ntPass = MSCHAP.NtPasswordHash(generatePassword);
ntPass = MSCHAP.HashNtPasswordHash(ntPass);
byte[] keys = MSCHAP.getKeysForMSCHAPv1(lmPass, ntPass);
byte[] p1 = new byte[16];
byte[] p2 = new byte[16];
byte[] b1 = null;
byte[] b2 = null;
byte[] secret = getSecret()==null?null:getSecret().getBytes();
byte[] challenge accessRequest.getAuthenticator();
System.arraycopy(keys, 0, p1, 0, 16);
System.arraycopy(keys, 16, p2, 0, 16);
b1 = MSCHAP.HashKeys(secret, challenge);
byte[] c1 = MSCHAP.Xor(b1, p1);
b2 = MSCHAP.HashKeys(secret, c1);
byte[] c2 = MSCHAP.Xor(b2, p2);
byte[] response = new byte[32];
System.arraycopy(c1, 0, response, 0, 16);
System.arraycopy(c2, 0, response, 16, 16);
RadiusAttribute attribute = new RadiusAttribute( MS_CHAP_MPPE_KEYS_TYPE, response);
attribute.setVendorId(VENDOR_SPECIAL_ID);
RadiusAttribute attribute_Encryption_Policy = new RadiusAttribute(
MS_MPPE_ENCRYPTION_POLICY_TYPE, MSCHAP
.toByteArray("00000001"));
attribute_Encryption_Policy.setVendorId(VENDOR_SPECIAL_ID);
RadiusAttribute attribute_Encryption_Types = new RadiusAttribute(
MS_MPPE_ENCRYPTION_TYPES_TYPE, MSCHAP
.toByteArray("00000006"));
attribute_Encryption_Types.setVendorId(VENDOR_SPECIAL_ID);
int ident = accessRequest.getPacketIdentifier();
packet = new RadiusPacket(type, accessRequest.getPacketIdentifier());
copyProxyState(accessRequest, packet);
其中,加密算法和异或算法可以按照算法步骤自己实现,有些算法比如说DES可以直接从jar包中引入对象实现。
相关附件:
gnu-crypto-2.0.1.jar
---------------------------------
假到真时真亦假,真到假时假亦真
---------------------------------