新的革命:自由网络 vs. 安全网络

我起了一个巨大的标题。我本不该的。我希望用这样的标题党引起更多人注意这个问题,共同来解决这个世界性的世纪难题。

这篇bo的内容起始于我关于网络实名制的一些想法。但是不成熟,所以没有抛出来。很快就遇到的CSDN以及其他诸多著名网站密码流出事件,更是证明了网络安全问题其实是长久以来悬而未决的难题。后来美国又发起了SOAP的论战,当时俺兴趣不在这个上,也没有仔细去读新闻。过年回来后知后觉地追踪了下时下火热的方韩大战的新闻旧闻,说来这故事跟网络自由和安全其实有些相关性,那就是拷问网络言论究竟应不应该受到责任约束的问题。今天上午在公司无事,反过头来详细阅读了译言翻译的上许多对SOAP和PIPA的详细分析和报道。再反思以前一直思考着的,互联网未来发展的趋势;该不该实名制;用什么方式帮助大家公平地排队买火车票;怎样有效地分发和贩售知识,这种新世纪的财富;怎样有效地让人们在互联网上能够公平自由地发表言论,同时又能切实地为自己的言论负责;怎样保证信息流动性的同时还保证他的可靠性。因为一直思考着这些,发现这些问题其实源于同宗,于是就有了今天这个标题。

经过半个世纪的发展,互联网正走向新世纪,新革命。互联网极大便利人们的同时,也极大地改变,甚至可以说是,毁灭着人类。因为他的便利性地下藏着的是巨大的漏洞,无尽的陷阱。互联网最初的设计来自于学校,而目的是用于军事。也就是说,最早的互联网使用者,是科研机构和政府部门,是用于组织内部,是互相信任的不同机构的通讯网。互联网,从其设计之初的基础来看,就是一个信任网络。互联网是一个明码通信网络。当然,VPN,SSL等技术从不同层次改进了网络的安全性,但VPN并不是互联网的一部分,只是一个局域安全网,而SSL则会遇到密钥分发这个密码学永远要面对的难题,中间人攻击仍然是他的软肋。目前网络常用的用户名-密码验证机制,有着他的巨大漏洞。这绝不是说网站增强自己数据库的安全性就能解决的问题。举例而言,假如我是黑客,我完全可以建立一个用明码存储用户名密码的网站,然后用吸引眼球的东西引诱大家来注册。例如说糟糕图床,例如说二次元漫画,例如说盗版游戏下载,例如说灵异事件讨论和照片分享,等等。你会说我不沾游戏不看盗版电影不上糟糕站,那你会不会订打折机票,会不会团购旅游?恭喜你,你又中招了。黑客世界除了能逆向工程软件和系统,还有逆向社会工程学。你会说我每个网站都用不同的用户名不同的密码。但是这个世界有几个你这样的聪明人,十几亿网民中有千分之一的密码泄漏,就够养活一个师的黑客大军了。

强调绝对的网络自由的后果就是,网络不存在任何安全性可言。那就会陷入无政府主义的痛苦深渊。每个上网的人都必须是黑客,否则就是黑客押镖保护下的惊慌旅客,再不然就是待宰羔羊。网站安全要靠站长自身素质、纪律和技术支持;上网安全要靠网民自身武艺高超。那将会是一个骗子横行的世界。例如我可以开发一个720软件,密码存我这,包你上网无忧。结果实际我是地下黑客组织的大后台,我一边大肆兜售安全软件,一边就把所有人的私人信息插标出售。那你还敢信银行专用防火墙吗?那你还敢信全自动保安系统吗?那你还敢信网上银行吗?自动操盘bot?天哪没有网络安全的世界是不可想象的。

那么,反其道而行之呢?建立强有效的安全网络?问题是如何去做,问题是技术水平是否足够,问题是国际政治环境是否允许,问题是新的政策新的法律新的技术将如何影响现有的利益集团。

