使用Java解析ASN.1

Overview

ASN.1解析包含两部分,一份是定义文件,一份是数据文件。以*.ASN为后缀的文件是定义文件,一般为结构化的明文文本;以*.dat为后缀的文件是数据文件,里面的内容是编码后的数据,需要解析后才能识别。

  • *.ASN文件样例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
-- ASN.1 Formal Description
HMSC DEFINITIONS IMPLICIT TAGS ::=
BEGIN
Chargingdatarecord ::= SEQUENCE
{
header [0] Header,
sequenceCDR [1] SequenceCDR,
trailer [2] Trailer,
extension [3] Extension
}

Header ::= SET
{
headerTimeStamp [0] HeaderTimeStamp,
recordingEntity [1] RecordingEntity,
extensions [2] Extensions
}

HeaderTimeStamp ::= TimeStamp

SequenceCDR ::= SEQUENCE OF CDR

CDR ::= CHOICE
{
mobileOriginated [0] MobileOriginated ,
callForwarding [100] CallForwarding ,
mobileTerminated [1] MobileTerminated ,
roamingRecord [2] RoamingRecord ,
incomingGatewayRecord [3] IncomingGatewayRecord ,
outgoingGatewayRecord [4] OutgoingGatewayRecord ,
transit [5] Transit ,
sMSMobileOriginated [6] SMSMobileOriginated ,
sMSMobileTerminated [7] SMSMobileTerminated ,
lCSMTRecord [17] LCSMTRecord ,
lCSMORecord [18] LCSMORecord ,
lCSNIRecord [19] LCSNIRecord ,
supplementaryService [10] SupplementaryService ,
hLRInterrogation [11] HLRInterrogation ,
commonEquipmentUsageRecord [14] CommonEquipmentUsageRecord ,
terminatingCAMEL [16] TerminatingCAMEL ,
locationUpdate [13] LocationUpdate
}

  • *.dat文件样例

image-20220722134144587

  • *.dat文件16进制样例

image-20220722134209068

使用Java解析ASN.1需要使用一些第三方的工具,如asn1bean,其前身是jASN1。

整个解析过程如下:

  1. 使用定义文件生成对应的Java文件
  2. 读入数据文件并封装到对应的Java对象中

ASN.1数据类型

常见的数据类型:

ASN.1类型 Java类型
OCTET STRING 字符串
INTEGER 数值
BOOLEAN 布尔
ENUMERATED 枚举
TimeStamp 时间戳
SET 对象
SEQUENCE 集合

特例:

ASN.1类型 Java类型
SEQUENCE OF CDR CDR对象的集合,CDR为ASN1中的SET
NULL 暂时不确定的值

ASN.1示例文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 双-作为注释
-- ASN.1 Formal Description
HMSC DEFINITIONS IMPLICIT TAGS ::=
BEGIN
-- 一般最外层类定义在最上面,Chargingdatarecord的类型是SEQUENCE,是ASN.1原生的集合类型
-- ::=后面的是数据类型
Chargingdatarecord ::= SEQUENCE
{
-- header的类型是Header,而Header是自定义的类型,需要自行定义
header [0] Header,
-- sequenceCDR的类型是SequenceCDR,也是自定义的类型
sequenceCDR [1] SequenceCDR,
-- Trailer和Extension也一样,都是自定义的类型
trailer [2] Trailer,
extension [3] Extension
}

-- HeaderTimeStamp的类型是TimeStamp,是ASN.1原生的时间戳类型
HeaderTimeStamp ::= TimeStamp
1
2
3
4
5
6
7
-- Header的类型是SET,是ASN.1原生的对象类型
Header ::= SET
{
headerTimeStamp [0] HeaderTimeStamp,
recordingEntity [1] RecordingEntity,
extensions [2] Extensions
}
1
2
-- SequenceCDR的类型是SEQUENCE OF CDR,首先是一个SEQUENCE集合,其次是CDR类型的集合
SequenceCDR ::= SEQUENCE OF CDR
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- CHOICE表明CDR类中都有哪些类型
-- 本案中使用到的,如mobileOriginated,mobileTerminated,transit,sMSMobileOriginated,sMSMobileTerminated等
CDR ::= CHOICE
{
mobileOriginated [0] MobileOriginated,
-- tag的顺序编号无所谓,不影响Java代码生成和解析
callForwarding [100] CallForwarding,
mobileTerminated [1] MobileTerminated,
roamingRecord [2] RoamingRecord,
incomingGatewayRecord [3] IncomingGatewayRecord,
outgoingGatewayRecord [4] OutgoingGatewayRecord,
transit [5] Transit,
sMSMobileOriginated [6] SMSMobileOriginated,
sMSMobileTerminated [7] SMSMobileTerminated,
lCSMTRecord [17] LCSMTRecord,
lCSMORecord [18] LCSMORecord,
lCSNIRecord [19] LCSNIRecord,
supplementaryService [10] SupplementaryService,
hLRInterrogation [11] HLRInterrogation,
commonEquipmentUsageRecord [14] CommonEquipmentUsageRecord,
terminatingCAMEL [16] TerminatingCAMEL,
locationUpdate [13] LocationUpdate
}
1
2
3
4
5
6
-- 有些字段在生成Java文件时提示缺少该类的定义,那么可以先定义为NULL,后面如果确实需要使用,再根据华为的文档去补充。
VasType ::= NULL
WpsCallFlag ::= NULL
LRNSource ::= NULL
DrcCallRN ::= NULL
DrcCallId ::= NULL

