Android Gesture Password Exploration

Android smartphones have a very high market share in the global market, and are increasingly favored by consumers.However, Android is an open source operating system, and it is easy to get root privileges. The security of Android is also one of the most important concerns for users and developers.

Gesture passwords, as a convenient security measure on mobile phones, are favored by many APP developers. Some financial APPs in the market are basically equipped with gesture passwords. The following figure is a state of the gesture drawing process.

At present, most Android phones have the function of gesture lock screen. Android system itself has the function of gesture password. Different ROM manufacturers have made different customizations.This article briefly introduces the principle of gesture password through Android's own source code.

Android Gesture Related Classes

Recall or try the process of unlocking an Android phone with a gesture: first, the user connects a path by clicking on a point in the nine panes, and when the finger is lifted, it will determine if the point path matches the setting.

There are two things involved in this process (regardless of the storage when setting up gestures):

  • Drawing gestures
  • Verification/Matching of Gestures

For these two processes, we can find two related classes by looking up the source code through AOSP:

  • LockPatternView.java:View class, a class for nine-grid gesture graphics display.
  • LockPatternUtils.java: Gesture conversion, matching tool class.

This article explains the principles of gesture representation and drawing by analyzing important parts of these two classes.

LockPatternView

This class is a subclass of View, which defines the entire gesture drawing area related Views, such as the nine-grid points, the drawing path, the state mode of View, and gesture monitoring.The onDraw method of the View parent class is overridden in the class. The selected state of the point and the drawing line are drawn in real time.

Each "palace" in the nine palaces is defined as a Cell as a static internal class, and each Cell contains two coordinates, a row and a column, with rows and columns in the range [0, 3].The advantage of this definition is that the nine palaces are represented by the idea of a matrix, and that the Cell value "row 3 + column*" can be used to represent the nine palaces with a total of 9 numbers ranging from 0 to 8.For example, if a path is drawn as "L", it can be represented as "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 + ")";
        }
}

In the process of gesture drawing, there are generally three states: drawing correctly, drawing in progress, drawing errors (actual development can be set to four, and the fourth is locked).

Gesture Nine Panels use "DisplayMode" to represent three display modes:

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
    }

Three modes allow you to change the gesture state during and after drawing a gesture.For example, change the color to represent the state: during the drawing process, the selected Cell s and lines are shown in blue, errors in red, and correctly in green.

Gesture drawing uses four listening functions in the interface OnPatternListener to listen for gesture start, end, clear, add, and so on.The interface is defined as follows:

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);
    }

The meaning of each method can be seen from the method names and comments, which are not repeated here.

Next, as the gesture is drawn, the View determines whether a Cell is selected at the current position of the finger and whether it should be connected to the gesture.Here are a few functions to understand:

  • getRowHit ( float y )

    Used to determine the row in the ninth grid where the current coordinates (x, y) of the fingers are located.

  • getColumnHit (float x )

    Used to determine which column of the current coordinate (x, y) of the finger is in the ninth grid.

  • 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);
      }

    The function code understands that mPatternDrawLookup is a global variable and also takes the form of a matrix to mark which Cell in the Nine Palaces is connected.As you can see from checkForNewHit, the connected Cell will no longer be selected, which is the common practice for gesture passwords.If you need to fulfill the requirement that each point can be connected multiple times, this part needs to be changed.

  • detectAndAddHit (float x, float y)

    Used to detect and determine if the current coordinates (x, y) of the finger need to be added to the current gesture.

    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;
      }

    First check ForNewHit to get the current location of the Cell and calculate the row and column difference between the current Cell and the last Cell in the gesture.Look at one piece of code

    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);

    The criterion is that the absolute difference between the current Cell and the last Cell in the gesture is 2, and the absolute difference between the Cell and the last Cell in the gesture is not 1, that is, two cells are not adjacent (including horizontal, vertical, and 45 degrees), so that the Cell between the current Cell and the last Cell in the gesture is obtained, if the Cell does not existIf something has been added, add a gesture.

    That is, gestures drawn do not cross points that have not been added.

Previously, the selected and unselected points during the drawing process were drawn in real time by overriding the onDraw method of View.The onDraw code is as follows:

@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);
            }
        }
    }

This part of the code is longer, so we will not analyze it in detail here. The main process is:

  1. Determines if the current display mode is drawing.If so, save the state of the connected points and calculate the point coordinates where your fingers are currently located; if not, go to step 2.

  2. Based on the saved state in 1, the selected points are drawn, and the style of the selected points has been changed.

    The state of the selected and unselected points is done in real time in this section. By traversing 9 points, different styles are drawn according to the state saved in 1 to change the brush properties.

  3. Draw a connection line (path).The main thing is to get the path, then drawPath.

Finally, onTouchEvent handles finger ACTION events, including ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_CANCEL events.For each event, determine whether the gesture drawing is over, change the display mode, refresh the View, callback method.

LockPatternUtils

LockPatternUtils is a tool class for handling gestures. Look at two methods, patternToString and 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);
    }

As you can see from the method definition, gestures are converted to byte arrays using numbers from 0 to 8.

  • 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;
        }
    }

The purpose of patternToHash is to hash byte arrays using the SHA-1 algorithm on the basis of patternToString.

It is worth mentioning that although SHA-1 is not reversible, the algorithm is not secure.If you use violent cracking, writing your own program can quickly collide.

Perhaps Android developers also understand that Android, as an open source system, cannot be truly absolutely secure. Everyone can get all the data in the system except the source code. So they don't take much effort to deal with the security of gestures.Of course, this is the author's guess.

In the actual development, according to the encryption level of APP and gesture requirements, the gesture information needs to be encrypted to different degrees.If you need to store locally, you also need to secure the local storage of your data.

Through the brief introduction above, I believe you have a general understanding of the principles of gesture passwords. The analysis above is mainly user-modifiable, that is, if you need to customize different gesture styles, you can change the corresponding part of the analysis above.

I personally made simple modifications based on Android's own LockPatternView, drawing the style shown in the image at the beginning of the article, such as drawCircle, layers, brushes.Related code is available youngmeng/LockPatternView See.

Keywords: Android Java Mobile

Added by steved on Sat, 06 Jul 2019 21:18:31 +0300