gnome-shell/js/gdm/authPrompt.js
Marco Trevisan (Treviño) c8bb45b41c gdm: Limit verification cancellations to be conform to allowed-failures
As per previous commit the user can cancel an ongoing authentication via
Escape key and that will always send the user back to the clock view in
lockscreen or user-selection view in login prompt.

However, we can be a little more permissive and don't switch view to be
able to restart the authentication without further action.

To avoid this to be abused though, we consider the user verification
cancellation via escape key to be a "soft-failure", so once the
configured "allowed-failures" gsettings value has been reached, we'd
just act as before, ignoring any further request (until we don't get
back to the user auth view).

In this way we still make brute-force attacks harder to do, while still
giving the well-behaving user some ability to fix mistakes.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1622>
2021-02-12 20:26:00 +00:00

570 lines
18 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported AuthPrompt */
const { Clutter, GObject, Pango, Shell, St } = imports.gi;
const Animation = imports.ui.animation;
const Batch = imports.gdm.batch;
const GdmUtil = imports.gdm.util;
const OVirt = imports.gdm.oVirt;
const Vmware = imports.gdm.vmware;
const Params = imports.misc.params;
const ShellEntry = imports.ui.shellEntry;
const UserWidget = imports.ui.userWidget;
const Util = imports.misc.util;
var DEFAULT_BUTTON_WELL_ICON_SIZE = 16;
var DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000;
var DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300;
var MESSAGE_FADE_OUT_ANIMATION_TIME = 500;
var AuthPromptMode = {
UNLOCK_ONLY: 0,
UNLOCK_OR_LOG_IN: 1,
};
var AuthPromptStatus = {
NOT_VERIFYING: 0,
VERIFYING: 1,
VERIFICATION_FAILED: 2,
VERIFICATION_SUCCEEDED: 3,
VERIFICATION_CANCELLED: 4,
VERIFICATION_IN_PROGRESS: 5,
};
var BeginRequestType = {
PROVIDE_USERNAME: 0,
DONT_PROVIDE_USERNAME: 1,
REUSE_USERNAME: 2,
};
var AuthPrompt = GObject.registerClass({
Signals: {
'cancelled': {},
'failed': {},
'next': {},
'prompted': {},
'reset': { param_types: [GObject.TYPE_UINT] },
},
}, class AuthPrompt extends St.BoxLayout {
_init(gdmClient, mode) {
super._init({
style_class: 'login-dialog-prompt-layout',
vertical: true,
x_expand: true,
x_align: Clutter.ActorAlign.CENTER,
});
this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;
this._gdmClient = gdmClient;
this._mode = mode;
this._defaultButtonWellActor = null;
this._cancelledRetries = 0;
let reauthenticationOnly;
if (this._mode == AuthPromptMode.UNLOCK_ONLY)
reauthenticationOnly = true;
else if (this._mode == AuthPromptMode.UNLOCK_OR_LOG_IN)
reauthenticationOnly = false;
this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly });
this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this));
this._userVerifier.connect('show-message', this._onShowMessage.bind(this));
this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this));
this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this));
this._userVerifier.connect('reset', this._onReset.bind(this));
this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this));
this._userVerifier.connect('credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this));
this.smartcardDetected = this._userVerifier.smartcardDetected;
this.connect('destroy', this._onDestroy.bind(this));
this._userWell = new St.Bin({
x_expand: true,
y_expand: true,
});
this.add_child(this._userWell);
this._hasCancelButton = this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN;
this._initEntryRow();
let capsLockPlaceholder = new St.Label();
this.add_child(capsLockPlaceholder);
this._capsLockWarningLabel = new ShellEntry.CapsLockWarning({
x_expand: true,
x_align: Clutter.ActorAlign.CENTER,
});
this.add_child(this._capsLockWarningLabel);
this._capsLockWarningLabel.bind_property('visible',
capsLockPlaceholder, 'visible',
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN);
this._message = new St.Label({
opacity: 0,
styleClass: 'login-dialog-message',
y_expand: true,
x_expand: true,
y_align: Clutter.ActorAlign.START,
x_align: Clutter.ActorAlign.CENTER,
});
this._message.clutter_text.line_wrap = true;
this._message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
this.add_child(this._message);
}
_onDestroy() {
this._userVerifier.destroy();
this._userVerifier = null;
}
vfunc_key_press_event(keyPressEvent) {
if (keyPressEvent.keyval == Clutter.KEY_Escape)
this.cancel();
return super.vfunc_key_press_event(keyPressEvent);
}
_initEntryRow() {
this._mainBox = new St.BoxLayout({
style_class: 'login-dialog-button-box',
vertical: false,
});
this.add_child(this._mainBox);
this.cancelButton = new St.Button({
style_class: 'modal-dialog-button button cancel-button',
accessible_name: _('Cancel'),
button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE,
reactive: this._hasCancelButton,
can_focus: this._hasCancelButton,
x_align: Clutter.ActorAlign.START,
y_align: Clutter.ActorAlign.CENTER,
child: new St.Icon({ icon_name: 'go-previous-symbolic' }),
});
if (this._hasCancelButton)
this.cancelButton.connect('clicked', () => this.cancel());
else
this.cancelButton.opacity = 0;
this._mainBox.add_child(this.cancelButton);
let entryParams = {
style_class: 'login-dialog-prompt-entry',
can_focus: true,
x_expand: true,
};
this._entry = null;
this._textEntry = new St.Entry(entryParams);
ShellEntry.addContextMenu(this._textEntry, { actionMode: Shell.ActionMode.NONE });
this._passwordEntry = new St.PasswordEntry(entryParams);
ShellEntry.addContextMenu(this._passwordEntry, { actionMode: Shell.ActionMode.NONE });
this._entry = this._passwordEntry;
this._mainBox.add_child(this._entry);
this._entry.grab_key_focus();
[this._textEntry, this._passwordEntry].forEach(entry => {
entry.clutter_text.connect('text-changed', () => {
if (!this._userVerifier.hasPendingMessages)
this._fadeOutMessage();
});
entry.clutter_text.connect('activate', () => {
let shouldSpin = entry === this._passwordEntry;
if (entry.reactive)
this._activateNext(shouldSpin);
});
});
this._defaultButtonWell = new St.Widget({
layout_manager: new Clutter.BinLayout(),
x_align: Clutter.ActorAlign.END,
y_align: Clutter.ActorAlign.CENTER,
});
this._defaultButtonWell.add_constraint(new Clutter.BindConstraint({
source: this.cancelButton,
coordinate: Clutter.BindCoordinate.WIDTH,
}));
this._mainBox.add_child(this._defaultButtonWell);
this._spinner = new Animation.Spinner(DEFAULT_BUTTON_WELL_ICON_SIZE);
this._defaultButtonWell.add_child(this._spinner);
}
_activateNext(shouldSpin) {
this.verificationStatus = AuthPromptStatus.VERIFICATION_IN_PROGRESS;
this.updateSensitivity(false);
if (shouldSpin)
this.startSpinning();
if (this._queryingService)
this._userVerifier.answerQuery(this._queryingService, this._entry.text);
else
this._preemptiveAnswer = this._entry.text;
this.emit('next');
}
_updateEntry(secret) {
if (secret && this._entry !== this._passwordEntry) {
this._mainBox.replace_child(this._entry, this._passwordEntry);
this._entry = this._passwordEntry;
} else if (!secret && this._entry !== this._textEntry) {
this._mainBox.replace_child(this._entry, this._textEntry);
this._entry = this._textEntry;
}
this._capsLockWarningLabel.visible = secret;
}
_onAskQuestion(verifier, serviceName, question, secret) {
if (this._queryingService)
this.clear();
this._queryingService = serviceName;
if (this._preemptiveAnswer) {
this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer);
this._preemptiveAnswer = null;
return;
}
this._updateEntry(secret);
// Hack: The question string comes directly from PAM, if it's "Password:"
// we replace it with our own to allow localization, if it's something
// else we remove the last colon and any trailing or leading spaces.
if (question === 'Password:' || question === 'Password: ')
this.setQuestion(_('Password'));
else
this.setQuestion(question.replace(/: *$/, '').trim());
this.updateSensitivity(true);
this.emit('prompted');
}
_onCredentialManagerAuthenticated() {
if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED)
this.reset();
}
_onSmartcardStatusChanged() {
this.smartcardDetected = this._userVerifier.smartcardDetected;
// Most of the time we want to reset if the user inserts or removes
// a smartcard. Smartcard insertion "preempts" what the user was
// doing, and smartcard removal aborts the preemption.
// The exceptions are: 1) Don't reset on smartcard insertion if we're already verifying
// with a smartcard
// 2) Don't reset if we've already succeeded at verification and
// the user is getting logged in.
if (this._userVerifier.serviceIsDefault(GdmUtil.SMARTCARD_SERVICE_NAME) &&
this.verificationStatus == AuthPromptStatus.VERIFYING &&
this.smartcardDetected)
return;
if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED)
this.reset();
}
_onShowMessage(userVerifier, message, type) {
this.setMessage(message, type);
this.emit('prompted');
}
_onVerificationFailed(userVerifier, canRetry) {
this._queryingService = null;
this.clear();
this.updateSensitivity(canRetry);
this.setActorInDefaultButtonWell(null);
this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;
Util.wiggle(this._entry);
}
_onVerificationComplete() {
this.setActorInDefaultButtonWell(null);
this.verificationStatus = AuthPromptStatus.VERIFICATION_SUCCEEDED;
this.cancelButton.reactive = false;
this.cancelButton.can_focus = false;
}
_onReset() {
this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;
this.reset();
}
setActorInDefaultButtonWell(actor, animate) {
if (!this._defaultButtonWellActor &&
!actor)
return;
let oldActor = this._defaultButtonWellActor;
if (oldActor)
oldActor.remove_all_transitions();
let wasSpinner;
if (oldActor == this._spinner)
wasSpinner = true;
else
wasSpinner = false;
let isSpinner;
if (actor == this._spinner)
isSpinner = true;
else
isSpinner = false;
if (this._defaultButtonWellActor != actor && oldActor) {
if (!animate) {
oldActor.opacity = 0;
if (wasSpinner) {
if (this._spinner)
this._spinner.stop();
}
} else {
oldActor.ease({
opacity: 0,
duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME,
delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY,
mode: Clutter.AnimationMode.LINEAR,
onComplete: () => {
if (wasSpinner) {
if (this._spinner)
this._spinner.stop();
}
},
});
}
}
if (actor) {
if (isSpinner)
this._spinner.play();
if (!animate) {
actor.opacity = 255;
} else {
actor.ease({
opacity: 255,
duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME,
delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY,
mode: Clutter.AnimationMode.LINEAR,
});
}
}
this._defaultButtonWellActor = actor;
}
startSpinning() {
this.setActorInDefaultButtonWell(this._spinner, true);
}
stopSpinning() {
this.setActorInDefaultButtonWell(null, false);
}
clear() {
this._entry.text = '';
this.stopSpinning();
}
setQuestion(question) {
this._entry.hint_text = question;
this._entry.show();
this._entry.grab_key_focus();
}
getAnswer() {
let text;
if (this._preemptiveAnswer) {
text = this._preemptiveAnswer;
this._preemptiveAnswer = null;
} else {
text = this._entry.get_text();
}
return text;
}
_fadeOutMessage() {
if (this._message.opacity == 0)
return;
this._message.remove_all_transitions();
this._message.ease({
opacity: 0,
duration: MESSAGE_FADE_OUT_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
setMessage(message, type) {
if (type == GdmUtil.MessageType.ERROR)
this._message.add_style_class_name('login-dialog-message-warning');
else
this._message.remove_style_class_name('login-dialog-message-warning');
if (type == GdmUtil.MessageType.HINT)
this._message.add_style_class_name('login-dialog-message-hint');
else
this._message.remove_style_class_name('login-dialog-message-hint');
if (message) {
this._message.remove_all_transitions();
this._message.text = message;
this._message.opacity = 255;
} else {
this._message.opacity = 0;
}
}
updateSensitivity(sensitive) {
if (this._entry.reactive === sensitive)
return;
this._entry.reactive = sensitive;
if (sensitive) {
this._entry.grab_key_focus();
} else {
this.grab_key_focus();
if (this._entry === this._passwordEntry)
this._entry.password_visible = false;
}
}
vfunc_hide() {
this.setActorInDefaultButtonWell(null, true);
super.vfunc_hide();
this._message.opacity = 0;
this.setUser(null);
this.updateSensitivity(true);
this._entry.set_text('');
}
setUser(user) {
let oldChild = this._userWell.get_child();
if (oldChild)
oldChild.destroy();
let userWidget = new UserWidget.UserWidget(user, Clutter.Orientation.VERTICAL);
this._userWell.set_child(userWidget);
if (!user)
this._updateEntry(false);
}
reset() {
let oldStatus = this.verificationStatus;
this.verificationStatus = AuthPromptStatus.NOT_VERIFYING;
this.cancelButton.reactive = this._hasCancelButton;
this.cancelButton.can_focus = this._hasCancelButton;
this._preemptiveAnswer = null;
if (this._userVerifier)
this._userVerifier.cancel();
this._queryingService = null;
this.clear();
this._message.opacity = 0;
this.setUser(null);
this._updateEntry(true);
this.stopSpinning();
if (oldStatus == AuthPromptStatus.VERIFICATION_FAILED)
this.emit('failed');
else if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
this.emit('cancelled');
let beginRequestType;
if (this._mode == AuthPromptMode.UNLOCK_ONLY) {
// The user is constant at the unlock screen, so it will immediately
// respond to the request with the username
if (oldStatus === AuthPromptStatus.VERIFICATION_CANCELLED)
return;
beginRequestType = BeginRequestType.PROVIDE_USERNAME;
} else if (this._userVerifier.serviceIsForeground(OVirt.SERVICE_NAME) ||
this._userVerifier.serviceIsForeground(Vmware.SERVICE_NAME) ||
this._userVerifier.serviceIsForeground(GdmUtil.SMARTCARD_SERVICE_NAME)) {
// We don't need to know the username if the user preempted the login screen
// with a smartcard or with preauthenticated oVirt credentials
beginRequestType = BeginRequestType.DONT_PROVIDE_USERNAME;
} else if (oldStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) {
// We're going back to retry with current user
beginRequestType = BeginRequestType.REUSE_USERNAME;
} else {
// In all other cases, we should get the username up front.
beginRequestType = BeginRequestType.PROVIDE_USERNAME;
}
this.emit('reset', beginRequestType);
}
addCharacter(unichar) {
if (!this._entry.visible)
return;
this._entry.grab_key_focus();
this._entry.clutter_text.insert_unichar(unichar);
}
begin(params) {
params = Params.parse(params, { userName: null,
hold: null });
this.updateSensitivity(false);
let hold = params.hold;
if (!hold)
hold = new Batch.Hold();
this._userVerifier.begin(params.userName, hold);
this.verificationStatus = AuthPromptStatus.VERIFYING;
}
finish(onComplete) {
if (!this._userVerifier.hasPendingMessages) {
this._userVerifier.clear();
onComplete();
return;
}
let signalId = this._userVerifier.connect('no-more-messages', () => {
this._userVerifier.disconnect(signalId);
this._userVerifier.clear();
onComplete();
});
}
cancel() {
if (this.verificationStatus == AuthPromptStatus.VERIFICATION_SUCCEEDED)
return;
if (this.verificationStatus === AuthPromptStatus.VERIFICATION_IN_PROGRESS) {
this._cancelledRetries++;
if (this._cancelledRetries > this._userVerifier.allowedFailures)
this.verificationStatus = AuthPromptStatus.VERIFICATION_FAILED;
} else {
this.verificationStatus = AuthPromptStatus.VERIFICATION_CANCELLED;
}
this.reset();
}
});