Commit 9d75c779 authored by Philipp Hörist's avatar Philipp Hörist

Remove GPG code from Gajim

Code moved into plugin
parent 64bac1d9
Pipeline #3314 passed with stages
in 2 minutes and 54 seconds
......@@ -179,11 +179,6 @@ def on_merge_accounts(action, param):
app.interface.roster.setup_and_draw_roster()
def on_use_pgp_agent(action, param):
action.set_state(param)
app.config.set('use_gpg_agent', param.get_boolean())
def on_add_account(action, param):
if 'account_creation_wizard' in app.interface.instances:
app.interface.instances['account_creation_wizard'].window.present()
......
......@@ -388,12 +388,6 @@ class GajimApplication(Gtk.Application):
act.connect('change-state', app_actions.on_merge_accounts)
self.add_action(act)
act = Gio.SimpleAction.new_stateful(
'agent', None,
GLib.Variant.new_boolean(app.config.get('use_gpg_agent')))
act.connect('change-state', app_actions.on_use_pgp_agent)
self.add_action(act)
# General Actions
general_actions = [
......
......@@ -399,12 +399,8 @@ class ChatControl(ChatControlBase):
'Show a list of formattings'))
else:
self._formattings_button.set_sensitive(False)
if self.contact.supports(NS_XHTML_IM):
self._formattings_button.set_tooltip_text(_('Formatting is not '
'available so long as GPG is active'))
else:
self._formattings_button.set_tooltip_text(_('This contact does '
'not support HTML'))
self._formattings_button.set_tooltip_text(
_('This contact does not support HTML'))
# Jingle detection
if self.contact.supports(NS_JINGLE_ICE_UDP) and \
......@@ -922,7 +918,7 @@ class ChatControl(ChatControlBase):
correct_id=obj.correct_id,
additional_data=obj.additional_data)
def send_message(self, message, keyID='', xhtml=None,
def send_message(self, message, xhtml=None,
process_commands=True, attention=False):
"""
Send a message to contact
......@@ -939,12 +935,8 @@ class ChatControl(ChatControlBase):
if message in ('', None, '\n'):
return None
contact = self.contact
keyID = contact.keyID
ChatControlBase.send_message(self,
message,
keyID,
type_='chat',
xhtml=xhtml,
process_commands=process_commands,
......@@ -1426,9 +1418,6 @@ class ChatControl(ChatControlBase):
self.update_actions()
def update_status_display(self, name, uf_show, status):
"""
Print the contact's status and update the status/GPG image
"""
self.update_ui()
self.parent_win.redraw_tab(self)
......
......@@ -773,7 +773,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
label = labels[lname]
return label
def send_message(self, message, keyID='', type_='chat',
def send_message(self, message, type_='chat',
resource=None, xhtml=None, process_commands=True, attention=False):
"""
Send the given message to the active tab. Doesn't return None if error
......@@ -797,7 +797,7 @@ class ChatControlBase(MessageControl, ChatCommandProcessor, CommandTools):
app.nec.push_outgoing_event(MessageOutgoingEvent(None,
account=self.account, jid=self.contact.jid, message=message,
keyID=keyID, type_=type_, chatstate=chatstate,
type_=type_, chatstate=chatstate,
resource=resource, user_nick=self.user_nick, xhtml=xhtml,
label=label, control=self, attention=attention, correct_id=correct_id,
automatic_message=False, encryption=self.encryption))
......
......@@ -35,7 +35,6 @@ import sys
import logging
import uuid
from pathlib import Path
from distutils.version import LooseVersion as V
from collections import namedtuple
import nbxmpp
......@@ -191,8 +190,6 @@ caps_hash = {} # type: Dict[str, List[str]]
_dependencies = {
'AVAHI': False,
'PYBONJOUR': False,
'PYGPG': False,
'GPG_BINARY': False,
'FARSTREAM': False,
'GEOCLUE': False,
'UPNP': False,
......@@ -203,9 +200,6 @@ _dependencies = {
def is_installed(dependency):
if dependency == 'GPG':
# Alias for checking python-gnupg and the GPG binary
return _dependencies['PYGPG'] and _dependencies['GPG_BINARY']
if dependency == 'ZEROCONF':
# Alias for checking zeroconf libs
return _dependencies['AVAHI'] or _dependencies['PYBONJOUR']
......@@ -246,40 +240,6 @@ def detect_dependencies():
except Exception:
pass
# python-gnupg
try:
import gnupg
# We need https://pypi.python.org/pypi/python-gnupg
# but https://pypi.python.org/pypi/gnupg shares the same package name.
# It cannot be used as a drop-in replacement.
# We test with a version check if python-gnupg is installed as it is
# on a much lower version number than gnupg
# Also we need at least python-gnupg 0.3.8
v_gnupg = gnupg.__version__
if V(v_gnupg) < V('0.3.8') or V(v_gnupg) > V('1.0.0'):
log('gajim').info('Gajim needs python-gnupg >= 0.3.8')
raise ImportError
_dependencies['PYGPG'] = True
except ImportError:
pass
# GPG BINARY
import subprocess
def test_gpg(binary='gpg'):
if os.name == 'nt':
gpg_cmd = binary + ' -h >nul 2>&1'
else:
gpg_cmd = binary + ' -h >/dev/null 2>&1'
if subprocess.call(gpg_cmd, shell=True):
return False
return True
if test_gpg(binary='gpg2'):
_dependencies['GPG_BINARY'] = 'gpg2'
elif test_gpg(binary='gpg'):
_dependencies['GPG_BINARY'] = 'gpg'
# FARSTREAM
try:
if os.name == 'nt':
......@@ -354,9 +314,6 @@ def detect_dependencies():
log('gajim').info('Used language: %s', LANG)
def get_gpg_binary():
return _dependencies['GPG_BINARY']
def get_an_id():
return str(uuid.uuid4())
......
......@@ -167,7 +167,6 @@ class Config:
'time_stamp': [opt_str, '[%X] ', _('This option let you customize timestamp that is printed in conversation. For example "[%H:%M] " will show "[hour:minute] ". See python doc on strftime for full documentation: http://docs.python.org/lib/module-time.html')],
'before_nickname': [opt_str, '', _('Characters that are printed before the nickname in conversations')],
'after_nickname': [opt_str, ':', _('Characters that are printed after the nickname in conversations')],
'use_gpg_agent': [opt_bool, False],
'change_roster_title': [opt_bool, True, _('Add * and [n] in roster title?')],
'restore_lines': [opt_int, 10, _('How many history messages should be restored when a chat tab/window is reopened?')],
'restore_timeout': [opt_int, -1, _('How far back in time (minutes) history is restored. -1 means no limit.')],
......@@ -285,7 +284,6 @@ class Config:
'positive_184_ack': [opt_bool, False, _('If enabled, Gajim will show an icon to show that sent message has been received by your contact')],
'show_avatar_in_tabs': [opt_bool, False, _('Show a mini avatar in chat window tabs and in window icon')],
'use_keyring': [opt_bool, True, _('If true, Gajim will use the Systems Keyring to store account passwords.')],
'pgp_encoding': [opt_str, '', _('Sets the encoding used by python-gnupg'), True],
'remote_commands': [opt_bool, False, _('If true, Gajim will execute XEP-0146 Commands.')],
'dark_theme': [opt_int, 2, _('2: System, 1: Enabled, 0: Disabled')],
'threshold_options': [opt_str, '1, 2, 4, 10, 0', _('Options in days which can be chosen in the sync threshold menu'), True],
......@@ -325,7 +323,6 @@ class Config:
'active': [opt_bool, True, _('If False, this account will be disabled and will not appear in roster window.'), True],
'proxy': [opt_str, '', '', True],
'keyid': [opt_str, '', '', True],
'gpg_sign_presence': [opt_bool, True, _('If disabled, don\'t sign presences with GPG key, even if GPG is configured.')],
'keyname': [opt_str, '', '', True],
'allow_plaintext_connection': [opt_bool, False, _('Allow plaintext connections')],
'tls_version': [opt_str, '1.2', ''],
......
This diff is collapsed.
......@@ -153,15 +153,6 @@ class NewAccountConnectedEvent(nec.NetworkIncomingEvent):
class NewAccountNotConnectedEvent(nec.NetworkIncomingEvent):
name = 'new-account-not-connected'
class BadGPGPassphraseEvent(nec.NetworkIncomingEvent):
name = 'bad-gpg-passphrase'
def generate(self):
self.account = self.conn.name
self.use_gpg_agent = app.config.get('use_gpg_agent')
self.keyID = app.config.get_per('accounts', self.conn.name, 'keyid')
return True
class ConnectionLostEvent(nec.NetworkIncomingEvent):
name = 'connection-lost'
......@@ -170,13 +161,6 @@ class ConnectionLostEvent(nec.NetworkIncomingEvent):
show='offline'))
return True
class GPGPasswordRequiredEvent(nec.NetworkIncomingEvent):
name = 'gpg-password-required'
def generate(self):
self.keyid = app.config.get_per('accounts', self.conn.name, 'keyid')
return True
class FileRequestReceivedEvent(nec.NetworkIncomingEvent):
name = 'file-request-received'
......@@ -610,7 +594,6 @@ class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
def init(self):
self.additional_data = AdditionalDataDict()
self.message = None
self.keyID = None
self.type_ = 'chat'
self.kind = None
self.timestamp = None
......
......@@ -23,7 +23,6 @@ class OptionKind(IntEnum):
PRIORITY = 9
FILECHOOSER = 10
CHANGEPASSWORD = 11
GPG = 12
@unique
......
......@@ -137,7 +137,7 @@ class Contact(CommonContact):
Information concerning a contact
"""
def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
sub='', ask='', resource='', priority=0, client_caps=None,
chatstate=None, idle_time=None, avatar_sha=None, groupchat=False,
is_pm_contact=False):
if not isinstance(jid, str):
......@@ -159,7 +159,6 @@ class Contact(CommonContact):
self.ask = ask
self.priority = priority
self.keyID = keyID
self.idle_time = idle_time
self.pep = {}
......@@ -306,7 +305,7 @@ class LegacyContactsAPI:
self._metacontact_manager.remove_account(account)
def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0, keyID='',
status='', sub='', ask='', resource='', priority=0,
client_caps=None, chatstate=None, idle_time=None,
avatar_sha=None, groupchat=False):
if groups is None:
......@@ -315,36 +314,36 @@ class LegacyContactsAPI:
account = self._accounts.get(account, account)
return Contact(jid=jid, account=account, name=name, groups=groups,
show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority, keyID=keyID, client_caps=client_caps,
priority=priority, client_caps=client_caps,
chatstate=chatstate, idle_time=idle_time, avatar_sha=avatar_sha,
groupchat=groupchat)
def create_self_contact(self, jid, account, resource, show, status, priority,
name='', keyID=''):
name=''):
conn = common.app.connections[account]
nick = name or common.app.nicks[account]
account = self._accounts.get(account, account) # Use Account object if available
self_contact = self.create_contact(jid=jid, account=account,
name=nick, groups=['self_contact'], show=show, status=status,
sub='both', ask='none', priority=priority, keyID=keyID,
sub='both', ask='none', priority=priority,
resource=resource)
self_contact.pep = conn.pep
return self_contact
def create_not_in_roster_contact(self, jid, account, resource='', name='',
keyID='', groupchat=False):
groupchat=False):
# Use Account object if available
account = self._accounts.get(account, account)
return self.create_contact(jid=jid, account=account, resource=resource,
name=name, groups=[_('Not in Roster')], show='not in roster',
status='', sub='none', keyID=keyID, groupchat=groupchat)
status='', sub='none', groupchat=groupchat)
def copy_contact(self, contact):
return self.create_contact(contact.jid, contact.account,
name=contact.name, groups=contact.groups, show=contact.show,
status=contact.status, sub=contact.sub, ask=contact.ask,
resource=contact.resource, priority=contact.priority,
keyID=contact.keyID, client_caps=contact.client_caps,
client_caps=contact.client_caps,
chatstate=contact.chatstate_enum,
idle_time=contact.idle_time, avatar_sha=contact.avatar_sha)
......
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
# Copyright (C) 2008 Jean-Marie Traissard <jim AT lapin.org>
# Jonathan Schleifer <js-gajim AT webkeks.org>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import os
from gajim.common import app
if app.is_installed('GPG'):
import gnupg
class GnuPG(gnupg.GPG):
def __init__(self):
use_agent = app.config.get('use_gpg_agent')
gnupg.GPG.__init__(self, gpgbinary=app.get_gpg_binary(), use_agent=use_agent)
encoding = app.config.get('pgp_encoding')
if encoding:
self.encoding = encoding
self.decode_errors = 'replace'
self.passphrase = None
self.always_trust = [] # list of keyID to always trust
def encrypt(self, str_, recipients, always_trust=False):
trust = always_trust
if not trust:
# check if we trust all keys
trust = True
for key in recipients:
if key not in self.always_trust:
trust = False
if not trust:
# check that we'll be able to encrypt
result = super(GnuPG, self).list_keys(keys=recipients)
for key in result:
if key['trust'] not in ('f', 'u'):
if key['keyid'][-8:] not in self.always_trust:
return '', 'NOT_TRUSTED ' + key['keyid'][-8:]
trust = True
result = super(GnuPG, self).encrypt(str_.encode('utf8'), recipients,
always_trust=trust, passphrase=self.passphrase)
if result.ok:
error = ''
else:
error = result.status
return self._stripHeaderFooter(str(result)), error
def decrypt(self, str_, keyID):
data = self._addHeaderFooter(str_, 'MESSAGE')
result = super(GnuPG, self).decrypt(data.encode('utf8'),
passphrase=self.passphrase)
return result.data.decode('utf8')
def sign(self, str_, keyID):
result = super(GnuPG, self).sign(str_.encode('utf8'), keyid=keyID, detach=True,
passphrase=self.passphrase)
if result.fingerprint:
return self._stripHeaderFooter(str(result))
if hasattr(result, 'status') and result.status == 'key expired':
return 'KEYEXPIRED'
return 'BAD_PASSPHRASE'
def verify(self, str_, sign):
if str_ is None:
return ''
# Hash algorithm is not transfered in the signed presence stanza so try
# all algorithms. Text name for hash algorithms from RFC 4880 - section 9.4
hash_algorithms = ['SHA512', 'SHA384', 'SHA256', 'SHA224', 'SHA1', 'RIPEMD160']
for algo in hash_algorithms:
data = os.linesep.join(
['-----BEGIN PGP SIGNED MESSAGE-----',
'Hash: ' + algo,
'',
str_,
self._addHeaderFooter(sign, 'SIGNATURE')]
)
result = super(GnuPG, self).verify(data.encode('utf8'))
if result.valid:
return result.key_id
return ''
def get_key(self, keyID):
return super(GnuPG, self).list_keys(keys=[keyID])
def get_keys(self, secret=False):
keys = {}
result = super(GnuPG, self).list_keys(secret=secret)
for key in result:
# Take first not empty uid
keys[key['keyid'][8:]] = [uid for uid in key['uids'] if uid][0]
return keys
def get_secret_keys(self):
return self.get_keys(True)
def _stripHeaderFooter(self, data):
"""
Remove header and footer from data
"""
if not data:
return ''
lines = data.splitlines()
while lines[0] != '':
lines.remove(lines[0])
while lines[0] == '':
lines.remove(lines[0])
i = 0
for line in lines:
if line:
if line[0] == '-':
break
i = i+1
line = '\n'.join(lines[0:i])
return line
def _addHeaderFooter(self, data, type_):
"""
Add header and footer from data
"""
out = "-----BEGIN PGP %s-----" % type_ + os.linesep
out = out + "Version: PGP" + os.linesep
out = out + os.linesep
out = out + data + os.linesep
out = out + "-----END PGP %s-----" % type_ + os.linesep
return out
......@@ -1108,54 +1108,6 @@ def get_current_show(account):
status = app.connections[account].connected
return app.SHOW_LIST[status]
def prepare_and_validate_gpg_keyID(account, jid, keyID):
"""
Return an eight char long keyID that can be used with for GPG encryption
with this contact
If the given keyID is None, return UNKNOWN; if the key does not match the
assigned key XXXXXXXXMISMATCH is returned. If the key is trusted and not yet
assigned, assign it.
"""
if app.connections[account].USE_GPG:
if keyID and len(keyID) == 16:
keyID = keyID[8:]
attached_keys = app.config.get_per('accounts', account,
'attached_gpg_keys').split()
if jid in attached_keys and keyID:
attachedkeyID = attached_keys[attached_keys.index(jid) + 1]
if attachedkeyID != keyID:
# Get signing subkeys for the attached key
subkeys = []
for key in app.connections[account].gpg.list_keys():
if key['keyid'][8:] == attachedkeyID:
subkeys = [subkey[0][8:] for subkey in key['subkeys'] \
if subkey[1] == 's']
break
if keyID not in subkeys:
# Mismatch! Another gpg key was expected
keyID += 'MISMATCH'
elif jid in attached_keys:
# An unsigned presence, just use the assigned key
keyID = attached_keys[attached_keys.index(jid) + 1]
elif keyID:
full_key = app.connections[account].ask_gpg_keys(keyID=keyID)
# Assign the corresponding key, if we have it in our keyring
if full_key:
for u in app.contacts.get_contacts(account, jid):
u.keyID = keyID
keys_str = app.config.get_per('accounts', account,
'attached_gpg_keys')
keys_str += jid + ' ' + keyID + ' '
app.config.set_per('accounts', account, 'attached_gpg_keys',
keys_str)
elif keyID is None:
keyID = 'UNKNOWN'
return keyID
def update_optional_features(account=None):
if account is not None:
accounts = [account]
......
......@@ -329,7 +329,7 @@ class HTTPUpload(BaseModule):
else:
app.nec.push_outgoing_event(MessageOutgoingEvent(
None, account=self._account, jid=file.contact.jid,
message=message, keyID=file.key_id, type_='chat',
message=message, type_='chat',
automatic_message=False, session=file.session))
else:
......@@ -353,9 +353,6 @@ class File:
setattr(self, key, val)
self.encrypted = False
self.contact = contact
self.key_id = None
if hasattr(contact, 'keyID'):
self.key_id = contact.keyID
self.stream = None
self.path = path
self.put = None
......
......@@ -25,7 +25,6 @@ from gajim.common.i18n import _
from gajim.common.nec import NetworkEvent
from gajim.common.const import KindConstant
from gajim.common.const import ShowConstant
from gajim.common.helpers import prepare_and_validate_gpg_keyID
from gajim.common.modules.base import BaseModule
......@@ -94,12 +93,6 @@ class Presence(BaseModule):
self._log.warning(stanza)
return
key_id = ''
if properties.signed is not None and self._con.USE_GPG:
key_id = self._con.gpg.verify(properties.status, properties.signed)
key_id = prepare_and_validate_gpg_keyID(
self._account, properties.jid.getBare(), key_id)
show = properties.show.value
if properties.type.is_unavailable:
show = 'offline'
......@@ -107,7 +100,6 @@ class Presence(BaseModule):
event_attrs = {
'conn': self._con,
'stanza': stanza,
'keyID': key_id,
'prio': properties.priority,
'need_add_in_roster': False,
'popup': False,
......@@ -185,13 +177,6 @@ class Presence(BaseModule):
contact.show = event.show
contact.status = properties.status
contact.priority = properties.priority
attached_keys = app.config.get_per('accounts', self._account,
'attached_gpg_keys').split()
if jid in attached_keys:
contact.keyID = attached_keys[attached_keys.index(jid) + 1]
else:
# Do not override assigned key
contact.keyID = event.keyID
contact.idle_time = properties.idle_timestamp
event.contact = contact
......@@ -355,7 +340,7 @@ class Presence(BaseModule):
def get_presence(self, to=None, typ=None, priority=None,
show=None, status=None, nick=None, caps=True,
sign=None, idle_time=None):
idle_time=None):
if show not in ('chat', 'away', 'xa', 'dnd'):
# Gajim sometimes passes invalid show values here
# until this is fixed this is a workaround
......@@ -365,9 +350,6 @@ class Presence(BaseModule):
nick_tag = presence.setTag('nick', namespace=nbxmpp.NS_NICK)
nick_tag.setData(nick)
if sign:
presence.setTag(nbxmpp.NS_SIGNED + ' x').setData(sign)
if idle_time is not None:
idle_node = presence.setTag('idle', namespace=nbxmpp.NS_IDLE)
idle_node.setAttr('since', idle_time)
......
......@@ -156,7 +156,7 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
def _on_remove_service(self, jid):
self.roster.delItem(jid)
# 'NOTIFY' (account, (jid, status, status message, resource, priority,
# keyID, timestamp))
# timestamp))
self._on_presence(jid, show='offline', status='')
def _on_presence(self, jid, show=None, status=None):
......@@ -169,7 +169,6 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
event_attrs = {
'conn': self,
'keyID': None,
'prio': 0,
'need_add_in_roster': False,
'popup': False,
......@@ -216,13 +215,6 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
contact.show = event.show
contact.status = event.status
contact.priority = event.prio
attached_keys = app.config.get_per('accounts', self.name,
'attached_gpg_keys').split()
if jid in attached_keys:
contact.keyID = attached_keys[attached_keys.index(jid) + 1]
else:
# Do not override assigned key
contact.keyID = event.keyID
contact.idle_time = event.idle_time
event.contact = contact
......@@ -363,7 +355,7 @@ class ConnectionZeroconf(CommonConnection, ConnectionHandlersZeroconf):
else:
self.reannounce()
def connect_and_init(self, show, msg, sign_msg):
def connect_and_init(self, show, msg):
# to check for errors from zeroconf
check = True
if not self.connect(show, msg):
......
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkDialog" id="choose_gpg_key_dialog">
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="default_width">550</property>
<property name="default_height">300</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="vbox33">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="hbuttonbox14">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="button26">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="button27">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="vbox91">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="prompt_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>