Android手势密码探索
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 9private 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");}}@Overridepublic 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 patternCell 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代碼如下:
@Overrideprotected 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 patternfinal 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 linefinal 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 circlesfor (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 sectionif ((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);}}}這部分代碼比較長,這里就不細細分析了,主要流程就是:
判斷當前顯示模式是否是正在繪制。如果是,保存連接的點的狀態,計算手指當前所在的點坐標;如果不是,進入第2步。
根據1中保存的狀態,繪制選中的點,已更改選中的點的樣式。
選中的點和未選中的點的狀態都是在這部分實時完成的,通過遍歷9個點,根據1中保存的狀態改變畫筆屬性繪制不同的樣式。
繪制連接線(path)。主要是獲得路徑,然后drawPath。
最后就是onTouchEvent處理手指ACTION事件,包括ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL事件。每種事件,判斷手勢繪制是否結束、改變顯示模式、刷新View、回調方法。
LockPatternUtils
LockPatternUtils是處理手勢的工具類,主要看下兩個方法patternToString、patternToHash兩個方法。
- patternToString
從方法定義可以看到,將手勢用0~8數字,轉換成byte數組來表示。
- patternToHash
patternToHash的作用是,在patternToString的基礎上,采用「SHA-1」算法對byte數組進行hash散列。
值得一提的是,SHA-1雖然不可逆,但算法并不安全。如果采用暴力破解的方式,自己寫個程序很快就能撞對。
也許Android的開發者也明白,Android作為開源系統,無法做到真正意義上的絕對安全,除了每個人都能獲得源碼外,獲得系統root權限就能拿到系統所有數據,因此并沒有花較大的力氣來處理手勢的安全問題。當然,這也是作者的猜想。
實際開發中,需要根據APP及手勢需求的加密等級,對手勢信息進行不同程度的加密。如果需要存儲到本地,還涉及到數據的本地存儲安全。
通過上面的簡單介紹,相信大家大致了解了手勢密碼的原理,上面分析的內容主要是用戶可以修改的,即如果你需要自定義不同的手勢樣式,可以更改上面分析的對應部分。
我個人基于Android自己的LockPatternView進行了簡單的修改,繪制的樣式如文章開始的圖所示,修改的地方如要是drawCircle、圖層、畫筆。相關代碼可到youngmeng/LockPatternView查看。
總結
以上是生活随笔為你收集整理的Android手势密码探索的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python:用海龟作图turtle画一
- 下一篇: 模电_数电_微机接口_微机应用实验装置,