Android手势密码探索

来源:互联网 发布:高能金域名都物业 编辑:程序博客网 时间:2024/05/18 02:25

Android 智能手机在全球市场有着极高的市场占有率,越来越受到广大消费者的青睐。但 Android 作为开源操作系统,且很容易可以获得系统 root 权限,Android 系统的安全问题也是用户和开发者最关心的问题之一。

手势密码作为手机上方便的一种安全保护措施,受到了众多 APP 开发者的青睐,市场上一些金融类 APP 基本都配有手势密码,如下图即为手势绘制过程的一个状态。

手势密码

目前大多数 Android 手机都具有手势锁屏功能,Android 系统自身是带了手势密码功能的,不同的 ROM 厂商做了不同的定制。本文通过Android自身的源码简单介绍手势密码的原理。

Android手势相关类

回忆或者尝试一下用手势解锁 Android 手机的过程:首先用户通过点击九宫格的点连接出一条路径,当手指抬起时,会判断此次连接的点路径是否和设置的相匹配。

在这个过程中,涉及到两个方面(不考虑设置手势时的存储):

  • 手势的绘制
  • 手势的验证/匹配

针对这两个过程,通过 AOSP 查找源码,我们可以发现两个相关类:

  • LockPatternView.java:View类,九宫格手势图形显示的类。
  • LockPatternUtils.java:手势转换、匹配工具类。

本篇文章通过分析这两个类中重要的部分来说明手势表示和绘制的原理。

LockPatternView

该类是 View 的子类,其中定义了整个手势绘制区相关的 View,比如九宫格的点、绘制的路径、View 的状态模式、以及手势监听等。类中覆写了 View 父类的 onDraw 方法,点的选中状态、绘制线条都是实时绘制的。

九宫格中的每个「宫」作为静态内部类定义为 Cell,每个 Cell 包括两个坐标,即行(row)和列(column),row 和 column 的范围均在 [0, 3) 内。这样定义的好处,一是利用矩阵的思想来表示九宫格,二是可以把「row 3 + column*」作为 Cell 的值,用 0~8 共 9 个数字来表示九宫格。比如,绘制的路径是 「L」 型,就可以用「03678」来表示这个路径。

public static final class Cell {        final int row;        final int column;        // keep # objects limited to 9        private static final Cell[][] sCells = createCells();        private static Cell[][] createCells() {            Cell[][] res = new Cell[3][3];            for (int i = 0; i < 3; i++) {                for (int j = 0; j < 3; j++) {                    res[i][j] = new Cell(i, j);                }            }            return res;        }        /**         * @param row The row of the cell.         * @param column The column of the cell.         */        private Cell(int row, int column) {            checkRange(row, column);            this.row = row;            this.column = column;        }        public int getRow() {            return row;        }        public int getColumn() {            return column;        }        public static Cell of(int row, int column) {            checkRange(row, column);            return sCells[row][column];        }        private static void checkRange(int row, int column) {            if (row < 0 || row > 2) {                throw new IllegalArgumentException("row must be in range 0-2");            }            if (column < 0 || column > 2) {                throw new IllegalArgumentException("column must be in range 0-2");            }        }        @Override        public String toString() {            return "(row=" + row + ",clmn=" + column + ")";        }}

手势绘制过程中,一般有三种状态:绘制正确、正在绘制、绘制错误(实际开发可以设置为四种,第四种即锁定状态)。

手势九宫格用「DisplayMode」表示三种显示模式:

public enum DisplayMode {        /**         * The pattern drawn is correct (i.e draw it in a friendly color)         */        Correct,        /**         * Animate the pattern (for demo, and help).         */        Animate,        /**         * The pattern is wrong (i.e draw a foreboding color)         */        Wrong    }

通过三种模式,可以更改绘制手势过程中及结束后手势状态。比如,更改颜色以表示状态:让绘制的过程中,选中的 Cell 和线条用蓝色表示,绘制错误时用红色表示,绘制正确时用绿色表示。

手势绘制过程中通过接口OnPatternListener中的4个监听函数来监听手势开始、结束、清除、添加等操作。接口的定义如下:

public static interface OnPatternListener {        /**         * A new pattern has begun.         */        void onPatternStart();        /**         * The pattern was cleared.         */        void onPatternCleared();        /**         * The user extended the pattern currently being drawn by one cell.         *         * @param pattern The pattern with newly added cell.         */        void onPatternCellAdded(List<Cell> pattern);        /**         * A pattern was detected from the user.         *         * @param pattern The pattern.         */        void onPatternDetected(List<Cell> pattern);    }

从方法名和注释就可以看出每个方法的含义,在此不再赘述。

接下来看下,手势在绘制手势的过程中,View是如何判断手指当前位置是否选中某个 Cell ,以及是否应该把该 Cell 连接入手势。这里需要了解几个函数:

  • getRowHit ( float y )

