速览FLAnimatedImage库笔记

FLAnimatedImage库很不错,分享下我的学习笔记

一、概述

  • SDWebImage处理网络GIF播放,是在FLAnimatedImage库基础上做的。

  • FLAnimatedImage库就主要的类是:FLAnimatedImage类FLAnimatedImageView类FLAnimatedImage看做个生产者,负责将GIF图片转换成一帧帧图片,提供给FLAnimatedImageView;后者可以看做是消费者,将一帧帧图片刷新显示到屏幕中。

  • FLAnimatedImage库中涉及一些线程安全weakProxy内存警告处理还是很值得借鉴的。

二、FLAnimatedImage 类

重要的两个方法,分别是initWithAnimatedGIFData:方法imageLazilyCachedAtIndex:方法

1、initWithAnimatedGIFData: 方法
  • 其一、获取GIF图片的信息,包括图片大小、图片帧数、首帧图片、每帧图片的属性信息(播放时间)、循环播放次数,最大缓存帧个数等。其中最大缓存帧个数是根据图片大小获得 或者 手动指定。

  • 其二、设置个弱代理(weakProxy),用来防止循环引用用的。

  • 其三、将自己添加allAnimatedImagesWeak对象中(添加操作使用@synchronized保证线程安全),这是一个NSHashTable对象,当哈希表中的FLAnimatedImage对象的引用计数是0时,表中会清除该对象。

    说明:线程同步中,一说同步锁,很多人直觉反应,性能最差,不用;但是FLAnimatedImage库和SDWebImage都在用,应该是其使用方便所致;其本身实现很有意思,利用对象的内存地址去获取互斥锁;还添加了异常处理,如果同步某对象抛出异常时,还会释放掉锁。

2、imageLazilyCachedAtIndex:方法
  • 通过imageLazilyCachedAtIndex:方法将帧每一帧图片给FLAnimatedImageView对象展示。

  • 根据当前的帧号(index)和当前缓存帧数(frameCacheSizeCurrent)获取接下来需要缓存的帧图片,并异步绘制出位图,并保存在内存缓存中。

    说明:都是提前绘制后面需要展示的图片,第一帧图片在init方法时就获得了。

3、内存警告的处理
  • 位图比较大,将位图缓存在内存中,即使FLAnimatedImage做了优化,当时GIF较大,或多张GIF同时播放,内存的压力会很大,很大概率收到内存警告。

  • allAnimatedImagesWeak保存所有的FLAnimatedImage对象,收到内存警告时,通知所有FLAnimatedImage对象,当前只允许缓存一帧图片;如果接下来没有新的内存警告,慢慢恢复到定义的缓存的图片帧数;但是如果内存警告次数超过三次,以后只能缓存一帧图片。

  • 在内存警告处理中,使用了些延迟执行的方法,防止循环引用,使用了弱代理(weakProxy)。

  • weakProxy是FLWeakProxy 对象;FLWeakProxy是NSProxy类子类,持有一个 weak 对象的代理,利用消息转发机制将消息的处理交给weak 对象;用来避免循环引用的一种方法,具体可以看我之前写的解决NSTimer/CADisplayLink的循环引用

三、FLAnimatedImageView

  • CADispalyLink是个和屏幕刷新率相同的定时器,将帧图片刷新显示在屏幕上。

  • 使用FLWeakProxy这类弱引用代理,打破循环引用;

  • 多个处理器情况下,CADispalyLink对象默认以NSRunLoopCommonModes添加到Runloop上,避免因为列表滑动不刷新显示。

  • CADispalyLink的刷新时间是:每帧图片播放时间的最大公约数;因为不能保证每帧图片的播放时间是一样的,取他们的最大公约数,使得每一帧都尽可能获得其播放时间。当然也可以暴力使用平均停留时间作为CADispalyLink的刷新时间。

四、后记

  • 这是之前阅读FLAnimatedImage库的笔记整理,一是本着学习的目的,二是为了评估 在列表页缩放裁剪播放n张GIF图的需求的可行性;

  • GIF图片的裁剪和合成小GIF图的重任交给客户端,本身就很冒险;裁剪过程中,GIF图片的帧数多,GIF图片多,对内存的挑战是很大的,在列表中下发小的GIF图播放比较好(后台表示压力大)。


代码自动生成笔记

iOS开发中,使用脚本语言(Ruby或Python)生成些Objective-C代码,提高代码生产力。

一、概述

  • 使用脚本生成简单的Objective-C代码,这件工作并不复杂;

  • 先介绍Xcode自动运行脚本相关设置自动添加类文件到项目

  • 最后介绍一个使用Ruby生成Objective-C代码的实例

二、Xcode自动运行脚本

目标:在运行项目时自动运行脚本。考虑到可能在项目中引入比较多的脚本,建议在项目的根路径下新建一个存放脚本的文件夹;在这个文件夹中,集中管理脚本。推荐两个比较常见的设置自动运行脚本的办法。