asn1bean工具

目录结构如下,省略了一些不相干的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
asb1bean
├── bin
│   ├── asn1bean-compiler
│   └── asn1bean-compiler.bat
├── build
│   └── libs-all
│   ├── antlr-2.7.7.jar
│   ├── asn1bean-1.13.0.jar
│   └── asn1bean-compiler-1.13.0.jar
├── gradle
│   └── wrapper
│   ├── gradle-wrapper.jar
│   └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat

./build/libs-all/下有两个jar包:

  • asn1bean-compiler-1.13.0.jar

根据定义文件生成Java文件需要该jar包,asn1bean-compiler.bat和asn1bean-compiler调用的就是这个jar包。

  • asn1bean-1.13.0.jar

生成的Java文件在编译时依赖该jar包,加入到具体项目时需要将该jar包添加到pom中。里面定义了一些依赖,如BerTag等。

使用asn1bean-compiler.bat或asn1bean-compiler就可以根据定义文件生成Java文件,指令如下:

1
2
3
## -f 参数指定定义文件
## -p 文件指定生成Java文件的package名
$ asn1bean-compiler.bat -f "<ASN_FILE_LOCATION>" -p "<JAVA_PACKAGE_NAME>"
1
$ asn1bean-compiler.bat -f "C:\Users\Administrator\Desktop\Zamtel_HW_Msc\2021-01-01\HuaweiMSC_V1_RX.ASN.modified" -p "com.sinovatio.shark.bean.asn1.zamtel"

生成Java文件

根据定义文件生成Java文件的过程中会出现一些错误,常见的错误原因有:注释引起的解析错误,类型定义未找到等。

注释引起的解析错误1

错误提示

1
2
3
4
5
6
7
8
9
10
11
12
13
## 提示1118行第37个字符有问题,遇到了非预期的'-'
Generated code will be saved in ./
Parsing "C:\Users\Administrator\Desktop\Zamtel_HW_Msc\2021-01-01\HuaweiMSC_V1_RX.ASN.original"
Exception in thread "main" line 1118:37: unexpected char: '-'
at com.beanit.asn1bean.compiler.parser.ASNLexer.nextToken(ASNLexer.java:334)
at antlr.TokenBuffer.fill(TokenBuffer.java:69)
at antlr.TokenBuffer.LA(TokenBuffer.java:80)
at antlr.LLkParser.LA(LLkParser.java:52)
at com.beanit.asn1bean.compiler.parser.ASNParser.module_body(ASNParser.java:301)
at com.beanit.asn1bean.compiler.parser.ASNParser.module_definition(ASNParser.java:182)
at com.beanit.asn1bean.compiler.parser.ASNParser.module_definitions(ASNParser.java:58)
at com.beanit.asn1bean.compiler.Compiler.getJavaModelFromAsn1File(Compiler.java:129)
at com.beanit.asn1bean.compiler.Compiler.main(Compiler.java:106)

定义文件

image-20220722141017580

解决方案

根据ASN.1的规范,注释以--开头,以--或行结束符结尾,例如:

1
-- this is comments end with double dash --

1
-- this is comments end with crlf

错误的原因是LEAF NODES前面的-有37个,没有成对出现,所以解析报错。而实际操作中,即使-成对出现,有的也会提示其他错误,如花括号不匹配,缺少END标记等。所以简单粗暴且一劳永逸的解决方法是删除注释。这里应该也有asn1bean工具解析的问题。

注释引起的解析错误2