    用来确定手指当前坐标 (x, y) 位于九宫格的第几

  • getColumnHit (float x )

    用来确定手指当前坐标 (x, y) 位于九宫格的第几

  • checkForNewHit (float x, float y)

    private Cell checkForNewHit(float x, float y) {      final int rowHit = getRowHit(y);      if (rowHit < 0) {          return null;      }      final int columnHit = getColumnHit(x);      if (columnHit < 0) {          return null;      }      if (mPatternDrawLookup[rowHit][columnHit]) {          return null;      }      return Cell.of(rowHit, columnHit);  }

    函数代码很好理解,mPatternDrawLookup 是个全局变量,同样采用矩阵的形式,用于标记九宫格中哪个 Cell 被连接。从 checkForNewHit 中可以看出,已经被连接的 Cell,是不会再被选中的,这也是目前手势密码普遍的做法。如果你需要实现“每个点可以被连接多次”的需求,这部分就需要改动了。

  • detectAndAddHit (float x, float y)

    用来检测并判断手指当前坐标 (x, y) 是否需要添加添加进当前手势中。

    private Cell detectAndAddHit(float x, float y) {      final Cell cell = checkForNewHit(x, y);      if (cell != null) {          // check for gaps in existing pattern          Cell fillInGapCell = null;          final ArrayList<Cell> pattern = mPattern;          if (!pattern.isEmpty()) {              final Cell lastCell = pattern.get(pattern.size() - 1);              int dRow = cell.row - lastCell.row;              int dColumn = cell.column - lastCell.column;              int fillInRow = lastCell.row;              int fillInColumn = lastCell.column;              if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {                  fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);              }              if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {                  fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);              }              fillInGapCell = Cell.of(fillInRow, fillInColumn);          }          if (fillInGapCell != null &&                  !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {              addCellToPattern(fillInGapCell);          }          addCellToPattern(cell);          if (mEnableHapticFeedback) {              performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,                      HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING                      | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);          }          return cell;      }      return null;  }

    首先通过 checkForNewHit 获得当前位置的的 Cell,计算当前Cell 与手势中最后一个 Cell 的行列差值。看其中一段代码

    if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {  fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);}if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {  fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);}fillInGapCell = Cell.of(fillInRow, fillInColumn);

    判断条件是:当前 Cell 与手势中最后一个 Cell 的或者的绝对差值为 2,且其的绝对差值不为1,即两个 Cell 不相邻(包括水平、竖直、45°方向的相邻),获得当前 Cell 与手势中最后一个 Cell 之间的 Cell,如果该 Cell 没有被添加进去过,则添加进手势。