方法1:External Build System(外部编译系统)

具体步骤分为以下 5 步

1)新建External Build System

在工程下添加一个target:选择Cross-platform > Other > External Build System,取名为CodeGenerator。

2)新建shell脚本文件

在工程的根路径下新建一个ToolScripts文件夹,新建脚本文件,名为start.sh

3)设置CodeGenerator

选择Targets > CodeGenerator >Info > External Build Tool Configuration,Build Tools中填入start.sh所在的路径;Directory中填入存放所有脚本的文件夹,本例中是ToolScripts所在的位置。如果编译失败,请检查这里Directory的设置,修改成对应的位置。

4)添加运行依赖

选择Targets > QSRunScriptDemo > Build Phase > Target Dependencies,添加运行依赖。

5)编译运行

编译运行,选择Show the report navigator -> build,可以看到对应的日志输出。

方法2:项目中直接添加脚本执行

具体步骤分为以下 4 步

1)新建shell脚本文件

在工程的根路径下新建一个ToolScripts文件夹,新建脚本文件,名为start.sh,这步骤和方法1一样。

2)添加Run Script

选择Targets > QSRunScriptDemo > Build Phase中添加 New Run Script Phase 即可。

3)设置Run Script

在Run Script中设置执行start.sh命令即可。

4)编译运行

编译运行,选择Show the report navigator -> build,可以看到对应的日志输出。

3、小结
  • 我们并没有直接设置执行Ruby 或 Python 代码,是因为这两类脚本可能并存在项目中,编译前执行shell脚本,在shell脚本中定义执行Ruby 或 Python脚本。

  • 接下来介绍如何将生成的类文件自动添加到项目中。

三、自动添加类文件到项目

目标:将通过Ruby 脚本来生成类文件自动添加到项目。(手动添加也可以,但是我想偷点懒)

1、准备工作
  • 在Xcode项目中,我们手动添加和删除类文件,其实是在修改project.pbxproj文件。

  • Cocoapods提供了一个可以创建和修改 Xcode 工程文件的工具:Xcodeproj,该工具中有Ruby 的开源库xcodeproj,可以帮助我们实现自动添加类文件到项目。(Cocoapods也是通过它自动添加文件到到项目)

  • 我们可以通过安装Cocoapods实现安装ruby的xcodeproj库;当然也可以直接安装,执行命令gem install xcodeproj即可。无论是选择哪种方案,都需要注意使用正确的ruby源。

  • 因为一直在使用Cocoapods,恰好在升级系统到macOS 10.13后,发现使用xcodeproj库有问题,原因是Cocoapods版本过旧,需要升级Cocoapods,升级过程中,要注意现在“taobao Gems 源已停止维护”,使用 ruby-china 提供镜像服务。下面以安装Cocoapods为例:

    sudo gem update --system    //升级gem,很有必要
    
    gem sources --remove https://rubygems.org/
    gem sources --remove https://ruby.taobao.org/       //如果安装了淘宝的镜像
    gem sources -a https://gems.ruby-china.org/          //保证只使用ruby-china镜像
    gem sources -l                                       //查看更换源结果,保证只有ruby-china镜像
    
    sudo gem uninstall cocoapods                           //如果有旧的CocoaPods,先卸载
    sudo gem install -n /usr/local/bin cocoapods          //安装最新的
    
2、主要代码实现
#添加类文件到项目
def add_files_to_projects(oc_file_paths,group_path)

  #1、获取.xcodeproj
  xcodeproj_path = File.expand_path File.join(__dir__, "../", "QSRunScriptDemo.xcodeproj")
  project = Xcodeproj::Project.open(xcodeproj_path)

  #2、获取target
  target = project.targets.first

  #3、创建group
  generator_group = project.main_group.find_subpath(group_path, true)
  generator_group.clear
  generator_group.set_source_tree('SOURCE_ROOT')

  #4、向group中添加文件
  file_refs = []
  oc_file_paths.each do |f|
      unless generator_group.find_file_by_path(f)
          file_ref = generator_group.new_reference(f)
          file_refs << file_ref
      end
  end
  #5、将文件加入 Build Phases
  target.add_file_references(file_refs)

  #6、保存 project
  project.save

end

四、自动生成Objective-C代码实例

#####1、输入:txt模板(文件名user_model.txt) #####

varchar,name
int,age
varchar,address
varchar,password

说明:文件名和OC类名相关,文本内每一行描述属性类型和名称。

#####2、执行脚本(自动执行) #####

在start.sh文件中输入执行脚本命令

ruby generator_model/model_generator.rb

#####3、输出:OC类文件及代码 #####

// QSUserModel.h文件
// Generated by Ruby.  DO NOT EDIT!
//  Copyright © 2017年 shaoqing. All rights reserved.