错误提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## 提示1500行第90个字符有非预期的'*'
Generated code will be saved in ./
Parsing "C:\Users\Administrator\Desktop\Zamtel_HW_Msc\2021-01-01\HuaweiMSC_V1_RX.ASN.demo"
line 1500:67: expecting "END", found 'SEQUENCE'
Exception in thread "main" line 1500:90: unexpected char: '*'
at com.beanit.asn1bean.compiler.parser.ASNLexer.nextToken(ASNLexer.java:334)
at antlr.TokenBuffer.fill(TokenBuffer.java:69)
at antlr.TokenBuffer.LA(TokenBuffer.java:80)
at antlr.LLkParser.LA(LLkParser.java:52)
at antlr.Parser.consumeUntil(Parser.java:149)
at antlr.Parser.recover(Parser.java:312)
at com.beanit.asn1bean.compiler.parser.ASNParser.module_definition(ASNParser.java:188)
at com.beanit.asn1bean.compiler.parser.ASNParser.module_definitions(ASNParser.java:58)
at com.beanit.asn1bean.compiler.Compiler.getJavaModelFromAsn1File(Compiler.java:129)
at com.beanit.asn1bean.compiler.Compiler.main(Compiler.java:106)

定义文件

image-20220722141434129

解决方案

虽然报错信息提示是*号,但这里依然是注释引起的问题。可以将该行注释删除。

类型定义未找到

错误提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## 类型定义MlcNumber没有找到
Generated code will be saved in ./
Parsing "C:\Users\Administrator\Desktop\Zamtel_HW_Msc\2021-01-01\HuaweiMSC_V1_RX.ASN.demo"
Generating classes for module "HMSC"
Exception in thread "main" java.lang.IllegalStateException: Type definition "MlcNumber" was not found in module "HMSC"
at com.beanit.asn1bean.compiler.BerClassWriter.followAndGetNextTaggedOrUniversalType(BerClassWriter.java:2460)
at com.beanit.asn1bean.compiler.BerClassWriter.followAndGetNextTaggedOrUniversalType(BerClassWriter.java:2436)
at com.beanit.asn1bean.compiler.BerClassWriter.followAndGetNextTaggedOrUniversalType(BerClassWriter.java:2404)
at com.beanit.asn1bean.compiler.BerClassWriter.followAndGetNextTaggedOrUniversalType(BerClassWriter.java:2393)
at com.beanit.asn1bean.compiler.BerClassWriter.isDirectAnyOrChoice(BerClassWriter.java:2478)
at com.beanit.asn1bean.compiler.BerClassWriter.getTag(BerClassWriter.java:465)
at com.beanit.asn1bean.compiler.BerClassWriter.writeSequenceOrSetEncodeFunction(BerClassWriter.java:1259)
at com.beanit.asn1bean.compiler.BerClassWriter.writeSequenceOrSetClass(BerClassWriter.java:839)
at com.beanit.asn1bean.compiler.BerClassWriter.writeConstructedTypeClass(BerClassWriter.java:536)
at com.beanit.asn1bean.compiler.BerClassWriter.translateModule(BerClassWriter.java:236)
at com.beanit.asn1bean.compiler.BerClassWriter.translate(BerClassWriter.java:176)
at com.beanit.asn1bean.compiler.Compiler.main(Compiler.java:119)

定义文件

image-20220722141739266

可以看到在定义文件中只有类型的引用而没有定义。

解决方案

只看到使用,没有看到定义。可以暂时定义为NULL,再生成Java试试,如果不报错就继续,如果依然报错,则需要参考华为的文档。

1
MlcNumber ::= NULL

成功提示

定义文件全部修改完成后,生成Java代码不报错即表示成功。可以在bin目录下看到对应的Java文件。

1
2
3
4
Generated code will be saved in ./
Parsing "C:\Users\Administrator\Desktop\Zamtel_HW_Msc\2021-01-01\HuaweiMSC_V1_RX.ASN.demo"
Generating classes for module "HMSC"
done

处理编译错误

生成的Java文件并不能直接使用,里面有一些编译错误需要修改。常见错误如类名和文件名不一致,字段定义重复等。这些错误与定义文件有关,一般修改定义文件后重新生成Java文件即可。也可以在生成的Java文件中直接重构。推荐修改定义文件后重新生成Java文件。

类名与文件名不一致

错误提示

image-20220722092908565

image-20220722092956962

image-20220722093023493

定义文件

image-20220722142002679

解决方案

类型定义只有全大写的VATTYPE定义,但使用时引用了VasType定义。为了更符合Java的命名规范,可以将大写的VASTYPE定义修改为VasType。

定义文件

image-20220722142400290

定义文件中既有对Portedflag的定义,也有对PortedFlag的定义。PortedFlag定义为ENUMERATED,Portedflag定义为INTEGER。

image-20220722142512977

image-20220722142542092

解决方案

显然PortedFlag的定义值覆盖了Portedflag,所以保留PortedFlag,并将所有引用修改为PortedFlag。

定义文件

image-20220722142742407

解决方案

SSCode和SsCode都是OCTET STRING,保留更符合Java命名规范的SsCode,并修改所有的引用。

字段定义重复

错误提示

image-20220722093901629

此类型的错误一般是由于定义文件错误而产生的,可以修改定义文件后重新生成Java文件。

定义文件

image-20220722143130451

image-20220722143345046

