Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 1 | /** |
| 2 | * Copyright (C) 2016 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.server.fingerprint; |
| 18 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 19 | import android.content.Context; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 20 | import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint; |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 21 | import android.hardware.biometrics.BiometricPrompt; |
| 22 | import android.hardware.biometrics.IBiometricPromptReceiver; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 23 | import android.hardware.fingerprint.Fingerprint; |
| 24 | import android.hardware.fingerprint.FingerprintManager; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 25 | import android.hardware.fingerprint.IFingerprintServiceReceiver; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 26 | import android.os.Bundle; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 27 | import android.os.IBinder; |
| 28 | import android.os.RemoteException; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 29 | import android.util.Slog; |
| 30 | |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 31 | import com.android.internal.logging.MetricsLogger; |
| 32 | import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| 33 | import com.android.internal.statusbar.IStatusBarService; |
| 34 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 35 | /** |
| 36 | * A class to keep track of the authentication state for a given client. |
| 37 | */ |
| 38 | public abstract class AuthenticationClient extends ClientMonitor { |
| 39 | private long mOpId; |
| 40 | |
Kevin Chyn | df9d33e | 2017-05-03 21:40:12 -0700 | [diff] [blame] | 41 | public abstract int handleFailedAttempt(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 42 | public abstract void resetFailedAttempts(); |
Kevin Chyn | df9d33e | 2017-05-03 21:40:12 -0700 | [diff] [blame] | 43 | |
| 44 | public static final int LOCKOUT_NONE = 0; |
| 45 | public static final int LOCKOUT_TIMED = 1; |
| 46 | public static final int LOCKOUT_PERMANENT = 2; |
| 47 | |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 48 | // Callback mechanism received from the client |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 49 | // (BiometricPrompt -> FingerprintManager -> FingerprintService -> AuthenticationClient) |
| 50 | private IBiometricPromptReceiver mDialogReceiverFromClient; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 51 | private Bundle mBundle; |
| 52 | private IStatusBarService mStatusBarService; |
| 53 | private boolean mInLockout; |
| 54 | private final FingerprintManager mFingerprintManager; |
| 55 | protected boolean mDialogDismissed; |
| 56 | |
Kevin Chyn | dba919a | 2018-03-16 14:35:10 -0700 | [diff] [blame] | 57 | // Receives events from SystemUI and handles them before forwarding them to FingerprintDialog |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 58 | protected IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() { |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 59 | @Override // binder call |
| 60 | public void onDialogDismissed(int reason) { |
| 61 | if (mBundle != null && mDialogReceiverFromClient != null) { |
| 62 | try { |
| 63 | mDialogReceiverFromClient.onDialogDismissed(reason); |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 64 | if (reason == BiometricPrompt.DISMISSED_REASON_USER_CANCEL) { |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 65 | onError(FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED, |
| 66 | 0 /* vendorCode */); |
| 67 | } |
| 68 | mDialogDismissed = true; |
| 69 | } catch (RemoteException e) { |
| 70 | Slog.e(TAG, "Unable to notify dialog dismissed", e); |
| 71 | } |
| 72 | stop(true /* initiatedByClient */); |
| 73 | } |
| 74 | } |
| 75 | }; |
| 76 | |
Kevin Chyn | d4f43c2 | 2018-03-12 17:33:13 -0700 | [diff] [blame] | 77 | /** |
| 78 | * This method is called when authentication starts. |
| 79 | */ |
| 80 | public abstract void onStart(); |
| 81 | |
| 82 | /** |
| 83 | * This method is called when a fingerprint is authenticated or authentication is stopped |
| 84 | * (cancelled by the user, or an error such as lockout has occurred). |
| 85 | */ |
| 86 | public abstract void onStop(); |
| 87 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 88 | public AuthenticationClient(Context context, long halDeviceId, IBinder token, |
Jim Miller | 837fa7e | 2016-08-08 20:16:22 -0700 | [diff] [blame] | 89 | IFingerprintServiceReceiver receiver, int targetUserId, int groupId, long opId, |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 90 | boolean restricted, String owner, Bundle bundle, |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 91 | IBiometricPromptReceiver dialogReceiver, IStatusBarService statusBarService) { |
Jim Miller | 837fa7e | 2016-08-08 20:16:22 -0700 | [diff] [blame] | 92 | super(context, halDeviceId, token, receiver, targetUserId, groupId, restricted, owner); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 93 | mOpId = opId; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 94 | mBundle = bundle; |
| 95 | mDialogReceiverFromClient = dialogReceiver; |
| 96 | mStatusBarService = statusBarService; |
| 97 | mFingerprintManager = (FingerprintManager) getContext() |
| 98 | .getSystemService(Context.FINGERPRINT_SERVICE); |
| 99 | } |
| 100 | |
| 101 | @Override |
| 102 | public void binderDied() { |
| 103 | super.binderDied(); |
| 104 | // When the binder dies, we should stop the client. This probably belongs in |
| 105 | // ClientMonitor's binderDied(), but testing all the cases would be tricky. |
| 106 | // AuthenticationClient is the most user-visible case. |
| 107 | stop(false /* initiatedByClient */); |
| 108 | } |
| 109 | |
| 110 | @Override |
| 111 | public boolean onAcquired(int acquiredInfo, int vendorCode) { |
| 112 | // If the dialog is showing, the client doesn't need to receive onAcquired messages. |
| 113 | if (mBundle != null) { |
| 114 | try { |
| 115 | if (acquiredInfo != FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) { |
| 116 | mStatusBarService.onFingerprintHelp( |
| 117 | mFingerprintManager.getAcquiredString(acquiredInfo, vendorCode)); |
| 118 | } |
| 119 | return false; // acquisition continues |
| 120 | } catch (RemoteException e) { |
| 121 | Slog.e(TAG, "Remote exception when sending acquired message", e); |
| 122 | return true; // client failed |
| 123 | } finally { |
| 124 | // Good scans will keep the device awake |
| 125 | if (acquiredInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) { |
| 126 | notifyUserActivity(); |
| 127 | } |
| 128 | } |
| 129 | } else { |
| 130 | return super.onAcquired(acquiredInfo, vendorCode); |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | @Override |
| 135 | public boolean onError(int error, int vendorCode) { |
| 136 | if (mDialogDismissed) { |
| 137 | // If user cancels authentication, the application has already received the |
| 138 | // FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED message from onDialogDismissed() |
| 139 | // and stopped the fingerprint hardware, so there is no need to send a |
| 140 | // FingerprintManager.FINGERPRINT_ERROR_CANCELED message. |
| 141 | return true; |
| 142 | } |
| 143 | if (mBundle != null) { |
| 144 | try { |
| 145 | mStatusBarService.onFingerprintError( |
| 146 | mFingerprintManager.getErrorString(error, vendorCode)); |
| 147 | } catch (RemoteException e) { |
| 148 | Slog.e(TAG, "Remote exception when sending error", e); |
| 149 | } |
| 150 | } |
| 151 | return super.onError(error, vendorCode); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 152 | } |
| 153 | |
| 154 | @Override |
| 155 | public boolean onAuthenticated(int fingerId, int groupId) { |
| 156 | boolean result = false; |
| 157 | boolean authenticated = fingerId != 0; |
| 158 | |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 159 | // If the fingerprint dialog is showing, notify authentication succeeded |
| 160 | if (mBundle != null) { |
| 161 | try { |
| 162 | if (authenticated) { |
| 163 | mStatusBarService.onFingerprintAuthenticated(); |
| 164 | } else { |
| 165 | mStatusBarService.onFingerprintHelp(getContext().getResources().getString( |
| 166 | com.android.internal.R.string.fingerprint_not_recognized)); |
| 167 | } |
| 168 | } catch (RemoteException e) { |
| 169 | Slog.e(TAG, "Failed to notify Authenticated:", e); |
| 170 | } |
| 171 | } |
| 172 | |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 173 | IFingerprintServiceReceiver receiver = getReceiver(); |
| 174 | if (receiver != null) { |
| 175 | try { |
| 176 | MetricsLogger.action(getContext(), MetricsEvent.ACTION_FINGERPRINT_AUTH, |
| 177 | authenticated); |
| 178 | if (!authenticated) { |
| 179 | receiver.onAuthenticationFailed(getHalDeviceId()); |
| 180 | } else { |
| 181 | if (DEBUG) { |
| 182 | Slog.v(TAG, "onAuthenticated(owner=" + getOwnerString() |
| 183 | + ", id=" + fingerId + ", gp=" + groupId + ")"); |
| 184 | } |
| 185 | Fingerprint fp = !getIsRestricted() |
| 186 | ? new Fingerprint("" /* TODO */, groupId, fingerId, getHalDeviceId()) |
| 187 | : null; |
Jim Miller | 837fa7e | 2016-08-08 20:16:22 -0700 | [diff] [blame] | 188 | receiver.onAuthenticationSucceeded(getHalDeviceId(), fp, getTargetUserId()); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 189 | } |
| 190 | } catch (RemoteException e) { |
| 191 | Slog.w(TAG, "Failed to notify Authenticated:", e); |
| 192 | result = true; // client failed |
| 193 | } |
| 194 | } else { |
| 195 | result = true; // client not listening |
| 196 | } |
Jim Miller | d197486 | 2016-05-03 18:35:18 -0700 | [diff] [blame] | 197 | if (!authenticated) { |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 198 | if (receiver != null) { |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 199 | vibrateError(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 200 | } |
| 201 | // allow system-defined limit of number of attempts before giving up |
Kevin Chyn | df9d33e | 2017-05-03 21:40:12 -0700 | [diff] [blame] | 202 | int lockoutMode = handleFailedAttempt(); |
| 203 | if (lockoutMode != LOCKOUT_NONE) { |
Jim Miller | d197486 | 2016-05-03 18:35:18 -0700 | [diff] [blame] | 204 | try { |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 205 | mInLockout = true; |
Kevin Chyn | df9d33e | 2017-05-03 21:40:12 -0700 | [diff] [blame] | 206 | Slog.w(TAG, "Forcing lockout (fp driver code should do this!), mode(" + |
| 207 | lockoutMode + ")"); |
| 208 | stop(false); |
| 209 | int errorCode = lockoutMode == LOCKOUT_TIMED ? |
| 210 | FingerprintManager.FINGERPRINT_ERROR_LOCKOUT : |
| 211 | FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 212 | |
| 213 | // TODO: if the dialog is showing, this error should be delayed. On a similar |
| 214 | // note, AuthenticationClient should override onError and delay all other errors |
| 215 | // as well, if the dialog is showing |
Kevin Chyn | df9d33e | 2017-05-03 21:40:12 -0700 | [diff] [blame] | 216 | receiver.onError(getHalDeviceId(), errorCode, 0 /* vendorCode */); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 217 | |
| 218 | // Send the lockout message to the system dialog |
| 219 | if (mBundle != null) { |
| 220 | mStatusBarService.onFingerprintError( |
| 221 | mFingerprintManager.getErrorString(errorCode, 0 /* vendorCode */)); |
| 222 | } |
Jim Miller | d197486 | 2016-05-03 18:35:18 -0700 | [diff] [blame] | 223 | } catch (RemoteException e) { |
| 224 | Slog.w(TAG, "Failed to notify lockout:", e); |
| 225 | } |
| 226 | } |
Kevin Chyn | df9d33e | 2017-05-03 21:40:12 -0700 | [diff] [blame] | 227 | result |= lockoutMode != LOCKOUT_NONE; // in a lockout mode |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 228 | } else { |
| 229 | if (receiver != null) { |
Michael Wright | 6726fd5 | 2017-06-27 00:41:45 +0100 | [diff] [blame] | 230 | vibrateSuccess(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 231 | } |
| 232 | result |= true; // we have a valid fingerprint, done |
| 233 | resetFailedAttempts(); |
Kevin Chyn | d4f43c2 | 2018-03-12 17:33:13 -0700 | [diff] [blame] | 234 | onStop(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 235 | } |
| 236 | return result; |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Start authentication |
| 241 | */ |
| 242 | @Override |
| 243 | public int start() { |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 244 | IBiometricsFingerprint daemon = getFingerprintDaemon(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 245 | if (daemon == null) { |
Kevin Chyn | 80e40cc | 2017-03-14 12:31:17 -0700 | [diff] [blame] | 246 | Slog.w(TAG, "start authentication: no fingerprint HAL!"); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 247 | return ERROR_ESRCH; |
| 248 | } |
Kevin Chyn | d4f43c2 | 2018-03-12 17:33:13 -0700 | [diff] [blame] | 249 | onStart(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 250 | try { |
| 251 | final int result = daemon.authenticate(mOpId, getGroupId()); |
| 252 | if (result != 0) { |
| 253 | Slog.w(TAG, "startAuthentication failed, result=" + result); |
Jim Miller | c57c8d9 | 2016-09-30 17:17:59 -0700 | [diff] [blame] | 254 | MetricsLogger.histogram(getContext(), "fingeprintd_auth_start_error", result); |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 255 | onError(FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 256 | return result; |
| 257 | } |
| 258 | if (DEBUG) Slog.w(TAG, "client " + getOwnerString() + " is authenticating..."); |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 259 | |
| 260 | // If authenticating with system dialog, show the dialog |
| 261 | if (mBundle != null) { |
| 262 | try { |
| 263 | mStatusBarService.showFingerprintDialog(mBundle, mDialogReceiver); |
| 264 | } catch (RemoteException e) { |
| 265 | Slog.e(TAG, "Unable to show fingerprint dialog", e); |
| 266 | } |
| 267 | } |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 268 | } catch (RemoteException e) { |
| 269 | Slog.e(TAG, "startAuthentication failed", e); |
| 270 | return ERROR_ESRCH; |
| 271 | } |
| 272 | return 0; // success |
| 273 | } |
| 274 | |
| 275 | @Override |
| 276 | public int stop(boolean initiatedByClient) { |
Kevin Chyn | 625a014 | 2017-04-10 14:53:59 -0700 | [diff] [blame] | 277 | if (mAlreadyCancelled) { |
| 278 | Slog.w(TAG, "stopAuthentication: already cancelled!"); |
| 279 | return 0; |
| 280 | } |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 281 | |
Kevin Chyn | d4f43c2 | 2018-03-12 17:33:13 -0700 | [diff] [blame] | 282 | onStop(); |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 283 | IBiometricsFingerprint daemon = getFingerprintDaemon(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 284 | if (daemon == null) { |
Kevin Chyn | 80e40cc | 2017-03-14 12:31:17 -0700 | [diff] [blame] | 285 | Slog.w(TAG, "stopAuthentication: no fingerprint HAL!"); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 286 | return ERROR_ESRCH; |
| 287 | } |
| 288 | try { |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 289 | final int result = daemon.cancel(); |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 290 | if (result != 0) { |
| 291 | Slog.w(TAG, "stopAuthentication failed, result=" + result); |
| 292 | return result; |
| 293 | } |
| 294 | if (DEBUG) Slog.w(TAG, "client " + getOwnerString() + " is no longer authenticating"); |
| 295 | } catch (RemoteException e) { |
| 296 | Slog.e(TAG, "stopAuthentication failed", e); |
| 297 | return ERROR_ESRCH; |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 298 | } finally { |
| 299 | // If the user already cancelled authentication (via some interaction with the |
| 300 | // dialog, we do not need to hide it since it's already hidden. |
| 301 | // If the device is in lockout, don't hide the dialog - it will automatically hide |
Vishwath Mohan | ecf00ce | 2018-04-05 10:28:24 -0700 | [diff] [blame] | 302 | // after BiometricPrompt.HIDE_DIALOG_DELAY |
Kevin Chyn | aae4a15 | 2018-01-18 11:48:09 -0800 | [diff] [blame] | 303 | if (mBundle != null && !mDialogDismissed && !mInLockout) { |
| 304 | try { |
| 305 | mStatusBarService.hideFingerprintDialog(); |
| 306 | } catch (RemoteException e) { |
| 307 | Slog.e(TAG, "Unable to hide fingerprint dialog", e); |
| 308 | } |
| 309 | } |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 310 | } |
Kevin Chyn | 625a014 | 2017-04-10 14:53:59 -0700 | [diff] [blame] | 311 | mAlreadyCancelled = true; |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 312 | return 0; // success |
| 313 | } |
| 314 | |
| 315 | @Override |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 316 | public boolean onEnrollResult(int fingerId, int groupId, int remaining) { |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 317 | if (DEBUG) Slog.w(TAG, "onEnrollResult() called for authenticate!"); |
| 318 | return true; // Invalid for Authenticate |
| 319 | } |
| 320 | |
| 321 | @Override |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 322 | public boolean onRemoved(int fingerId, int groupId, int remaining) { |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 323 | if (DEBUG) Slog.w(TAG, "onRemoved() called for authenticate!"); |
| 324 | return true; // Invalid for Authenticate |
| 325 | } |
| 326 | |
| 327 | @Override |
Jim Miller | 40e4645 | 2016-12-16 18:38:53 -0800 | [diff] [blame] | 328 | public boolean onEnumerationResult(int fingerId, int groupId, int remaining) { |
Jim Miller | cb2ce6f | 2016-04-13 20:28:18 -0700 | [diff] [blame] | 329 | if (DEBUG) Slog.w(TAG, "onEnumerationResult() called for authenticate!"); |
| 330 | return true; // Invalid for Authenticate |
| 331 | } |
| 332 | } |