#import <UIKit/UIKit.h>

@interface QSUserModel : NSObject

@property (nonatomic,copy)NSString *name;
@property (nonatomic,assign)NSInteger age;
@property (nonatomic,copy)NSString *address;
@property (nonatomic,copy)NSString *password;

@end

//QSUserModel.m文件
// Generated by Ruby.  DO NOT EDIT!
//  Copyright © 2017年 shaoqing. All rights reserved.

#import "QSUserModel.h"
@implementation QSUserModel

@end

说明:类名QSUserModel,根据模板文件名而来;属性定义根据模板内容而来。

#####4、总结 #####

  • 通过ruby脚本,实现了根据模板实现了自动生成Model类自动添加文件到项目

  • 在iOS项目中,脚本生成代码这类做法,不单单是帮助iOS开发减少重复工作量,从更广的范围来看,其实是为了让跨团队,跨部门的合作更加高效,更好地达成目标。

  • 本例只是一个简单例子,主要为了说清思路,设计比较粗糙;在实际工程中,模板中需要定义的信息很多,txt并不是一个很好的选择;ruby脚本还需要更多完善的地方。


iOS图片优化小记

根据我自己的经验,说一下我对图片优化的理解。

一、他山之石

1、图片的加载步骤

从磁盘中加载图片,并通过UIImageVIew显示在屏幕上,需要经过以下步骤:

1、从磁盘拷贝数据到内核缓冲区
2、从内核缓冲区复制数据到用户空间
3、生成UIImageView,把图像数据赋值给UIImageView
4、如果图像数据为未解码的PNG/JPG,解码为位图数据
5、CATransaction捕获到UIImageView layer树的变化
6、主线程Runloop提交CATransaction,开始进行图像渲染
  6.1 如果数据没有字节对齐,Core Animation会再拷贝一份数据,进行字节对齐。
  6.2 GPU处理位图数据,进行渲染。

说明1:这部分摘抄自iOS图片加载速度极限优化—FastImageCache解析,它介绍了FastImageCache极限优化的手段。

2、FastImageCache的优化手段

优化有三

  • 使用mmap这样的内存映射方案,将文件映射进内存;比普通的read()读取少了一次内存拷贝(内核缓存区拷贝到用户内存空间)

  • 图像子线程解码,将耗时的解码工作从主线程移到了子线程; 缓存解码后的位图数据到磁盘,避免重复解码。

  • 字节对齐;生成字节对齐的数据,防止CoreAnimation在渲染时再拷贝一份数据。

3、FastImageCache的缺点

缺点有三

  • 位图数据缓要保存在磁盘,占据空间大

  • 接口不友好,需预定义好缓存的图像尺寸。FastImageCache无法像SDWebImage那样无缝接入UIImageView。

  • FastImageCache库已经快4年没有新的版本更新,很多项目使用SDWebImage这样的活跃库。

4、我的拙见
  • 在项目中,为了优化,直接颠覆原来的,风险很大;但是借鉴其他好的方法,对原有的进行优化,是个值得做的事情。

  • 图片的优化,绕不开图片的解码缓存下载三大件。

  • 优化工作需要考虑 低代价高回报 这件事。

二、本地图片加载的优化

1、优化的目的

目的有三:

  • 减少App包的大小;图片体积的暴增最大原因之一;可能是新业务引入大量图片资源。

  • 提高图片加载的性能;使用UIImage的方法imageNamed:方法加载图片,默认在主线程解码会拖累你的APP性能。可以考虑将图片的解码放在子线程

  • 图像不失真;JPG或PNG格式的图片展示时,可能会失真。PNG比JPG好一些。

2、优化手段

手段有三:

  • PNG图片虽好不作为唯一的选择,本地图片可以使用其他方案代替。如矢量图PDFICONFONTCG绘制。(像运营类色彩比较丰富的图片,优先使用PNG图片)

  • 缓存的使用。无论是图片的解码,还是绘制CG图片,都是个耗时耗CPU的操作,将这些结果缓存(到内存中)下来,避免重复解码和绘制,是个好的选择,这里推荐YYCache这样的方案(线程安全、LRU淘汰算法)。

  • 及时清理旧的图片资源。使用类似LSUnusedResources 清理旧的图片文件。

    说明:导入项目中的图片记得压缩。

三、网络图片的优化

相比较本地图片,它面临复杂的网络环境、复杂的图片来源。

1、优化目的

目的有三:

  • 异步下载,不阻塞主线程操作;

  • 图片快速加载显示

  • 良好的接口,让使用者用的开心 ;(不必关心复杂的网络环境、图片加载性能等)

2、优化手段

