|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +""" |
| 4 | +================================================================ |
| 5 | + HEADER |
| 6 | +================================================================ |
| 7 | +% SYNOPSIS |
| 8 | ++ grub_set_password.py PASSWORD |
| 9 | +% |
| 10 | +% DESCRIPTION |
| 11 | +% This script locks all GRUB functionality, apart from booting with the |
| 12 | +% default options of an installed Linux-based operating system, behind the |
| 13 | +% given password. |
| 14 | +% |
| 15 | +% It takes one mandatory parameter: the password to use. (The GRUB username |
| 16 | +% associated with this password will always be "superuser".) |
| 17 | +% |
| 18 | +================================================================ |
| 19 | +- IMPLEMENTATION |
| 20 | +- version grub_set_password.py (magenta.dk) 1.0.0 |
| 21 | +- author Alexander Faithfull |
| 22 | +- copyright Copyright 2019, Magenta ApS |
| 23 | +- Portions copyright 2015 Ryan Sawhill Aroha |
| 24 | +- license GNU General Public License v3+ |
| 25 | +- email af@magenta.dk |
| 26 | +- |
| 27 | +================================================================ |
| 28 | + HISTORY |
| 29 | + 2019/10/28 : af : Script created |
| 30 | +
|
| 31 | +================================================================ |
| 32 | + END_OF_HEADER |
| 33 | +================================================================ |
| 34 | +""" |
| 35 | + |
| 36 | +from os import chmod, rename, urandom |
| 37 | +from sys import argv, exit |
| 38 | +from hashlib import pbkdf2_hmac |
| 39 | +from binascii import hexlify |
| 40 | +from subprocess import run, DEVNULL |
| 41 | + |
| 42 | +# This function was taken from https://github.com/ryran/burg2-mkpasswd-pbkdf2 |
| 43 | +# (and lightly tweaked for use here): |
| 44 | +# |
| 45 | +# Copyright 2015 Ryan Sawhill Aroha <rsaw@redhat.com> |
| 46 | +# |
| 47 | +# This program is free software: you can redistribute it and/or modify |
| 48 | +# it under the terms of the GNU General Public License as published by |
| 49 | +# the Free Software Foundation, either version 3 of the License, or |
| 50 | +# (at your option) any later version. |
| 51 | +# |
| 52 | +# This program is distributed in the hope that it will be useful, |
| 53 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 54 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 55 | +# General Public License <gnu.org/licenses/gpl.html> for more details. |
| 56 | + |
| 57 | + |
| 58 | +def grub2_mkpasswd_pbkdf2(passphrase, iterCount=100000, saltLength=64, debug=False): |
| 59 | + algo = "sha512" |
| 60 | + |
| 61 | + binSalt = urandom(saltLength) |
| 62 | + hexSalt = hexlify(binSalt).decode("ascii") |
| 63 | + passHash = hexlify( |
| 64 | + pbkdf2_hmac(algo, passphrase.encode("ascii"), binSalt, iterCount) |
| 65 | + ).decode("ascii") |
| 66 | + |
| 67 | + if debug: |
| 68 | + print("algo = '{}'".format(algo)) |
| 69 | + print("iterCount = '{}'".format(iterCount)) |
| 70 | + print("saltLength = '{}'".format(saltLength)) |
| 71 | + print("hexSalt = '{}'".format(hexSalt)) |
| 72 | + return "grub.pbkdf2.{}.{}.{}.{}".format(algo, iterCount, hexSalt, passHash) |
| 73 | + |
| 74 | + |
| 75 | +# This patch tweaks the behaviour of update-grub(1) very slightly so that the |
| 76 | +# default launch entry for the installed operating system can be used even if |
| 77 | +# GRUB's other functions are password-protected |
| 78 | +diff = r"""\ |
| 79 | +diff --git a/10_linux b/10_linux |
| 80 | +old mode 100644 |
| 81 | +new mode 100755 |
| 82 | +index 68700d9..b8ef18d |
| 83 | +--- a/10_linux |
| 84 | ++++ b/10_linux |
| 85 | +@@ -129,7 +129,7 @@ linux_entry () |
| 86 | + fi |
| 87 | + echo "menuentry '$(echo "$title" | grub_quote)' ${CLASS} \$menuentry_id_option 'gnulinux-$version-$type-$boot_device_id' {" | sed "s/^/$submenu_indentation/" |
| 88 | + else |
| 89 | +- echo "menuentry '$(echo "$os" | grub_quote)' ${CLASS} \$menuentry_id_option 'gnulinux-simple-$boot_device_id' {" | sed "s/^/$submenu_indentation/" |
| 90 | ++ echo "menuentry '$(echo "$os" | grub_quote)' ${CLASS} \$menuentry_id_option 'gnulinux-simple-$boot_device_id' --unrestricted {" | sed "s/^/$submenu_indentation/" |
| 91 | + fi |
| 92 | + if [ "$quick_boot" = 1 ]; then |
| 93 | + echo " recordfail" | sed "s/^/$submenu_indentation/" |
| 94 | +""" # noqa W291,E501 |
| 95 | + |
| 96 | + |
| 97 | +def main(): |
| 98 | + if len(argv) != 2: |
| 99 | + print("Syntax: {0} PASSWORD".format(argv[0])) |
| 100 | + exit(1) |
| 101 | + |
| 102 | + # Since we manually patch /etc/grub.d/10_linux (and we need that patch to |
| 103 | + # remain in place, or the system will become unbootable without the |
| 104 | + # password), instruct dpkg(1) to leave it alone! |
| 105 | + diversion = run(["dpkg-divert", "--add", "--no-rename", "/etc/grub.d/10_linux"]) |
| 106 | + if diversion.returncode != 0: |
| 107 | + print("diversion failed") |
| 108 | + exit(1) |
| 109 | + |
| 110 | + # Check if we've already patched /etc/grub.d/10_linux by checking if |
| 111 | + # unapplying it would succeed |
| 112 | + already_applied = run( |
| 113 | + [ |
| 114 | + "patch", |
| 115 | + "--dry-run", |
| 116 | + "--reverse", |
| 117 | + "--silent", |
| 118 | + "--force", |
| 119 | + "/etc/grub.d/10_linux", |
| 120 | + ], |
| 121 | + input=diff, |
| 122 | + stdout=DEVNULL, |
| 123 | + stderr=DEVNULL, |
| 124 | + universal_newlines=True, |
| 125 | + ) |
| 126 | + if already_applied.returncode != 0: |
| 127 | + # If we haven't, then patch it now |
| 128 | + print("patching /etc/grub.d/10_linux") |
| 129 | + application = run( |
| 130 | + ["patch", "--silent", "--force", "/etc/grub.d/10_linux"], |
| 131 | + input=diff, |
| 132 | + universal_newlines=True, |
| 133 | + ) |
| 134 | + if application.returncode != 0: |
| 135 | + print("patch failed") |
| 136 | + exit(1) |
| 137 | + else: |
| 138 | + print("/etc/grub.d/10_linux is already patched") |
| 139 | + |
| 140 | + # For safety's sake, patch(1) sometimes leaves a copy of the original file |
| 141 | + # behind (for example, if the patch didn't *precisely* match). If that file |
| 142 | + # exists, then we should make sure it's not executable so |
| 143 | + # that update-grub(1) won't try to run it |
| 144 | + try: |
| 145 | + chmod("/etc/grub.d/10_linux.orig", 0o600) |
| 146 | + except FileNotFoundError: |
| 147 | + pass |
| 148 | + |
| 149 | + # Now update /etc/grub.d/40_custom with the appropriately-hashed form of |
| 150 | + # the password. We do this in a slightly careful way: we populate a |
| 151 | + # temporary file with all of the lines from that file that *don't* have a |
| 152 | + # special tag comment in them (to make sure we don't leave old settings |
| 153 | + # lying around), after which we write two new lines with that tag comment. |
| 154 | + # Then we change the permissions on the temporary file to make it |
| 155 | + # executable and move it into place over the old one |
| 156 | + encoded = grub2_mkpasswd_pbkdf2(argv[1], debug=True) |
| 157 | + with open("/etc/grub.d/40_custom.tmp", "wt") as new: |
| 158 | + with open("/etc/grub.d/40_custom", "r+t") as old: |
| 159 | + for line in old: |
| 160 | + if "# OS2borgerPC lockdown" not in line: |
| 161 | + new.write(line) |
| 162 | + new.write('set superusers="superuser" # OS2borgerPC lockdown\n') |
| 163 | + new.write( |
| 164 | + "password_pbkdf2 superuser" " {0} # OS2borgerPC lockdown\n".format(encoded) |
| 165 | + ) |
| 166 | + chmod("/etc/grub.d/40_custom.tmp", 0o700) |
| 167 | + rename("/etc/grub.d/40_custom.tmp", "/etc/grub.d/40_custom") |
| 168 | + |
| 169 | + # Finally, having done all of that, run update-grub(1) to generate a new |
| 170 | + # grub.cfg |
| 171 | + result = run(["update-grub"]) |
| 172 | + if result.returncode != 0: |
| 173 | + print("update-grub failed") |
| 174 | + exit(1) |
| 175 | + |
| 176 | + |
| 177 | +if __name__ == "__main__": |
| 178 | + main() |
0 commit comments