mirror of
https://github.com/vale981/rmview
synced 2025-03-05 17:21: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
27
README.md
27
README.md
|
@ -106,14 +106,14 @@ All the settings are optional.
|
||||||
Connection parameters are provided as a dictionary with the following keys (all optional):
|
Connection parameters are provided as a dictionary with the following keys (all optional):
|
||||||
|
|
||||||
| Parameter | Values | Comments |
|
| Parameter | Values | Comments |
|
||||||
| ------------------------ | -------------------------------------- | ------------------------------------- |
|
| ----------------- | ------------------------------------------------------- | ------------------------------------- |
|
||||||
| `address` | IP of remarkable | tool prompts for it if missing |
|
| `address` | IP of remarkable | tool prompts for it if missing |
|
||||||
| `auth_method` | Either `"password"` or `"key"` | defaults to password if key not given |
|
| `auth_method` | Either `"password"` or `"key"` | defaults to password if key not given |
|
||||||
| `username` | Username for ssh access on reMarkable | default: `"root"` |
|
| `username` | Username for ssh access on reMarkable | default: `"root"` |
|
||||||
| `password` | Password provided by reMarkable | not needed if key provided |
|
| `password` | Password provided by reMarkable | not needed if key provided |
|
||||||
| `key` | Local path to key for ssh | not needed if password provided |
|
| `key` | Local path to key for ssh | not needed if password provided |
|
||||||
| `timeout` | Connection timeout in seconds | default: 1 |
|
| `timeout` | Connection timeout in seconds | default: 1 |
|
||||||
| `insecure_auto_add_host` | Ignores the check on the fingerprint | default: `false` |
|
| `host_key_policy` | `"ask"`, `"ignore_new"`, `"ignore_all"`, `"auto_add"` | default: `"ask"` (description below) |
|
||||||
|
|
||||||
The `address` parameter can be either:
|
The `address` parameter can be either:
|
||||||
- a single string, in which case the address is used for connection
|
- 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.
|
- 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
|
- 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.
|
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:**
|
: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.
|
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 paramiko
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
|
from binascii import hexlify
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
@ -13,6 +14,43 @@ import logging
|
||||||
log = logging.getLogger('rmview')
|
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):
|
class rMConnectSignals(QObject):
|
||||||
onConnect = pyqtSignal(object)
|
onConnect = pyqtSignal(object)
|
||||||
|
@ -23,7 +61,8 @@ class rMConnect(QRunnable):
|
||||||
|
|
||||||
_exception = None
|
_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__()
|
super(rMConnect, self).__init__()
|
||||||
self.address = address
|
self.address = address
|
||||||
self.signals = rMConnectSignals()
|
self.signals = rMConnectSignals()
|
||||||
|
@ -31,14 +70,24 @@ class rMConnect(QRunnable):
|
||||||
self.signals.onConnect.connect(onConnect)
|
self.signals.onConnect.connect(onConnect)
|
||||||
if callable(onError):
|
if callable(onError):
|
||||||
self.signals.onError.connect(onError)
|
self.signals.onError.connect(onError)
|
||||||
# self.key = key
|
|
||||||
try:
|
try:
|
||||||
self.client = paramiko.SSHClient()
|
self.client = paramiko.SSHClient()
|
||||||
if insecure_auto_add_host:
|
|
||||||
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
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:
|
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()
|
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:
|
if key is not None:
|
||||||
key = os.path.expanduser(key)
|
key = os.path.expanduser(key)
|
||||||
try:
|
try:
|
||||||
|
@ -59,7 +108,6 @@ class rMConnect(QRunnable):
|
||||||
'password': password,
|
'password': password,
|
||||||
'pkey': pkey,
|
'pkey': pkey,
|
||||||
'timeout': timeout,
|
'timeout': timeout,
|
||||||
'look_for_keys': False
|
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._exception = e
|
self._exception = e
|
||||||
|
|
|
@ -4,10 +4,10 @@ from PyQt5.QtCore import *
|
||||||
|
|
||||||
from . import resources
|
from . import resources
|
||||||
from .workers import FrameBufferWorker, PointerWorker
|
from .workers import FrameBufferWorker, PointerWorker
|
||||||
from .connection import rMConnect
|
from .connection import rMConnect, RejectNewHostKey, AddNewHostKey, UnknownHostKeyException
|
||||||
from .viewer import QtImageViewer
|
from .viewer import QtImageViewer
|
||||||
|
|
||||||
from paramiko import BadHostKeyException, SSHException
|
from paramiko import BadHostKeyException, HostKeys
|
||||||
|
|
||||||
from .rmparams import *
|
from .rmparams import *
|
||||||
|
|
||||||
|
@ -38,8 +38,9 @@ class rMViewApp(QApplication):
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
super(rMViewApp, self).__init__(args)
|
super(rMViewApp, self).__init__(args)
|
||||||
|
|
||||||
self.DEFAULT_CONFIG = QStandardPaths.standardLocations(QStandardPaths.ConfigLocation)[0]
|
self.CONFIG_DIR = QStandardPaths.standardLocations(QStandardPaths.ConfigLocation)[0]
|
||||||
self.DEFAULT_CONFIG = os.path.join(self.DEFAULT_CONFIG, 'rmview.json')
|
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 = [] if len(args) < 2 else [args[1]]
|
||||||
config_files += ['rmview.json', self.DEFAULT_CONFIG]
|
config_files += ['rmview.json', self.DEFAULT_CONFIG]
|
||||||
|
@ -175,13 +176,27 @@ class rMViewApp(QApplication):
|
||||||
else:
|
else:
|
||||||
return False
|
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)
|
log.info(self.config)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def requestConnect(self):
|
def requestConnect(self, host_key_policy=None):
|
||||||
self.viewer.setWindowTitle("rMview - Connecting...")
|
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(
|
self.threadpool.start(
|
||||||
rMConnect(**self.config.get('ssh'),
|
rMConnect(**args,
|
||||||
|
known_hosts=self.LOCAL_KNOWN_HOSTS,
|
||||||
onError=self.connectionError,
|
onError=self.connectionError,
|
||||||
onConnect=self.connected ) )
|
onConnect=self.connected ) )
|
||||||
|
|
||||||
|
@ -200,8 +215,6 @@ class rMViewApp(QApplication):
|
||||||
self.ssh = ssh
|
self.ssh = ssh
|
||||||
self.viewer.setWindowTitle("rMview - " + self.config.get('ssh').get('address'))
|
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")
|
_,out,_ = ssh.exec_command("cat /sys/devices/soc0/machine")
|
||||||
rmv = out.read().decode("utf-8")
|
rmv = out.read().decode("utf-8")
|
||||||
version = re.fullmatch(r"reMarkable (\d+)\..*\n", rmv)
|
version = re.fullmatch(r"reMarkable (\d+)\..*\n", rmv)
|
||||||
|
@ -358,41 +371,46 @@ class rMViewApp(QApplication):
|
||||||
icon = QPixmap(":/assets/dead.svg")
|
icon = QPixmap(":/assets/dead.svg")
|
||||||
icon.setDevicePixelRatio(self.devicePixelRatio())
|
icon.setDevicePixelRatio(self.devicePixelRatio())
|
||||||
mbox.setIconPixmap(icon)
|
mbox.setIconPixmap(icon)
|
||||||
|
mbox.addButton("Settings...", QMessageBox.ResetRole)
|
||||||
|
mbox.addButton(QMessageBox.Cancel)
|
||||||
if isinstance(e, BadHostKeyException):
|
if isinstance(e, BadHostKeyException):
|
||||||
mbox.setDetailedText(str(e))
|
mbox.setDetailedText(str(e))
|
||||||
mbox.setInformativeText(
|
mbox.setInformativeText(
|
||||||
"<big>The host at %s has the wrong key.<br>"
|
"<big>The host at %s has the wrong key.<br>"
|
||||||
"This usually happens just after a software update on the tablet.</big><br><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>"
|
"<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>"
|
"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."
|
"The easiest way to do this is connecting manually via ssh and follow the instructions."
|
||||||
"<br></li><li>"
|
"<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."
|
"This is not recommended unless you are in a trusted network."
|
||||||
"<br></li><li>"
|
"<br></li><ol>" % (e.hostname)
|
||||||
"Connect using username/password."
|
|
||||||
"<br></li><ol>" % (self.config.get('ssh').get('address'))
|
|
||||||
)
|
)
|
||||||
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(
|
mbox.setInformativeText(
|
||||||
"<big>The host at %s is unknown.<br>"
|
"<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>"
|
"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>"
|
"<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>"
|
"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."
|
"The easiest way to do this is connecting manually via ssh and follow the instructions."
|
||||||
"<br></li><li>"
|
"<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."
|
"This is not recommended unless you are in a trusted network."
|
||||||
"<br></li><li>"
|
"<br></li><ol>" % (e.hostname)
|
||||||
"Connect using username/password."
|
|
||||||
"<br></li><ol>" % (self.config.get('ssh').get('address'))
|
|
||||||
)
|
)
|
||||||
|
mbox.addButton("Ignore", QMessageBox.NoRole)
|
||||||
|
mbox.addButton("Add", QMessageBox.YesRole)
|
||||||
else:
|
else:
|
||||||
mbox.setInformativeText("I could not connect to the reMarkable at %s:\n%s." % (self.config.get('ssh').get('address'), e))
|
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.addButton(QMessageBox.Retry)
|
||||||
mbox.setDefaultButton(QMessageBox.Retry)
|
mbox.setDefaultButton(QMessageBox.Retry)
|
||||||
answer = mbox.exec()
|
answer = mbox.exec()
|
||||||
|
@ -400,6 +418,16 @@ class rMViewApp(QApplication):
|
||||||
self.requestConnect()
|
self.requestConnect()
|
||||||
elif answer == QMessageBox.Cancel:
|
elif answer == QMessageBox.Cancel:
|
||||||
self.quit()
|
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:
|
else:
|
||||||
self.openSettings(prompt=False)
|
self.openSettings(prompt=False)
|
||||||
self.quit()
|
self.quit()
|
||||||
|
|
Loading…
Add table
Reference in a new issue