手段有三:

  • 使用优秀的第三方库,如SDWebImage。关于SDWebImage我在简书上写了文章SDWebImage源码理解,可以看看。

  • 使用压缩比高,质量小,失真小的图片格式,如Google的Webp、Tencent的SharpP格式,这两图片文件小,很适合在网络中传输,消耗的流量少,下载快,解码快(SharpP和webp的编码慢)。

  • 结合业务需要优化图片;网络环境获取图片比较复杂,且难以预测;对下载下来的图片进行裁剪(按显示的目标大小是否需要圆角描边阴影等),能很大程度上避免图片像素不对齐(图片大小和显示大小不一致)、像素混合(图层透明)、离屏渲染(圆角和阴影等)等问题。

    参考图片流量节省大杀器:基于 CDN 的 sharpP 自适应图片技术实践

3、一些小的优化手段
  • 要从网络中获取的重要图片(如广告、运营相关的图片),要先校验图片的完整性,校验通过才去真正处理并显示,否则不处理。

  • 纯显示的视图,CALayer代替UIImageVIew,可以减少GPU计算(去透明/像素对齐)

End


浅谈代码混淆

代码混淆的目的,是为了对抗反编译

一、反编译工具

虽然AppleStore会对App加壳保护,但是在越狱手机上,破壳App并非不可能,失去保护的App,可以利用工具去反编译;常见的反编译工具有: class-dumpHopper Disassembler

1、class-dump简介
  • 下载并安装class-dump,将class-dump复制到/usr/local/bin/目录下,使用下面命令可以反编译出头文件

    class-dump -H xxx.app -o /Users/xxxx/Desktop/heads 
    
2、Hopper Disassembler简介
  • 下载并安装Hopper Disassembler

    //破解办法:
    将Hopper Disassembler v4.app移动至应用程序文件夹
    双击HopperV4Patcher,将应用程序中的Hopper Disassembler v4.app拖进HopperV4Patcher完成注册
    
  • 解压缩App文件,获得二进制文件,直接拖进Hopper Disassembler,选择点右上角的if(b)f(x),就可以看到类名、方法、伪代码,字符串等信息。

说明:开发中,我们可以利用Xcode在debug下编译后产生的(.app)文件, 作为反编译练习的原材料。这个文件放在/Users/user name/Library/Developer/Xcode/DerivedData/ xx Project Name/Build/Products/Debug-iphonesimulator

二、代码混淆

1、简介
  • 代码混淆: 主要是混淆重要的 方法名类名字符串常量。(大面积的混淆没有太大意义),手段主要是利用宏定义。

  • 宏定义的关键就是替换,将A替换成B;A可以是见名知意函数名、类名或字符串常量,B可以是百思不得其解的名字或 函数 等等。

  • 利用宏定义,做到扰乱耳目,迷惑他人的目的。

    说明:具体宏方便的知识,可以参考宏定义的黑魔法 - 宏菜鸟起飞手册

2、具体方案
  • 混淆方法名 和 类名

    单段的selector,如func: ,可以通过#define func 来实现字符串替换。
    多段的selector,如a:b:c: ,可以通过分别#define a 、b、c 来实现字符串替换。
    
  • 混淆字符串常量

    代码中使用的都是密文(密文通过脚本生成);OC中定义对应的 密文转成明文 方法;这样编译出来的只有字符串常量只有密文,反编译者难以根据字符串信息,去推敲逻辑。

3、方案实现
  • 脚本实现方法名 、 类名 和 字符串常量的混淆,输入是想要混淆的对象,输出是 #define 要混淆的对象 混淆内容。混淆内容可以是随机字符串等难以理解的信息。
  • 考虑到方便自己、恶心别人的目的,我们将混淆结果合并在一个.h中,在工程Prefix.pch的最前面#import这个.h文件。不导入也可以编译、导入则实现混淆。

脚本实现如下

#!/usr/bin/python 
# -*- coding: utf-8 -*-

import sys
import os
from random import Random
import time;  
import base64

#
# 执行脚本命令:python Confuse.py
#

# ***** 工具类 *****

class Util(object):
    def __init__(self, arg):
        super(Util, self).__init__()
        self.arg = arg

    # 生成随机串
    @classmethod
    def randomString(self,randomlength=16):

        str = ''
        chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz'
        length = len(chars) - 1
        random = Random()
        for i in range(randomlength):
            str += chars[random.randint(0, length)]
        return str

    @classmethod
    def loadFileCotent(self,filePath):

        contents = []
        with open(filePath) as f:
            line = f.readline()
            while line:
                contents.append(line.rstrip('\n')) 
                line = f.readline()
        return contents

    #加密(可以换成你想要的加密方法)
    @classmethod
    def encrypt(self,content):
        res = ' '.join([bin(ord(c)).replace('0b', '') for c in content])
        return base64.b64encode(res)

    # 解密 (需要OC代码实现一份)
    @classmethod
    def decrypt(self,content):
        return ''.join([chr(i) for i in [int(b, 2) for b in base64.b64decode(content).split(' ')]])