实名制:毫无疑问,实名制会给网络减少不少乐趣。用一个名字拉风的马甲胡吹海泡的时代一去不复返了。但是好处呢?每个人都要为自己说的话负责;或者仍然允许匿名,但是如果你用的是匿名,则你说的话根本没人搭理。谩骂、水军、造谣、煽动、这些毫无营养的网络垃圾终于可以省省了。更多的空间让给建设性的建议和评审,让给知识的有效流动。同时,实名制的信息签名制度使得一个人说的话在被复制和传播的时候他的签名被同时传播,于是制止了匿名转载,保证了CC实现。另外如果有一个强有力的独立部门承担网络ID的认证工作,则任何其他网站都不需要保存用户的密码了,于是再也没有密码泄漏问题。当然前提是那个独立认证部门必须切切实实的强有力,值得信赖。
技术难度:签名的不可复制性需要密码学的严谨论证;公民的网络ID将成为他的新一代身份证,同时也是他全部虚拟财产的钥匙,需要强有力的安全部门保证这个帐号的安全。

默认加密:现代互联网仍然是默认明文的。使用一些简易设施,任何人都能截取局域网或者wifi中传递的私人信息。加密应该在网络的底层实现,而不是依赖应用程序提供者的道德约束。问题是想要从底层修改网络协议,影响面太广,不是一日可完成的任务。并且这要求世界各国的网站齐心合力一同做到。新的IPV6协议中包含更多的安全部件,但仍然是可选项。强制安全的网络协议是未来人类的必然选择。
技术难度:向后兼容

异构云系统:这是我的独立发明。未来互联网就是云系统。但是现在的云系统都是同构的,同一个系统内部的无数并行运算组件其实是运行在同一个机房几乎完全相同硬件上的完全相同的软件。这也就是说,无论你的安全机制多么严密,只要黑客攻克了你那么一点点,那么所有的机器,所有的组件,全都被攻克了。我认为真正安全的系统必须是异构复合的。同样是处理邮件,同一个服务,在云端由不同的并行组件,就可能由成千上万种完全不同的方式处理掉(当然结果是相同的,异曲同工嘛)。这在自然界,就是称作“物种多样性”的杰作。这样黑客就算创造出一种病毒,也仅仅能摧毁某种特定类型的细胞,而其他的细胞完全不受影响。那种特定类型的细胞灭绝之后,这种病毒也就跟着一起灭绝了。要如何实现异构复合的云计算系统?毫无疑问要依赖遗传编程。啊伟大的人工智能,创造奇迹吧,改变软件工程的历史性大手笔!
技术难度:遗传编程的工程应用。于此相比,更大的难度在于安全性对于服务提供商而言是附加题,他们不会在没有利益驱动的前提下自动花大笔的精力去做。因此此事的推动需要等待互联网的进一步进步。

我写了好多,但还是太少了。但是很明显的,再多写也没有意义。推动网络革命需要的技术进步,远远比不上他所需要的社会进步那么急切。如果互联网不能进一步前进,进一步暴露他的问题,给各利益方以驱动,则即使技术摆在那里,也没有人想要去用。结果又变成了,技术拯救人类和技术毁灭人类的死亡竞速。

因此纸尽言犹,先就此收笔,静候网络发展的佳音吧。

又是命名法

命名法是个我不想再纠结的东西,不过最近又遇上了…
碰巧过完年,脑子锈掉了,居然读不懂自己年前写的代码了。我向来奉行“代码即注释”原则,也就是说…几乎不写注释…完全依靠函数和变量命名的含义来表达代码的意图和思路,除非真有复杂到需要注释的地方,例如莫名其妙的需要多线程同步的地方之外,其余一律不写注释。不过反作用就是,年前那阵子贪玩,没认真好好写代码,导致命名有点糟糕,过了年回来再读就有点绕人了…

一边改那些变量的名字,一边就遇到这个命名法的问题。我还在用匈牙利前缀,整数用n,浮点数用f,字符串用sz。现在需要自己增加几个自己的规则。
iterator类型,我原先用it做前缀,打出的代码看上去非常丑陋,例如:


