Blog


FIDO UAF系列文章

接下来会陆续写一些FIDO UAF协议的文章,包括文档翻译和知识梳理。现在FIDO UAF有关的文章还比较少,做这件事应该比较有意义。文档以1.0为准。

实现平台默认为Android平台。具体和Web平台和iOS平台的差异在于:各层之间的通讯方式,以及设备调用的方法不同。对协议信息的处理流程是一致的。

我的文章也会发布在Github上。如果大家需要讨论或修正文章中的错误,欢迎在Github上交流。Github的文章会更新一些。
Github地址:https://github.com/SickWorm/FIDO-UAF-Chinese-Document

以下是目录:

解析文章

FIDO UAF Client端工作流程介绍
FIDO UAF中4种Authenticators的区别
FIDO UAF各文档主要内容介绍
Android中Activity/Service获取调用者的信息(FIDO UAF Client获取调用者的信息)

文档翻译

FIDO UAF Authenticator Commands v1.0

版权所有,转载请注明出处:
http://sickworm.com/?p=209




一次活见鬼的调试经历

本篇文章没有什么干货,仅是我的一次调试经历。

昨天在调试FIDO项目,运行时发现某个变量的值和我设置的不对。调试进去,几番折腾,吓呆了我:

img1
img2

仅仅经过一个赋值,就把赋值和被赋值的变量都改变了?我的所有知识都无法解释这个问题。

没办法,现实摆在眼前。只能通过一系列的实验排除问题。

  1. 我怀疑是外部变量的问题。于是我传入常量,问题依然存在。

  2. 我在赋值前进行clone(),入参不再改变。根据1和2,我觉得应该是该对象的变量出的问题。

  3. 我不再进行入参的赋值,而是在构造器直接赋值this.attestationType = new short[1]; this.attestationType[0] = 1;。结果:第一句赋值成功,第二句没有效果。

  4. 我不再在构造函数中赋值,而是另开一个方法单独赋值。问题依然存在。
    img3

  5. 我开始怀疑是不是注解的问题。@TagMember是一个RUNTIME注解,功能类似GSON,用于进行对象和特定数据格式(TLV)之间的转换。我把注解注释掉,问题不再复现!
    img4

  6. 我怀疑是注解里面的变量有问题。于是我把supportedExtensionIDs的注解复制到attestationType上。问题依然不再复现。

  7. 这就奇怪了,为什么注解里的值会影响变量本身呢?这和注解的原理和功能并不一致。为了防止“改动完又改回去,突然就可以了”这样的怪圈,我回归了一下原代码,问题重新出现。
    img5

  8. 接下来更奇怪的事情出现了。我把supportedExtensionIDs的值从null改回到{"1", "2"},问题不再出现!这个问题我在步骤1就已经测试过了,这是真的出现了“改动完又改回去,突然就可以了”的致命事件。。我被打懵了。。
    img6

  9. 我再改回null,问题又回来了。
    img7

  10. 我试着精简我的问题模型。我把其他变量都删掉了,只留下attestationType。问题依然存在。

  11. 继续关注刚刚注释注解的线索。我的其他变量都有注解,却偏偏只有这个变量出现了问题。由于步骤7的现象,我把attestationTypesupportedExtensionIDs的位置交换,并把注解的值也交换了。问题依然存在。

  12. 我把attestationType的类型修改为byte[]和int[]。赋值问题不再出现!于是我现在得到的结论是:被注解的short[]类型会出现赋值问题

  13. 为什么偏偏是short[]类型呢?我想的有点偏,我认为是注解的short tag()这个方法导致的问题。但修改TagMember这个tag又会影响太多代码,于是我拷贝了@TagMember类,并定义为@TagMember2。将attestationType的注解改为@TagMember2,并改为int tag()。问题消失了。我自豪的猜想:难道我遇到了JAVA编译器的问题?

  14. 就都麻袋,还不能太早下结论。我把@TagMember2改回short tag()。现在两个注解除了类名都一样了。运行,没有赋值问题。意思就是:和short tag()没有关系。

  15. 一样的注解,但是却不一样的结果?只有@TagMember才会出现问题,@TagMember2没有问题,但他们明明是一样的。@TagMember到底有什么特别之处呢?

  16. 哦,我想起来了。我在父类里面会对其进行反射调用,取值构造新数据结构(步骤4有提到)。而这个方法会通过toString()执行。

  17. caocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocaocao!!!!!!!!!!!一下子都想通了。

  18. 验证一下,我把父类的继承去掉,让该类继承Object。赋值问题消失。

  19. 我检查了一下toString()中我对short[]类型的处理,发现我在赋值的时候仅做了浅拷贝(也就是直接赋值),然后做了修改值的操作。而在debug过程中每一个单步调试IDE都会调用对象的toString()方法,而toString()方法会修改short[]类型的变量,所以出现了一开始的赋值问题。

  20. 关于步骤7里面的线索(supportedExtensionIDs会影响attestationType的赋值成功):是因为我的toString()方法里没有对String[]类型的处理分支。而在null的时候,我的处理会忽略这个变量,则流程继续走下去,把short[]的处理分支执行了。而在非null的时候,因为toString()无法处理,会直接报错,则没有走到处理short[]的分支。所以supportedExtensionIDs在非null的时候,attestationType的赋值不受影响。

  21. 关于步骤7和步骤1的同样环境不同结果的问题:最后我无法复现步骤1的情况,恐怕是调试期间我不小心改了哪里吧。。我认为不会是很值得研究的原因,解决了主要疑惑和次要疑惑,有时候该放手就放手吧。

  22. 我在整理文章的时候想通了步骤21的疑惑。是断点位置的问题。在后面的调试中,我把断点设在了构造函数末尾,则supportedExtensionIDs的值会影响到attestationType(因为在那个时候IDE才开始调用toString())。而如果是一步一步调试,则supportedExtensionIDs被赋值前attestationType已经被修改了。

  23. 步骤6的问题,是因为我刚好把步骤22所说的断电给移动了,导致了这个现象,其实和值并没有关系。