#混淆函数名和方法名
def confuseFunc(filePath):

    originContents = Util.loadFileCotent(filePath)
    confuseRes = ''
    for i in range(0,len(originContents)):
        outStr = "#define " + originContents[i] + " " + Util.randomString() + "\n"
        confuseRes = confuseRes + outStr

    #返回混淆结果
    return confuseRes

#混淆字符串
def confuseText(filePath):

    originContents = Util.loadFileCotent(filePath)
    confuseRes = ''
    for i in range(0,len(originContents)):
        text = originContents[i]
        text_encrypted = Util.encrypt(text)
        # print "decrypt",Util.decrypt(text_encrypted)
        outStr = "#define kKBParameter%sKey  QSString(@\"%s\")\n" %(text.capitalize(),text_encrypted)
        confuseRes = confuseRes + outStr

    #返回混淆结果
    return confuseRes

confuse_file_name =  "QSConfuseHeader"

if __name__ == '__main__':

    fileContent = '''//
//  ''' + confuse_file_name + '''
//  XXProject
//
//  Created on''' + time.asctime( time.localtime(time.time()) ) + '''!\n 
#ifndef ''' + confuse_file_name + '''_h
#define ''' + confuse_file_name + '''_h\n\n'''

    confuseFuncContent = confuseFunc("func_class.txt")
    if len(confuseFuncContent) > 0:
        fileContent += "\n//函数混淆\n" + confuseFuncContent

    confuseTextContent = confuseText("string.txt")
    if len(confuseTextContent) > 0:
        fileContent += "\n//字符串混淆\n" + confuseTextContent

    endFileContent = "\n\n#endif /* " + confuse_file_name + "_h */"

    if len(fileContent) > 0:
        fileContent+= endFileContent
        print fileContent
        # 打开一个文件
        outputFile = open(confuse_file_name + ".h", "wb")
        outputFile.write(fileContent)
        # 关闭打开的文件
        outputFile.close()


说明1:目前需要混淆内容放在txt文件(需要混淆函数名和类名放在func_class.txt,需要混淆字符串常量放在string.txt中);进一步做法是,给需要混淆的字符串打上标记,然后用脚本搜索这些需要混淆的对象,实现混淆;

说明2:代码混淆需要考虑代码的可读性,建议做法是,对应混淆后的结果使用宏定义替换,类似于

//QSString(x)是个宏方法,定义是解密实现,在OC中实现
#define kKBParameterGoodKey  QSString(@"MTEwMDExMSAxMTAxMTExIDExMDExMTEgMTEwMDEwMA==")

说明3:在脚本中,加密方法并非真正的加密,只是将字符串转成二进制,再Base64编码而已;这个根据自己业务需要增加真正的加密算法,对应的解密算法放在OC中实现。

其他参考iOS安全攻防(二十三):Objective-C代码混淆

三、关键逻辑混淆

提供两种方案

方案1
方案2
  • 使用C/C++语言实现逻辑,这里利用到Objective-C 与 C/C++混编的知识。下面以C++为例,混编最需要注意的是OC 调用 C++C++ 调用 OC

  • OC 调用 C++:这种情况比较简单,因为编译器做了优化,把 .m 文件直接改为 .mm ,然后直接用就好了。

  • C++ 调用 OC:分为两种情况:1)完全的 C++ ,实现文件以 .cpp 结尾,里面不允许直接OC的方法;通过编写接口,间接调用 OC 方法,实际上 C++ 调用的是OC编译过后的 .o 文件;2)部分 C++ 部分 OC ,实现文件以 .mm 结尾,里面允许直接使用 OC 方法。

总结

  • 代码混淆只是提高了破解的门槛和难度,不能百分百保证代码安全,但是如果不做这些,你的核心业务无疑在裸奔,这是很危险的事情。

浅谈AES

一、概述

  • 对称加密非对称加密要快。频繁的数据加密,优先考虑使用对称加密

  • DES -> 3DES -> AES(Advanced Encryption Standard,高级加密标准);目前对称加密使用AES为佳,它已经取代DES和3DES对称算法了。

  • AES是项目中使用较多的对称加密算法,本文简单介绍下AES。

二、AES算法的重要组成

1、密钥长度(Key Size)

AES算法规定密钥长度有三种:128bits、192bits和256 bits。其中128 bits的key可以满足大部分业务需求

2、加密模式(Cipher Mode)

AES属于块加密(Block Cipher),加密数据块(Block)必须为128bits(16字节);加密模式主要有以下六种模式:

1) ECB (Electronic Codebook Book,电子密码本模式)

  • 简介:将数据分成若干块,分别对每块进行加密,最后将这些密文块组成最后的加密结果

  • 说明:最简单的模式,安全性较低。因为ECB的明文分组与密文分组是一一对应的关系,如果明文中存在多个相同的明文分组,其对应的密文分组也是相同的;不推荐使用