for(std::vector<Phone>::iterator itPhone = phoneList.begin(), 
    itPhoneEnd = phoneList.end(); itPhone != itPhoneEnd; ++itPhone) {
  //...
}

单纯就觉得这两个字母放在前面不搭,不美…现在我用小写t做前缀,好看多了。


for(std::vector<Phone>::iterator tPhone = phoneList.begin(),
    tPhoneEnd = phoneList.end(); tPhone != tPhoneEnd; ++tPhone) {
  //...
}

当然了,这是古董代码了,有了foreach,这样的代码已经可以进垃圾堆了。不过难免用到iterator的时候俺就该记得用这个t前缀。还有就是项目里的代码我不想霰弹枪一起动,先这样放着比乱改安全很多…

受这个启发,typedef出来的变量类型用大写T前缀,例如


typedef std::vector<Phone> TPhoneList; 
for(TPhoneList::iterator tPhone = phoneList.begin(), 
    tPhoneEnd = phoneList.end(); tPhone != tPhoneEnd; ++tPhone) {
  //...
}

我喜欢typedef出几个项目中常用的容器类型。仅仅到容器类型,而iterator类型直接使用Type::iterator引用,而不进一步typedef。这样恰好保持代码的简洁程度适中,容易理解。

std::string类型用str前缀,而raw string类型(0结尾的字节序列)用sz类型,从而区分两者。这一点通常不重要,但是当你使用printf的时候:


std::string strName = "Peter Pan";
printf("hello %s\n", strName.c_str());

str前缀会提醒你记得用上c_str()方法。事实上这个问题惹恼我好几回了,最倒霉的一回是同事从北京打电话跟我联调,他那边输出的log出现乱码,而我这边丝毫没有,他通过IM给我看输出,告诉我printf打出来的。我不相信我的眼睛,好像就是变戏法。直到调了好久他才突然说,噢!忘记.c_str()了!
当然这仍旧不是好方法。真正问题出在printf不是一个类型安全的函数,编译器无法为你做编译时检查。但是类似printf, sprintf这类函数使用方便,即使最终产品不使用,调试阶段输出log还是会经常使用的。这个时候区分std::string和raw string就变得有意义了。

堆上空间使用p前缀,而栈上空间不使用。这里主要说的是数组。主要目的是当你使用p前缀的变量时候,就会提醒着自己记得思考该让谁delete掉他这个问题了。
例如


char szName[] = "Peter Pan";
char* pszName = new char[10];

我忽然在思考,是不是应该用q前缀来命名那些堆空间的拥有者,而用p前缀来命名那些弱引用。但如果是这样,那么不明拥有者,或者拥有者变更的情况怎么办?再引入一个ps前缀(p_shared_)?也许会弄得太复杂了。也许既然想了这么多,还不如一开始就用智能指针。为什么我不想引入智能指针?我不想引入复杂度。因此如果仅仅为了区区命名而导致复杂度增加,还不如一开始就简单化。所以还是所有指针都用p前缀。到底谁负责,自己好好想清楚。我想,引导读程序的人思考,比用带有暗示意味却可能是错误的命名要好得多。
在使用弱引用时,我经常使用的名字,例如pRead, pWrite, pBegin, pEnd, pCurrent, 等等,这些名字具有强烈的暗示性,说明他们是弱引用,正常人是不会去释放这样名称的变量的,更不会给这样的变量开辟空间。而另一些名字则比较危险,比如pNext, pChild, pParent, pSibling。这些名字的变量,可能你需要去释放他,可能你不需要(例如这是一个树状数组)。这个时候注释是必须的,否则很可能今天写的代码,明天就忽然觉得,不对,这里既然pNext被移动了,那么原来那个东西就应该先释放掉。然后Boom!程序崩溃。还好,现代程序员几乎不需要自己编写类似这样的基础数据结构,直接用酒精久经考验的标准库就行了。而且,就算偶尔碰上了,只要仔细调试通过,以后几乎不会再去改他了(毕竟最多的改动还是在上层嘛)。所以不算什么太大的麻烦。