所以整个“活见鬼”的原因是这样的:

  1. 该类的父类重写了toString,toString里面会反射调用各个成员的值,组成一个新的数据格式。
  2. toString()中我对short[]类型的处理有问题,我没考虑浅拷贝的问题,在里面做一个修改值的行为,把原变量修改了。导致变量本身的值出现的变化。
  3. 在debug的每一次都会调用该变量的toString(),所以值在点击下一步的时候,就会被改变。
  4. 该问题一开始暴露在运行时,是因为运行时后面代码也有调toString(),所以造成了假象。如果我只是用打印的方法查询该变量值而不是debug,在运行时的构造函数中该值不会被改变。
  5. 所以表面看起来只是单步调试了一个赋值语句,而值就被变化了。深层结果是调试器调用了toString()导致的修改值行为。

本次经历给我的经验是:

  1. 对数组的赋值和操作,尤其是在构造函数时,应当进行拷贝而不是直接引用(数组直接clone()就可以了)。之前没因为浅拷贝遇到问题,所以一直没有在意。
  2. 一些莫名其妙的表象其实背后可能是清晰可推理的故障多米诺。我们要做的是,先找出复现和修复规律,然后慢慢精简问题模型,再通过控制变量法找到问题所在。而不是困与一时无法解释花哨表象,无法前进。其实想通之后,原来乱七八糟的bug现象都可以解释了。
  3. debug阶段可能会因为toString()方法影响真实运行时结果。

版权所有,转载请注明出处:
http://sickworm.com/?p=184




Android工程使用org.apache.commons.codec(commons-codec)库,运行时提示Base64.encodeBase64URLSafeString“java.lang.NoSuchMethodError”解决方法

最近工作是开发FIDO UAF项目。FIDO UAF是一个旨在提供身份验证通用方案,以代替繁杂密码记忆的一个方案,Google,阿里这些大头都是核心成员,感觉是用来未来代替密码的1号方案。可能是定义的太通用了,UAF目前应用面还不广。