    意思就是说,绘制的手势不会跨过没有添加的点。


前面说到,绘制过程中选中的点和未选中的点是通过覆写 View 的 onDraw 方法实时绘制的。onDraw代码如下:

@Override    protected void onDraw(Canvas canvas) {        final ArrayList<Cell> pattern = mPattern;        final int count = pattern.size();        final boolean[][] drawLookup = mPatternDrawLookup;        if (mPatternDisplayMode == DisplayMode.Animate) {            // figure out which circles to draw            // + 1 so we pause on complete pattern            final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;            final int spotInCycle = (int) (SystemClock.elapsedRealtime() -                    mAnimatingPeriodStart) % oneCycle;            final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;            clearPatternDrawLookup();            for (int i = 0; i < numCircles; i++) {                final Cell cell = pattern.get(i);                drawLookup[cell.getRow()][cell.getColumn()] = true;            }            // figure out in progress portion of ghosting line            final boolean needToUpdateInProgressPoint = numCircles > 0                    && numCircles < count;            if (needToUpdateInProgressPoint) {                final float percentageOfNextCircle =                        ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /                                MILLIS_PER_CIRCLE_ANIMATING;                final Cell currentCell = pattern.get(numCircles - 1);                final float centerX = getCenterXForColumn(currentCell.column);                final float centerY = getCenterYForRow(currentCell.row);                final Cell nextCell = pattern.get(numCircles);                final float dx = percentageOfNextCircle *                        (getCenterXForColumn(nextCell.column) - centerX);                final float dy = percentageOfNextCircle *                        (getCenterYForRow(nextCell.row) - centerY);                mInProgressX = centerX + dx;                mInProgressY = centerY + dy;            }            // TODO: Infinite loop here...            invalidate();        }        final Path currentPath = mCurrentPath;        currentPath.rewind();        // draw the circles        for (int i = 0; i < 3; i++) {            float centerY = getCenterYForRow(i);            for (int j = 0; j < 3; j++) {                CellState cellState = mCellStates[i][j];                float centerX = getCenterXForColumn(j);                float translationY = cellState.translationY;                if (isHardwareAccelerated() && cellState.hwAnimating) {                    DisplayListCanvas displayListCanvas = (DisplayListCanvas) canvas;                    displayListCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY,                            cellState.hwRadius, cellState.hwPaint);                } else {                    drawCircle(canvas, (int) centerX, (int) centerY + translationY,                            cellState.radius, drawLookup[i][j], cellState.alpha);                }            }        }        // TODO: the path should be created and cached every time we hit-detect a cell        // only the last segment of the path should be computed here        // draw the path of the pattern (unless we are in stealth mode)        final boolean drawPath = !mInStealthMode;        if (drawPath) {            mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));            boolean anyCircles = false;            float lastX = 0f;            float lastY = 0f;            for (int i = 0; i < count; i++) {                Cell cell = pattern.get(i);                // only draw the part of the pattern stored in                // the lookup table (this is only different in the case                // of animation).                if (!drawLookup[cell.row][cell.column]) {                    break;                }                anyCircles = true;                float centerX = getCenterXForColumn(cell.column);                float centerY = getCenterYForRow(cell.row);                if (i != 0) {                    CellState state = mCellStates[cell.row][cell.column];                    currentPath.rewind();                    currentPath.moveTo(lastX, lastY);                    if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {                        currentPath.lineTo(state.lineEndX, state.lineEndY);                    } else {                        currentPath.lineTo(centerX, centerY);                    }                    canvas.drawPath(currentPath, mPathPaint);                }                lastX = centerX;                lastY = centerY;            }            // draw last in progress section            if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)                    && anyCircles) {                currentPath.rewind();                currentPath.moveTo(lastX, lastY);                currentPath.lineTo(mInProgressX, mInProgressY);                mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(                        mInProgressX, mInProgressY, lastX, lastY) * 255f));                canvas.drawPath(currentPath, mPathPaint);            }        }    }

这部分代码比较长,这里就不细细分析了,主要流程就是:

  1. 判断当前显示模式是否是正在绘制。如果是,保存连接的点的状态,计算手指当前所在的点坐标;如果不是,进入第2步。

  2. 根据1中保存的状态,绘制选中的点,已更改选中的点的样式。

    选中的点和未选中的点的状态都是在这部分实时完成的,通过遍历9个点,根据1中保存的状态改变画笔属性绘制不同的样式。

  3. 绘制连接线(path)。主要是获得路径,然后drawPath。

最后就是onTouchEvent处理手指ACTION事件,包括ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL事件。每种事件,判断手势绘制是否结束、改变显示模式、刷新View、回调方法。

LockPatternUtils

LockPatternUtils是处理手势的工具类,主要看下两个方法patternToString、patternToHash两个方法。

  • patternToString
/**     * Serialize a pattern.     * @param pattern The pattern.     * @return The pattern in string form.     */    public static String patternToString(List<LockPatternView.Cell> pattern) {        if (pattern == null) {            return "";        }        final int patternSize = pattern.size();        byte[] res = new byte[patternSize];        for (int i = 0; i < patternSize; i++) {            LockPatternView.Cell cell = pattern.get(i);            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());        }        return new String(res);    }

从方法定义可以看到,将手势用0~8数字,转换成byte数组来表示。

  • patternToHash
/*     * Generate an SHA-1 hash for the pattern. Not the most secure, but it is     * at least a second level of protection. First level is that the file     * is in a location only readable by the system process.     * @param pattern the gesture pattern.     * @return the hash of the pattern in a byte array.     */    public static byte[] patternToHash(List<LockPatternView.Cell> pattern) {        if (pattern == null) {            return null;        }        final int patternSize = pattern.size();        byte[] res = new byte[patternSize];        for (int i = 0; i < patternSize; i++) {            LockPatternView.Cell cell = pattern.get(i);            res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());        }        try {            MessageDigest md = MessageDigest.getInstance("SHA-1");            byte[] hash = md.digest(res);            return hash;        } catch (NoSuchAlgorithmException nsa) {            return res;        }    }

patternToHash的作用是,在patternToString的基础上,采用「SHA-1」算法对byte数组进行hash散列。

值得一提的是,SHA-1虽然不可逆,但算法并不安全。如果采用暴力破解的方式,自己写个程序很快就能撞对。

也许Android的开发者也明白,Android作为开源系统,无法做到真正意义上的绝对安全,除了每个人都能获得源码外,获得系统root权限就能拿到系统所有数据,因此并没有花较大的力气来处理手势的安全问题。当然,这也是作者的猜想。

实际开发中,需要根据APP及手势需求的加密等级,对手势信息进行不同程度的加密。如果需要存储到本地,还涉及到数据的本地存储安全。


通过上面的简单介绍,相信大家大致了解了手势密码的原理,上面分析的内容主要是用户可以修改的,即如果你需要自定义不同的手势样式,可以更改上面分析的对应部分。

我个人基于Android自己的LockPatternView进行了简单的修改,绘制的样式如文章开始的图所示,修改的地方如要是drawCircle、图层、画笔。相关代码可到youngmeng/LockPatternView查看。

0 0
原创粉丝点击