呜,暂时想记录的就这么多了,给以后想要偷懒的自己看。

Monkey Test 遐思

给公司做了个Symbian Monkey Test工具,结果跑monkey test的任务就一起承担下来。
Monkey Test其实就是个随机输入序列。我想知道这个输入序列长度同发现bug的概率的关系。

现在,一个bug的引发需要一个输入序列串。当这个输入串长度是1的时候,就是说,任意一个随机输入都可能导致bug。这样的软件其实根本不需要测试,直接砍掉重练就好了。因此需要用到monkey测试的程序,一般来说他的bug都是隐藏较深的。假如引发一个bug的输入序列长度是L。则问题就是,一个随机生成的输入序列Input(monkey),它的长度Lm,与引发一个bug所需要的输入序列Input(bug)的长度Lb的关系。

这又涉及到一个问题,就是可能的输入集合。现在我们只考虑手机触屏的话,一台640*320屏幕的手机,触屏输入集合是18万之多。但是考虑到一般的软件都使用控件,而控件一般不会太小而导致用户点击困难,可以用最小能够点击的控件大小作为单位,把屏幕切分开来,每个单元作为一个可能的输入。这就大大减小了输入集合。我们以32*32作为单元的话,一个屏幕会有200个单元格。为照顾一般性,我们设输入集合拥有a个元素。则一个长度为Lm的随机输入串,其可能的组合有aLm之多。

现在我们要考虑的是,这样一个长串中,恰好包含了输入串Input(bug)的可能性。这是一个数列求和。
第一项是Input(monkey)的第0个输入开始恰好匹配Input(bug)的概率。
第二项是从第1个输入开始匹配的概率。但还需减去与第一项重叠的部分。也就是从第0个输入开始,和从第1个输入开始,同时匹配的概率。
第三项是从第2个输入开始匹配的概率。还需减去与第一项重叠的部分以及与第二项重叠的部分。然后再补偿第一、第二、第三项同时匹配的概率。
这样的求和非常复杂,并且是否可能出现匹配的情况,其实是受Input(bug)本身的影响。这里我们作为估算,忽略掉重叠带来的误差,最后会得到一个偏乐观的结果。
于是Input(monkey)恰好包含Input(bug)的概率

p < (Lm-Lb)*a-Lb

当Lm远大于Lb时,我们可近似认为Lm与发现bug的概率成正比。于是Lm增长一倍,则p增大一倍。
由于是简单的线性关系,在Lb不变的情况下,只需简单的增大monkey test的测试序列长度,就能尽可能多地发现bug。因此我们可以预期,当Lm足够大的时候,几乎可以把所有Lb < Llimit的bug都找出来。而这个时候,进一步增长monkey test的长度就变得无效了,因为如果找完了Lb < Llimit的bug,想找到新的bug,一定都是Lb > Llimit的,换句话说,就会导致Lb的增大,而正如我们所见Lb是处于公式的指数上的。增大Lm仅能线性增大找到bug的概率,而增大Lb却导致概率指数级下降,最终导致的结果就是进一步增大Lm却很难找到新的bug了。

既然知道这个道理,我想计算出monkey test大概合适的测试长度。得到最大化的利益。我们根据上述讨论,设a = 200。根据经验bug通常有一个Lb <= 3。则如果我们希望发现一个特定bug的概率超过90%,则有

0.9 < p < (Lm - 3)*200-3

则我们的Lm需要7.2M。假如按照我们常用的设置,一秒钟执行两次操作的话,需要跑超过41天。

这个绝望的估计结果主要原因是200个的输入集合太过巨大了。假如我们仅考虑手机键盘操作的12个键的话,我们可以测试到Lb <= 5,其需要测试的步数大概是220K,按照1秒两个操作需要测试约30个小时,这就在可行范围之内了。