2) CBC(Cipher Block Chaining,密码分组链接模式)

  • 简介:将数据分块后,每一块数据与上一块密文XOR(异或)后,再进行加密;

  • 说明:比较推荐使用的模式(但Apple已经不推荐使用CBC模式,建议使用GCM模式)。

3) CTR(Counter,计数器模式)

  • 简介:通过将逐次累加的计数器进行加密来生成密钥流的流密码;最终的密文分组是通过将计数器加密得到的比特序列,与明文分组进行XOR得到的。

  • 说明:比较推荐使用的模式

4) CFB(Cipher FeedBack,密码反馈模式)

  • 简介:和CBC类似,加密模式和CBC类似,但其可以将块密码变为自同步的流密码。

  • 说明:不推荐使用的模式

5) OFB(Output FeedBack, 输出反馈模式)

  • 简介:将块密码变成同步的流密码,它产生密钥流的块,然后将其与明文块进行异或,得到密文。

  • 说明:不推荐使用的模式

6) GCM(Galois/Counter Mode,)

  • 简介:GCM中的G就是指GMAC(消息验证码),C就是指CTR(计数器模式)

  • 说明:GCM可以提供对消息的加密和完整性校验,AES模式最好选择。

总结: 推荐先后顺序:GCM > CTR > CBC;详细内容可以参考Block cipher modeAES-GCM加密算法

3、填充方式(Padding)

1)块加密要求对特定长度的数据块进行加密,因此CBC模式ECB模式需要在最后一数据块加密前进行数据填充。(CFB,OFB和CTR模式由于与key进行加密操作的是上一块加密后的密文,因此不需要对最后一段明文进行填充)。

2)iOS SDK中提供了PKCS7Padding,Java提供了PKCS5Padding。原则上PKCS5Padding限制了填充的Block Size为8 字节(大于8 字节,填充和PKCS7Padding相同),而PKCS7Padding的Block Size可以为1到255字节; 填充值的算法(需要填充x个字节,填充的值就是x)都是一样的:

value = k - (l mod k)  ,K = 块大小,l = 数据长度;假设k是16,I是5,那么value是11,也就是说,需要填充11个byte的 0x0B(十进制是11)

3)因为AES加密数据块Block Size规定为16字节(128bits), 大于8 字节(64 bits), 所以PKCS5Padding和PKCS7Padding填充效果是一样的。

说明:数据块的长度如果是16倍数,padding长度为16,padding值为0x10。即在输入后面补齐16字节的0x10;如果数据块的长度不是16倍数,padding长度为16-L%16,padding值为16-L%16。即在输入后面补齐16-L%16的字节,值为16-L%16。

4)平时使用最多的是PKCS7Padding,NoPadding、ISO10126Padding和ZeroPadding忽略不提。

4、初始向量(Initialization Vector)

CBC、CFB和OFB这些加密模式需要传入一个初始向量,其大小与Block Size相等;当不传入初始向量时,系统将默认使用一个全0的初始向量。

三、AES算法Python实现

说明:使用Python的加密包pycrypto (下载后,执行python setup.py build + python setup.py install),实现AES的ECB、CBC、CTR、CFB和OFB模式下的加密和解密;定义一个PaddingHelper类,实现PKCS7Padding;

#!/usr/bin/python 
# -*- coding:utf-8 -*-

import sys
import os
import base64
from Crypto.Cipher import AES
from Crypto.Util import Counter

# ***** Padding Helper For AES *****

class  PaddingHelper(object):
    """docstring for  Padding Helper"""
    def __init__(self, arg):
        super(PaddingHelper, self).__init__()
        self.arg = arg

    # 保证keyStr的长度是16
    @classmethod
    def validKeyStr(self,keyStr):    
        keySize = 16
        if len(keyStr) < keySize:
            zeroCount = keySize - len(keyStr) % keySize  
            for i in range(0, zeroCount):  
                keyStr = keyStr + '\0' 
        else:
            return keyStr[0:keySize]
        return keyStr  


    # 实现PKCS7Padding/PKCS7Padding
    @classmethod
    def PKCS7Padding(self,text,blockSize=16):    
        count = len(text)    
        mod_num = count % blockSize
        add_num = blockSize - mod_num
        return text + chr(add_num) * add_num    

    @classmethod
    def MovePKCS7Pading(self,text,blockSize=16):
        lastChar = text[-1]    
        paddingLen = ord(lastChar)
        # 截取最后一部分填充快
        lastChunk = text[-paddingLen:]

        if lastChunk == chr(paddingLen) * paddingLen:
            # 移除填充部分
           return text[:-paddingLen]
        return text     

