mirror of
https://github.com/vale981/rmview
synced 2025-03-05 09:11:39 -05:00
Proper Known Hosts handling
- Dialog offers to Add/Update host keys - `host_key_policy` setting
This commit is contained in:
parent
9108221e91
commit
057e227438
3 changed files with 130 additions and 47 deletions
41
README.md
41
README.md
|
@ -105,15 +105,15 @@ All the settings are optional.
|
|||
|
||||
Connection parameters are provided as a dictionary with the following keys (all optional):
|
||||
|
||||
| Parameter | Values | Comments |
|
||||
| ------------------------ | -------------------------------------- | ------------------------------------- |
|
||||
| `address` | IP of remarkable | tool prompts for it if missing |
|
||||
| `auth_method` | Either `"password"` or `"key"` | defaults to password if key not given |
|
||||
| `username` | Username for ssh access on reMarkable | default: `"root"` |
|
||||
| `password` | Password provided by reMarkable | not needed if key provided |
|
||||
| `key` | Local path to key for ssh | not needed if password provided |
|
||||
| `timeout` | Connection timeout in seconds | default: 1 |
|
||||
| `insecure_auto_add_host` | Ignores the check on the fingerprint | default: `false` |
|
||||
| Parameter | Values | Comments |
|
||||
| ----------------- | ------------------------------------------------------- | ------------------------------------- |
|
||||
| `address` | IP of remarkable | tool prompts for it if missing |
|
||||
| `auth_method` | Either `"password"` or `"key"` | defaults to password if key not given |
|
||||
| `username` | Username for ssh access on reMarkable | default: `"root"` |
|
||||
| `password` | Password provided by reMarkable | not needed if key provided |
|
||||
| `key` | Local path to key for ssh | not needed if password provided |
|
||||
| `timeout` | Connection timeout in seconds | default: 1 |
|
||||
| `host_key_policy` | `"ask"`, `"ignore_new"`, `"ignore_all"`, `"auto_add"` | default: `"ask"` (description below) |
|
||||
|
||||
The `address` parameter can be either:
|
||||
- a single string, in which case the address is used for connection
|
||||
|
@ -124,17 +124,24 @@ To establish a connection with the tablet, you can use any of the following:
|
|||
- Specify `"auth_method": "key"` to use a SSH key. In case an SSH key hasn't already been associated with the tablet, you can provide its path with the `key` setting.
|
||||
- Provide a `password` in settings
|
||||
|
||||
|
||||
If `auth_method` is `password` but no password is specified, then the tool will ask for the password on connection.
|
||||
|
||||
As a security measure, the keys used by known hosts are checked at each connection to prevent man-in-the-middle attacks.
|
||||
The first time you connect to the tablet, it will not be among the known hosts.
|
||||
In this situation rMview will present the option to add it to the known hosts, which should be done in a trusted network.
|
||||
Updates to the tablet's firmware modify the key used by it, so the next connection would see the mismatch between the old key and the new.
|
||||
Again rMview would prompt the user in this case with the option to update the key. Again this should be done in a trusted network.
|
||||
The `host_key_policy` parameter controls this behaviour:
|
||||
- `"ask"` is the default behaviour and prompts the user with a choice when the host key is unknown or not matching.
|
||||
- `"ignore_new"` ignores unknown keys but reports mismatches.
|
||||
- `"ignore_all"` ignores both unknown and not matching keys. Use at your own risk.
|
||||
- `"auto_add"` adds unknown keys without prompting but reports mismatches.
|
||||
|
||||
The old `"insecure_auto_add_host": true` parameter is deprecated and equivalent to `"ignore_all"`.
|
||||
|
||||
In case your `~/.ssh/known_hosts` file contains the relevant key associations, rMview should pick them up.
|
||||
If you use the "Add/Update" feature when prompted by rMview (for example after a tablet update) then `~/.ssh/known_hosts` will be ignored from then on.
|
||||
|
||||
:warning: **Connecting after an update:**
|
||||
An update to the reMarkable tablet would change its "fingerprint" i.e. the identifier that signals we are connecting to the expected device (and not somebody impersonating it).
|
||||
When this happens you may get an error message upon connection.
|
||||
There are two main ways to fix this:
|
||||
1. Change your `~/.ssh/known_hosts` file to match the new fingerprint (you can get instructions by connecting manually via ssh).
|
||||
2. Set the `insecure_auto_add_host` setting to `true`, which will make rmview ignore the check.
|
||||
This is not recommended unless you are in a trusted network.
|
||||
|
||||
:warning: **Key format error:**
|
||||
If you get an error when connect using a key, but the key seems ok when connecting manually with ssh, you probably need to convert the key to the PEM format (or re-generate it using the `-m PEM` option of `ssh-keygen`). See [here](https://github.com/paramiko/paramiko/issues/340#issuecomment-492448662) for details.
|
||||
|
|
|
@ -6,6 +6,7 @@ from PyQt5.QtWidgets import *
|
|||
import paramiko
|
||||
import struct
|
||||
import time
|
||||
from binascii import hexlify
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
@ -13,6 +14,43 @@ import logging
|
|||
log = logging.getLogger('rmview')
|
||||
|
||||
|
||||
class UnknownHostKeyException(paramiko.SSHException):
|
||||
|
||||
def __init__(self, hostname, key):
|
||||
paramiko.SSHException.__init__(self, hostname, key)
|
||||
self.hostname = hostname
|
||||
self.key = key
|
||||
|
||||
def __str__(self):
|
||||
msg = "Unknown host key for server '{}': got '{}'"
|
||||
return msg.format(
|
||||
self.hostname,
|
||||
self.key.get_base64(),
|
||||
)
|
||||
|
||||
AddNewHostKey = paramiko.AutoAddPolicy
|
||||
|
||||
|
||||
class RejectNewHostKey(paramiko.MissingHostKeyPolicy):
|
||||
|
||||
def missing_host_key(self, client, hostname, key):
|
||||
raise UnknownHostKeyException(hostname, key)
|
||||
|
||||
|
||||
class IgnoreNewHostKey(paramiko.MissingHostKeyPolicy):
|
||||
|
||||
def missing_host_key(self, client, hostname, key):
|
||||
log.warning("Unknown %s host key for %s: %s", key.get_name(), hostname, hexlify(key.get_fingerprint()))
|
||||
|
||||
|
||||
HOST_KEY_POLICY = {
|
||||
"ask": RejectNewHostKey,
|
||||
"ignore_new": IgnoreNewHostKey,
|
||||
"ignore_all": IgnoreNewHostKey,
|
||||
"auto_add": AddNewHostKey
|
||||
}
|
||||
|
||||
|
||||
|
||||
class rMConnectSignals(QObject):
|
||||
onConnect = pyqtSignal(object)
|
||||
|
@ -23,7 +61,8 @@ class rMConnect(QRunnable):
|
|||
|
||||
_exception = None
|
||||
|
||||
def __init__(self, address='10.11.99.1', username='root', password=None, key=None, timeout=1, onConnect=None, onError=None, insecure_auto_add_host=False, **kwargs):
|
||||
def __init__(self, address='10.11.99.1', username='root', password=None, key=None, timeout=1,
|
||||
onConnect=None, onError=None, host_key_policy=None, known_hosts=None, **kwargs):
|
||||
super(rMConnect, self).__init__()
|
||||
self.address = address
|
||||
self.signals = rMConnectSignals()
|
||||
|
@ -31,13 +70,23 @@ class rMConnect(QRunnable):
|
|||
self.signals.onConnect.connect(onConnect)
|
||||
if callable(onError):
|
||||
self.signals.onError.connect(onError)
|
||||
# self.key = key
|
||||
|
||||
try:
|
||||
self.client = paramiko.SSHClient()
|
||||
if insecure_auto_add_host:
|
||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
else:
|
||||
self.client.load_system_host_keys()
|
||||
|
||||
if host_key_policy != "ignore_all":
|
||||
if known_hosts and os.path.isfile(known_hosts):
|
||||
self.client.load_host_keys(known_hosts)
|
||||
log.info("LOADED %s", known_hosts)
|
||||
else:
|
||||
# ideally we would want to always load the system ones
|
||||
# and have the local keys have precedence, but paramiko gives
|
||||
# always precedence to system keys
|
||||
self.client.load_system_host_keys()
|
||||
|
||||
|
||||
policy = HOST_KEY_POLICY.get(host_key_policy, RejectNewHostKey)
|
||||
self.client.set_missing_host_key_policy(policy())
|
||||
|
||||
if key is not None:
|
||||
key = os.path.expanduser(key)
|
||||
|
@ -59,7 +108,6 @@ class rMConnect(QRunnable):
|
|||
'password': password,
|
||||
'pkey': pkey,
|
||||
'timeout': timeout,
|
||||
'look_for_keys': False
|
||||
}
|
||||
except Exception as e:
|
||||
self._exception = e
|
||||
|
|
|
@ -4,10 +4,10 @@ from PyQt5.QtCore import *
|
|||
|
||||
from . import resources
|
||||
from .workers import FrameBufferWorker, PointerWorker
|
||||
from .connection import rMConnect
|
||||
from .connection import rMConnect, RejectNewHostKey, AddNewHostKey, UnknownHostKeyException
|
||||
from .viewer import QtImageViewer
|
||||
|
||||
from paramiko import BadHostKeyException, SSHException
|
||||
from paramiko import BadHostKeyException, HostKeys
|
||||
|
||||
from .rmparams import *
|
||||
|
||||
|
@ -38,8 +38,9 @@ class rMViewApp(QApplication):
|
|||
def __init__(self, args):
|
||||
super(rMViewApp, self).__init__(args)
|
||||
|
||||
self.DEFAULT_CONFIG = QStandardPaths.standardLocations(QStandardPaths.ConfigLocation)[0]
|
||||
self.DEFAULT_CONFIG = os.path.join(self.DEFAULT_CONFIG, 'rmview.json')
|
||||
self.CONFIG_DIR = QStandardPaths.standardLocations(QStandardPaths.ConfigLocation)[0]
|
||||
self.DEFAULT_CONFIG = os.path.join(self.CONFIG_DIR, 'rmview.json')
|
||||
self.LOCAL_KNOWN_HOSTS = os.path.join(self.CONFIG_DIR, 'rmview_known_hosts')
|
||||
|
||||
config_files = [] if len(args) < 2 else [args[1]]
|
||||
config_files += ['rmview.json', self.DEFAULT_CONFIG]
|
||||
|
@ -175,13 +176,27 @@ class rMViewApp(QApplication):
|
|||
else:
|
||||
return False
|
||||
|
||||
# backwards compatibility
|
||||
if self.config['ssh'].get('insecure_auto_add_host') and 'host_key_policy' not in self.config['ssh']:
|
||||
log.warning("The 'insecure_auto_add_host' setting is deprecated, see documentation.")
|
||||
self.config['ssh']['host_key_policy'] = "ignore_all"
|
||||
|
||||
if self.config['ssh']['host_key_policy'] == "auto_add":
|
||||
if not os.path.isfile(self.LOCAL_KNOWN_HOSTS):
|
||||
open(self.LOCAL_KNOWN_HOSTS, 'a').close()
|
||||
|
||||
log.info(self.config)
|
||||
return True
|
||||
|
||||
def requestConnect(self):
|
||||
def requestConnect(self, host_key_policy=None):
|
||||
self.viewer.setWindowTitle("rMview - Connecting...")
|
||||
args = self.config.get('ssh')
|
||||
if host_key_policy:
|
||||
args = args.copy()
|
||||
args['host_key_policy'] = host_key_policy
|
||||
self.threadpool.start(
|
||||
rMConnect(**self.config.get('ssh'),
|
||||
rMConnect(**args,
|
||||
known_hosts=self.LOCAL_KNOWN_HOSTS,
|
||||
onError=self.connectionError,
|
||||
onConnect=self.connected ) )
|
||||
|
||||
|
@ -200,8 +215,6 @@ class rMViewApp(QApplication):
|
|||
self.ssh = ssh
|
||||
self.viewer.setWindowTitle("rMview - " + self.config.get('ssh').get('address'))
|
||||
|
||||
# check we are dealing with RM1
|
||||
# cat /sys/devices/soc0/machine -> reMarkable 1.x
|
||||
_,out,_ = ssh.exec_command("cat /sys/devices/soc0/machine")
|
||||
rmv = out.read().decode("utf-8")
|
||||
version = re.fullmatch(r"reMarkable (\d+)\..*\n", rmv)
|
||||
|
@ -358,48 +371,63 @@ class rMViewApp(QApplication):
|
|||
icon = QPixmap(":/assets/dead.svg")
|
||||
icon.setDevicePixelRatio(self.devicePixelRatio())
|
||||
mbox.setIconPixmap(icon)
|
||||
mbox.addButton("Settings...", QMessageBox.ResetRole)
|
||||
mbox.addButton(QMessageBox.Cancel)
|
||||
if isinstance(e, BadHostKeyException):
|
||||
mbox.setDetailedText(str(e))
|
||||
mbox.setInformativeText(
|
||||
"<big>The host at %s has the wrong key.<br>"
|
||||
"This usually happens just after a software update on the tablet.</big><br><br>"
|
||||
"You have three options to fix this problem:"
|
||||
"You have three options to fix this permanently:"
|
||||
"<ol><li>"
|
||||
"Press Update to replace the old key with the new."
|
||||
"<br></li><li>"
|
||||
"Change your <code>~/.ssh/known_hosts</code> file to match the new fingerprint.<br>"
|
||||
"The easiest way to do this is connecting manually via ssh and follow the instructions."
|
||||
"<br></li><li>"
|
||||
"Set <code>\"insecure_auto_add_host\": true</code> to rmView\'s settings.<br>"
|
||||
"Set <code>\"host_key_policy\": \"ignore_new\"</code> in the <code>ssh</code> section of rmView\'s settings.<br>"
|
||||
"This is not recommended unless you are in a trusted network."
|
||||
"<br></li><li>"
|
||||
"Connect using username/password."
|
||||
"<br></li><ol>" % (self.config.get('ssh').get('address'))
|
||||
"<br></li><ol>" % (e.hostname)
|
||||
)
|
||||
elif isinstance(e, SSHException) and str(e).endswith('known_hosts'):
|
||||
mbox.addButton("Ignore", QMessageBox.NoRole)
|
||||
mbox.addButton("Update", QMessageBox.YesRole)
|
||||
elif isinstance(e, UnknownHostKeyException):
|
||||
mbox.setDetailedText(str(e))
|
||||
mbox.setInformativeText(
|
||||
"<big>The host at %s is unknown.<br>"
|
||||
"This usually happens if this is the first time you use ssh with your tablet.</big><br><br>"
|
||||
"You have three options to fix this problem:"
|
||||
"You have three options to fix this permanently:"
|
||||
"<ol><li>"
|
||||
"Press Add to add the key to the known hosts."
|
||||
"<br></li><li>"
|
||||
"Change your <code>~/.ssh/known_hosts</code> file to match the new fingerprint.<br>"
|
||||
"The easiest way to do this is connecting manually via ssh and follow the instructions."
|
||||
"<br></li><li>"
|
||||
"Set <code>\"insecure_auto_add_host\": true</code> to rmView\'s settings.<br>"
|
||||
"Set <code>\"host_key_policy\": \"ignore_new\"</code> in the <code>ssh</code> section of rmView\'s settings.<br>"
|
||||
"This is not recommended unless you are in a trusted network."
|
||||
"<br></li><li>"
|
||||
"Connect using username/password."
|
||||
"<br></li><ol>" % (self.config.get('ssh').get('address'))
|
||||
"<br></li><ol>" % (e.hostname)
|
||||
)
|
||||
mbox.addButton("Ignore", QMessageBox.NoRole)
|
||||
mbox.addButton("Add", QMessageBox.YesRole)
|
||||
else:
|
||||
mbox.setInformativeText("I could not connect to the reMarkable at %s:\n%s." % (self.config.get('ssh').get('address'), e))
|
||||
mbox.addButton(QMessageBox.Cancel)
|
||||
mbox.addButton("Settings...", QMessageBox.ResetRole)
|
||||
mbox.addButton(QMessageBox.Retry)
|
||||
mbox.setDefaultButton(QMessageBox.Retry)
|
||||
mbox.addButton(QMessageBox.Retry)
|
||||
mbox.setDefaultButton(QMessageBox.Retry)
|
||||
answer = mbox.exec()
|
||||
if answer == QMessageBox.Retry:
|
||||
self.requestConnect()
|
||||
elif answer == QMessageBox.Cancel:
|
||||
self.quit()
|
||||
elif answer == 1: # Ignore
|
||||
self.requestConnect(host_key_policy="ignore_all")
|
||||
elif answer == 2: # Add/Update
|
||||
if not os.path.isfile(self.LOCAL_KNOWN_HOSTS):
|
||||
open(self.LOCAL_KNOWN_HOSTS, 'a').close()
|
||||
hk = HostKeys(self.LOCAL_KNOWN_HOSTS)
|
||||
hk.add(e.hostname, e.key.get_name(), e.key)
|
||||
hk.save(self.LOCAL_KNOWN_HOSTS)
|
||||
log.info("Saved host key in %s", self.LOCAL_KNOWN_HOSTS)
|
||||
self.requestConnect()
|
||||
else:
|
||||
self.openSettings(prompt=False)
|
||||
self.quit()
|
||||
|
|
Loading…
Add table
Reference in a new issue