[编辑]这里我仅讨论了想要发现一个特定bug的情况。这是由于我们不知道软件究竟有多少个bug,为保守起见,就按照最悲观的方案来。如果想讨论多个bug的情况,例如10个,则我们仅需令10个bug都找不到的概率降低到0.1以下,则发现每个bug的概率约为0.2。注意到这里其实是对数关系,面对指数级数,这个关系不会对Lm的长度有显著贡献。

0.2 < p < (Lm-3)*200-3

将Lm缩短3.5倍,也无非是从41天降低到九天,仍然是不可接受的长度。当然你会说可以增加同时执行测试的手机数量。但是一旦Lb步长增加1,则立刻就会发现这点缩短其实无意义。[/编辑]

由此也可见,monkey test对于过于复杂以及bug隐藏过深的程序几乎不起作用,如需稳定性测试需要另求他途。

初读康德

最近开始读《纯粹理性批判》,德文翻过来的文章读起来非常累。目前仅仅读了序言和第一章。感觉他的一些论断和我自己的一些理解有很大不同。

康德首先把知识分成先验的和经验的。然后又把理性思维分成综合的和分析的。在我看来,先验知识相当于数学中的“公理”,经验知识相当于公理所讨论的对象。综合相当于提出新的公理,而分析相当于从现有公理和定理中推导出新的定理。

为什么我把“公理所讨论的对象”归类于经验知识,是因为,所有理论讨论的对象,都是经验中抽象出来的实在。例如几何中讨论的点、线、面、体,如果没有生活在这个三维世界中,是万难提出来的,又比如自然数,正是“来自于自然”,高等数学的创造来自于对天体动力学研究的需求,等等。数学世界是如此,物理、化学等经验学科就更不必说了。那么先验知识究竟是什么?我认为先验知识是组合经验知识的能力,是从经验中抽取知识的能力。这种能力是天生就存在的。只是在接触到实际的对象之前,这些能力无法发挥作用罢了。

然后,康德希望讨论的是“纯粹理性”,也就是不包含经验知识的理性。当然,他讨论的问题是“纯粹理性”的“批判”,也就是如何验证、如何批判“纯粹理性”得出的结论是否正确这个问题。

由于仅仅读了第一章,我不知道他会如何展开他的批判。但是就我目前的思考来看,“纯粹理性”很可能是不存在的。我不相信不通过经验就能获得任何先验知识。当然,逻辑本身的结构存在于我们的大脑中,这本身是先验的。但是如果没有经验世界的刺激,这些逻辑构造不可能发挥任何作用,也不可能因此获取到任何知识。就如同我前面做的类比,先验知识是理论,而经验知识是对象。如果没有对象,理论缺乏必要的主语和谓语和宾语,本身就无法表达出来。

为什么连谓语都不能确定,我认为,人类所知的任何谓语,都是从经验中获取来的,不论是奔跑飞行这类具体的动词,还是综合、分析这类抽象的动词,如果没有经验世界,是绝对无法由人脑自发产生出来的,因为既然不存在谓词所作用的对象,则这个行为本身也无法成立。

用个计算机术语来说,“纯粹理性”就好象是“纯虚类”,里面的任何操作,任何对象,都是“模板的”,都是“未定义”的。在不使用经验世界的元素将其“特化”之前,抽象类是绝对不能“实例化”的。

我想,康德想追求的这个“纯粹理性”就是“上帝”。但我怀疑,“上帝”是否真的可以使用人类语言描述出来。如果把人类的纯粹理性称之为上帝的话,可以解释非常多事情,同时可以解释为何上帝真名是无法念出的。因为他必须是不包含任何经验知识的存在。而这种存在在人类世界是无法找到的。

借用老子的话来说,“道可道,非常道”。当“上帝”这个概念,用“上帝”这个名词表达出来的时候,它就变成是经验的了,于是失去了他的“纯粹性”,从而不是上帝。“吾不知其名,强名之而曰‘道’”。但是这个概念必须有一个名称,因此用“上帝”来代表他。我不知道康德的纯粹理性批判是否得出我的这个结论,我会抱着这些问题继续读下去,看看作者的意思具体是怎样的,而我的理解是否可以与他的理论相融。