# AES PSK7/PSK5填充
class AESUtil():
    def __init__(self, key, model):

        self.key = PaddingHelper.validKeyStr(key)
        self.mode = model
        self.iv = key   #初始向量初始化k,可以是别的值,但必须16字节

    #加密函数,如果text不是16的倍数,那就补足为16的倍数
    def encrypt(self, text):

        if self.mode == AES.MODE_CTR:
             # Create new AES CTR object #
            cryptor = AES.new(self.key, self.mode, counter=Counter.new(128))
        else:
            cryptor = AES.new(self.key, self.mode, self.iv)

        text = PaddingHelper.PKCS7Padding(text,16)
        ciphertext = base64.b64encode(cryptor.encrypt(text))
        return ciphertext

    #解密后,去掉补足的空格用strip() 去掉
    def decrypt(self, text):   
        if self.mode == AES.MODE_CTR:
             # Create new AES CTR object #
            cryptor = AES.new(self.key, self.mode, counter=Counter.new(128))
        else:    
            cryptor = AES.new(self.key, self.mode, self.iv)

        plainText = cryptor.decrypt(base64.b64decode(text))
        return PaddingHelper.MovePKCS7Pading(plainText)

if __name__ == '__main__':

    encrykey = "0123456789abcdef"
    # plainText = '1234567891234567'
    plainText = 'hello world'

    dict = {"CBC模式": AES.MODE_CBC, 
            "CFB模式": AES.MODE_CFB, 
            "CTR模式": AES.MODE_CTR,
            "ECB模式": AES.MODE_ECB,
            "OFB模式": AES.MODE_OFB};

    for key, value in dict.iteritems():
        aes = AESUtil(encrykey,value)
        encryptText = aes.encrypt(plainText)
        print "**********%s 加解密*******" %(key)
        print "密文:",encryptText
        print "明文:",aes.decrypt(encryptText)

执行脚本的输出结果如下

**********ECB模式 加解密*******
密文: gWm+1O9JqIdFWcWyANqt5w==
明文: hello world
**********CFB模式 加解密*******
密文: GsZEtzHsqRLRjxfUU6T0Zg==
明文: hello world
**********CBC模式 加解密*******
密文: wem0Upqsl5MBD0Z39jWO/g==
明文: hello world
**********OFB模式 加解密*******
密文: GhcS5HH8im5yy3xtfAywYA==
明文: hello world
**********CTR模式 加解密*******
密文: adUgVHVGk4Y3/LZatTZf6Q==
明文: hello world

四、AES算法使用说明

  • 对AES加密的结果,要使用Base64编码。因为其加密结果得到的字符串可能有不可见字符,保存和传输都有问题;把数据做Base64编码,统统变成可见字符。(Base64编码 将二进制流中每6个bit一组表示数据, 不足的部分补零,每两个0 用 一个 = 表示,把含有不可见字符串的信息用可见字符串表示出来 );对应的AES解密时,先要进行Base64解码

  • AES有很明显的优势:安全加解密快(AES比RSA的加解密快1000倍);但是劣势也明显,需重点保护密钥的安全;在实践中推荐组合使用AES*RSA技术。

  • 实践中常见的方案是:1)用AES来加密数据;2)AES的密钥通过伪随机数生成器生成;3)AES的密钥通过RSA公钥加密;4)最终将AES密钥的密文 和 数据的密文交给后台,后台利用对应的策略解密。

  • AES算法流程可以参考密码算法详解——AES


浅谈数据加密

导语:本文介绍常用的数据加密方式(不仅仅局限在iOS中使用)。

一、概述

加密是用来避免攻击这盗取信息的一种手段,加密算法主要有三类:Hash算法对称加密算法非对称加密算法

1、Hash算法
  • Hash算法 把一个任意长度的字节串映射 为一定长度的十六进制的数字串;这个数字串称为哈希值

  • Hash算法一种单向算法,不能通过这个Hash值重新获得目标信息;一般用来存储不可还原的密码检验数据的完整性,常见的Hash算法有MD2、MD4、MD5、HAVAL、SHA

  • 重点说明:MD2、MD4、MD5还有SHA-1都已经被碰撞攻击突破,Apple已经移除对MD5和SHA-1签名证书的信任,对于安全性较高的数据,建议使用SHA-2 Family 或其他能避免碰撞攻击的算法。

2、对称加密算法
  • 对称加密算法是指:加密和解密使用相同密钥的加密算法。对称加密算法的加解密的速度快,通常在大量数据加密时使用。

  • 常见的对称加密算法有DES算法,3DES算法,TDEA算法,Blowfish算法,RC5算法,IDEA算法和AES算法。

  • 重点说明:DES 、3DES 已经被AES(Advanced Encryption Standard,高级加密标准)替代了;AES-ECB(电子密码本模式)最简单,安全性差,不建议使用,可以使用AES-CBCAES-CTRAES-GCM(Apple推荐的);对称密钥的安全是必须要考虑并且解决的事情。