UAF还有个兄弟叫U2F,是个更精简的方案。U2F是一个用硬件外设(token)代替密码的方案,有点像u盾。外国已经有相关设备,Google登录已经可以通过U2F设备进行验证。使用很方便:你在电脑插入一个U2F设备(初次需要绑定账户),再打开网页,Google就自动为你登录。腾讯也有做一个叫QKey的,很快就会面市了。U2F实用性比较强。

回到正题,我在开发过程中,同事找到了eBay的UAF实现开源代码。我稍微看了一下,看到他们的Base64编码是使用了Base64.encodeBase64URLSafeString这个方法。我试了一下,和普通的Base64编码不同的是把编码末尾的”=“号去掉了。这样的话Http的Get参数就不会出现问题。虽然UAF要求使用POST发送请求,但我看到大佬都这么做,我就跟着用吧。

这个方法在org.apache.commons.codec(commons-codec)中定义。我也下了这个库,放在我自己的项目中,并替换了原来的Base64实现。而稍后在我进行单元测试的时候出问题了。Android单元测试提示我找不到“Base64.encodeBase64URLSafeString”这个方法。

这就奇怪了,编译期没有报错,运行时报错?我一下子没想明白。我反编译commons-codec库,里面是有这个方法的。我在运行时用反射打印出来,没有这个方法。然后我用Java单元测试试了一下,Java单元测试通过。

那看来是Android运行环境的问题了。我上网找了一下,居然有和我一样问题的人:
java.lang.NoSuchMethodError: org.apache.commons.codec.binary.Base64.encodeBase64

里面写的很清楚,Android的framework引用了一个旧版的commons-codec(而这个旧版本没有这个方法)。这会导致后加载的同一名称的包无效(无法加载并覆盖)。所以我在运行时就找不到这个方法了。

解决方法

没办法。Java的类加载安全机制决定了我们没办法使用后加载的类。我想起在“码农翻身”公众号里有看过相关的文章。我找了一下,节选如下:

我感到前途未卜, 但也不能坐以待毙, 一定得多了解信息。
“大哥, 你叫什么名字” , 我看小个子还算和气。
“我就是大名鼎鼎的文件验证器了, 能管很多事”
“那刚才他为啥还得请示上级呢” , 我用眼神指了一下开车的ClassLoader
文件验证器的声音一下子就压低了:
“你不知道,说来话长, 我们之前出现过事故,有个黑客写了个类java.lang.String, 和我们老板手下有一个干活最卖力的员工名字一模一样,只是这个黑客类里边竟然有格式化硬盘的代码,我们的小兵Classloader 不明就里,就把 这个黑客类给先装载了,也执行了, 最后的结果,唉,很惨的… ”

“那后来怎么办?”
“后来我们老板就定下了规矩:他的骨干员工像String, ArrayList等只能由他自己的心腹去装载, 我听说老板的心腹都是分层级的,像传销一样, 每个都有上线, 最顶层的叫Bootstrap Classloader , 下一次级叫Extension Classloader, 现在开车的这位其实叫App Classloader,位于最底层, 咱这位Classloader 在装载一个类之前,一定要问一问这几位权利极高的大爷,请他们先装载,这几位爷装载不了,才由我们这些小兵来出马。“

其实只要先加载了就没办法再重新加载了。我们普通app环境做不到上述黑客的环境,上面的文章只是给大家加深记忆。

不过如果你的类是你自己用Classloader 加载的话,还是可以重新加载的,相关文章:
JAVA动态重新加载Class机制

有办法。其实你把里面的实现拷出来,换个包名也可以。但是类之间的互相依赖可能会有点多。

但我突然发现,

为什么eBay的代码却用了这个库呢????他们是不是没有实际跑过呢???【黑人问号】

总结

Android工程使用org.apache.commons.codec(commons-codec)库,运行时提示“java.lang.NoSuchMethodError”的原因是:Android内部已经加载过同名的旧类库,导致项目中引用的库无法加载,而旧库的Base64类又没有这个方法。

解决方法是:
1. 换一个库,使用Android自带的Base64方法,并加上Base64.URL_SAFE标记位。(最好是Base64.NO_WARP | Base64.NO_PADDING | Base64.URL_SAFE)
2. 把实现拷出来(依赖过多,不现实)

版权所有,转载请注明出处:
https://sickworm.com/?p=175