可以看到MobileOriginated类型中定义了两个translateNumber字段,Tag Number分别是6和245。

Tag Number为245的还有一个字段mctType,显然应该去掉Tag Number为245的translateNumber。

处理运行时错误

编译错误处理完以后就可以开始尝试解析数据文件,解析过程中还会有错误需要处理。常见错误是缺少字段定义,或字段定义类型与实际数据不匹配。可以根据错误提示补充或修正定义文件,并重新生成Java文件。

替换时要注意根据实际情况选择手动替换不一致的行,或替换整个文件,有时整个文件替换会覆盖已经修改好的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.sinovatio.hmsc.CDR;
import com.sinovatio.hmsc.Chargingdatarecord;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/* decode.java示例 */
public class Decode {
public static void main(String[] args) throws IOException {
String filePath = "C:\\Users\\Administrator\\Desktop\\Zamtel_HW_Msc\\0721\\H20220719876783.dat";
try (InputStream is = new FileInputStream(new File(filePath));) {
Chargingdatarecord c = new Chargingdatarecord();
int pos = c.decode(is);

for (CDR cdr : c.getSequenceCDR().getCDR()) {
System.out.println(cdr.toString());
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

类型错误

错误提示

image-20220722095507894

这里需要关注几个信息:

  • primitive: 32

  • tag number: 2

  • Header.java

说明是Header类中Tag Number为2的字段有问题,传入的类型是32,一般是传入的类型与定义类型不一致。

根据BerTag可以看到,32是CONSTRUCTED,对应ASN.1的SET。

image-20220722095828313

debug可以看到,实际期望的类型是Berg.PRIMITIVE,值为0,而传入的值为32,所以问题是定义和实际数据对不上,那以数据文件为准,修改定义。

image-20220722095926071

定义文件

image-20220722100057076

Header中Tag Number为2的字段是Extensions类型,找下Extensions类型定义,可以看到被定义为NULL。

image-20220722144721192

修改成SET并重新生成Java文件

image-20220722100549093

字段未定义

错误提示

image-20220722101048043

一样的排查方法,Transit类,Tag Number为160,传入类型为0。

定义文件

image-20220722145106378

查看定义文件,发现Transit类中没有对Tag Number为160的字段进行定义。

根据华为文档,可以看到Tag Number 159是rateIndication,160应该紧随其后,文档中rateIndication的Tag值为0x9F 81 1F,那Tag Number 160的Tag值应该是0x9F 81 20,对应字段roamingNumber。

image-20220722145416807

roamingNumber的类型为ADDRESS,在定义文件中搜索ADDRESS相关的类型。

image-20220722145533160

发现有ExternalAddress和AddressString两种,而ExternalAddress最终也指向AddressString,所以先将roamingNumber的类型定义为AddressString。

image-20220722145820023

类似的错误,基本都是定义文件中缺少相关字段,可以对照华为文档补上。

类型错误2

image-20220722110912075

第一个类型错误中,临时将Extensions定义为一个不包含任何字段的SET,目前看还是不对。由于华为的文档里也没有描述,只能从命名猜测Extensions为Extension的集合,参考最一开始将SequenceCDR定义为SEQUENCE OF CDR,这里也可以将Extensions定义为SEQUENCE OF Extension。修改定义后重新生成Java文件即可正常解析数据文件。

image-20220722111541552

测试数据文件

如果不确定数据文件是否正确,可以先从数据文件dump一个结构文件出来看下,一般数据文件没有问题,都可以正常dump出一个结构文件。如果像赞比亚刚开始数据文件错误,那dump时会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import cn.hutool.core.io.FileUtil;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.util.ASN1Dump;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class Dump {
public static void main(String[] args) throws IOException {
String asnFilePath = "C:\\Users\\Administrator\\Desktop\\Zamtel_HW_Msc\\0721\\H20220719876780.dat";
String outputStrucAndDataFilePath = asnFilePath + ".dump";
ASN1InputStream ais = new ASN1InputStream(new FileInputStream(new File(asnFilePath)));
while (ais.available() > 0) {
ASN1Primitive obj = ais.readObject();
FileUtil.writeUtf8String(ASN1Dump.dumpAsString(obj, true), new File(outputStrucAndDataFilePath));
}
ais.close();
}
}

dump需要额外的依赖,artifactId名称需要根据jdk版本来确定,JDK1.8使用bcprov-jdk18on,可以在 Maven Repository 里搜索。

1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.71</version>
</dependency>

pom.xml

正常项目只需要加入以下依赖即可。

1
2
3
4
5
<dependency>
<groupId>com.beanit</groupId>
<artifactId>asn1bean</artifactId>
<version>1.13.0</version>
</dependency>

参考

https://www.beanit.com/asn1/

https://github.com/beanit/asn1bean