3、非对称加密
  • 非对称加密算法是指:加密和解密使用不同密钥的加密算法,也称为公私钥加密,适合对少量数据的加密。

  • 非对称加密算法的公钥是公开的,秘钥是自己保存的,比对称加密有更好的安全性,但是加解密速度要远远慢于对称加密;常见的非对称加密算法有:RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)等。

  • 重点说明RSA是使用最广的非对称加密算法,但是小于1024位的RSA私钥很容易被分解攻击,出于安全的考虑,建议在项目中使用大于等于2048位的RSA私钥。

二、总结

  • 大量数据(较大量的上报信息)加密,建议使用AES;对于少量数据(手机号、邮箱等)加密,建议使用RSA;对于比较重要的资源下载(如运营活动图片、配置数据等),对下载下来的数据,要校验hash值,防止数据被篡改,保证数据的完整性。

  • 对于核心的业务功能的数据,仅仅靠一种加密算法是不够的,综合利用两三种加解密算法,实现业务目标的同时,提高被破解的难度


使用hexo搭建个人博客

一、Hexo + Github

  • 2018年以前在简书上谢谢博客,2018年之后技术博客更新到这里

  • Hexo是个不错的博客框架,结合Github,可以实现低成本(学习成本和经济成本) 、快速实现搭建个人博客的梦想。主要分以下几个步骤

1、安装和建站
//安装npm
brew install npm

//npm 安装 Hexo。
npm install hexo-cli -g

//初始化hexo文件夹
hexo init <folder>

//进入文件夹
cd <folder>

//新建所需要的文件
npm install

新建完成后,指定文件夹的目录如下

.
├── _config.yml (网站的配置信息,在此配置大部分的参数)
├── package.json (应用程序的信息)
├── scaffolds (模版文件夹, 新建文章时,Hexo根据 scaffold 来建立文件)
├── source (资源文件夹)
|   ├── _drafts
|   └── _posts
└── themes (主题文件夹, Hexo 会根据主题来生成静态页面)
2、配置_config.yml
# Site
title: 南华coder的空间
subtitle:
description: 曾经仗剑走天涯,如今沉醉代码忙
author: 南华coder
language: zh-Hans
timezone:
avatar: /uploads/avatar.jpeg

注意:冒号(:)后要有空格

3、新建文件和本地发布
// layout是文章的布局,默认是post,可以通过修改 _config.yml 中的   
// default_layout 参数来指定默认布局; 默认以标题做为文件名称
hexo new [layout] <title>

//生成静态页面
hexo generate 

//本地发布
hexo server

注意:可以使用 hexo s -g 实现生成页面和本地部署,访问 http://localhost:4000/ 可以看到效果。

4、部署到Github上
# 修改_config.yml文件,添加Github仓库的地址
deploy:
  type: git
  repo: https://github.com/buaa0300/buaa0300.github.io.git
  branch: master

 // 生成页面
 hexo g 或 hexo generate

 // 部署
 hexo d 或 hexo deploy

说明:可以使用hexo g -d 实现生成页面和部署

5、主题

Hexo还提供了许多主题,让你的页面看起来更漂亮,我选择比较大众化的hexo-theme-next

1
2
3
//下载
cd themes
git clone https://github.com/iissnan/hexo-theme-next
//修改根路径下的_config.yml (网站的配置信息)
theme: hexo-theme-next

//修改主题样式(themes/hexo-theme-next/_config.yml)
# Muse是默认样式
#scheme: Muse  
#scheme: Mist
#scheme: Pisces
scheme: Gemini
6、配置域名
  • 可以直接在远端项目的直接路径下,新增CNAME文件夹,里面输入你申请下来的域名
1
nanhuacoder.top
  • 可以进入hexo本地目录, 进入source路径下,创建CNAME文件,输入你申请下来的域名,然后通过hexo g -d重新生成部署(推荐这种)
7、其他

其他详细内容可以参考Hexo官方文档手把手教你用Hexo+Github 搭建属于自己的博客

二、随笔

1、再见2017
  • 2017年 就这么匆匆而去,毕业工作快满2年了;从17年4月开始,断断续续在简书上写了些技术文章;而今回头看看,文章存在许多不足;最后两个月因为项目太忙(也有懒的原因),几乎断更了。
  • 两年的时间,对于一个iOS Developer来说,不算短的时间,带着2017的遗憾,怀着对2018年的美好憧憬,忐忑前行。
  • 之前的文章大概写了6w字,依然保存在南华coder的简书,新的文章将在这里继续。
2、再见2018
  • 2018年也就匆匆过去了,又年长了一岁,当过面试官、当过实习生导师;

  • 有多人惊异于我的精力充沛,每天上下班通勤3小时,晚上下班晚,到家还偶尔哄哄小宝宝;其实吧,我感觉还好,算是正常吧。