#!/usr/bin/python3
# *-* coding:utf-8 *-*

#***************************************************************************
# This file is part of the CRYPTO BONE
# File     : external-cryptobone-admin  (installed in /usr/bin)
# Version  : 1.5 (external cryptobone)
# License  : BSD
# Date     : 1  March 2023
# Contact  : Please send enquiries and bug-reports to innovation@senderek.ie
#
#
# Copyright (c) 2015-2023
#	Ralf Senderek, Ireland.  All rights reserved. (https://senderek.ie)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. All advertising materials mentioning features or use of this software
#    must display the following acknowledgement:
#	   This product includes software developed by Ralf Senderek.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND  ANY EXPRESS OR 
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE  IMPLIED WARRANTIES 
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
# ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
# POSSIBILITY OF SUCH DAMAGE.
#****************************************************************************

import os
import base64
import time

OS = os.name

###############################################################
def unix (command) :
     if OS == "posix":
          Pipe = os.popen(command, "r")
          Result = Pipe.read()
          Pipe.close()
     return Result

###############################################################
# GUI result of yes-no dialog
REPLY = False

RED     = "#fcc"
GREEN   = "#cfc"
GRAY    = "#ccc"
STATUS  = ""

INFO       = "#ddd"
DEBUGCOLOR = "#aaa"
BACKGROUND = "#ddd"
ERRORCOLOR = "#f00"
OKCOLOR    = "#0f0"
INFOCOLOR  = "#00f"
KEYCOLOR   = "#efe"
YESCOLOR   = "#afa"
NOCOLOR    = "#faa"

GUI = False
try:
     from tkinter import *
     from tkinter import font as tkFont
     GUI = True
except:
     print ("The GUI cannot be run. \nMaybe Tkinter is not installed?")
     import sys
     sys.exit(4)

X11 = False
PID1=unix("pidof gdm-wayland-session")
PID2=unix("pidof Xorg")
PID3=unix("pidof Xwayland")
if ( (len(PID1) > 0) or (len(PID2) > 0) or (len(PID3) > 0)):
     X11=True


###############################################################
# GUI functions
###############################################################

def clear_error():
     ErrorFrame.configure(bg=INFO)
     ErrorLabel.configure(bg=INFO)
     ErrorLabel.configure(text="")
     Window.update_idletasks()

###############################################################
def showinfo(Title, Text):
     Text = Title + " " + Text
     ErrorFrame.configure(bg=INFO)
     ErrorLabel.configure(text=Text)
     ErrorLabel.configure(fg=INFOCOLOR, bg=INFO)
     Window.update_idletasks()

###############################################################
def showsuccess(Title, Text):
     Text = Title + " " + Text
     ErrorFrame.configure(bg=INFO)
     ErrorLabel.configure(text=Text)
     ErrorLabel.configure(fg=OKCOLOR, bg=INFO)
     Window.update_idletasks()

###############################################################
def showerror(Title, Text):
     Text = Title + " " + Text
     ErrorFrame.configure(bg=INFO)
     ErrorLabel.configure(text=Text) 
     ErrorLabel.configure(fg=ERRORCOLOR, bg=INFO)
     Window.update_idletasks()

###############################################################
def askyesno(Header, Msg):
     global REPLY

     # create a pop-up Toplevel window to receive a choice in REPLY

     def returnyes():
          global REPLY
          REPLY = True
          dialog.destroy()
          return REPLY

     def returnno():
          global REPLY
          REPLY = False
          dialog.destroy()
          return REPLY

     dialog = Toplevel()
     dialog.title (Header)
     L = Label(dialog, text=Msg, font=Bold )
     ReplyFrame = Frame(dialog, bg=BACKGROUND, pady=5)
     Yes = Button(ReplyFrame, text= "  Yes  ", font=BFont, width=16, bg=YESCOLOR, command=returnyes)
     No  = Button(ReplyFrame, text= "  No   ", font=BFont, width=16, bg=NOCOLOR, command=returnno)

     L.pack(padx=30, pady=10)
     Yes.pack(padx=8, side=LEFT, pady=10)
     No.pack(padx=8, side=LEFT, pady=10)
     ReplyFrame.pack()
     dialog.transient(Window)
     dialog.grab_set()
     dialog.wait_window()

###############################################################
def get_status():
     global ACTIVE, DB, FIREWALL, REMOTE, SSHD
     print (STATUS)
     print () 

     RES=unix("systemctl is-enabled cryptoboneexternd")
     if "enabled" in RES :
          ACTIVE = True
          EnableButton.configure(text="Disable External Crypto Bone")
          RES=unix("systemctl is-active cryptoboneexternd")
          if "xactive" in "x"+RES:
                ActiveLabel.configure(text="This external Crypto Bone is enabled and running.")
                ActiveLabel.configure(bg=GREEN)
          else:		
                if "failed" in RES:
                     ActiveLabel.configure(text="This external Crypto Bone is enabled but failed to start.")
                     ActiveLabel.configure(bg=GRAY)
                else:     
                     ActiveLabel.configure(text="This external Crypto Bone is enabled but not running.")
                     ActiveLabel.configure(bg=GRAY)
		
     else:
          ACTIVE = False
          EnableButton.configure(text="Enable External Crypto Bone")
          ActiveLabel.configure(text="The external Crypto Bone is not enabled on this machine.")
          ActiveLabel.configure(bg=GRAY)
     
     if "database:initialised" in STATUS:
          DB = True
          DBLabel.configure(text="The encrypted data base is initialized.")
          DBLabel.configure(bg=GREEN)
     else:	  
          DB = False
          DBLabel.configure(text="The encrypted data base is missing.")
          DBLabel.configure(bg=GRAY)

     RES=unix("systemctl is-active firewalld")
     if "xactive" in "x"+RES :
          FIREWALL = False
          FWButton.configure(text="Activate Restrictive Firewall")
          FirewallLabel.configure(text="The system's original firewall daemon is active.")
          FirewallLabel.configure(bg=GRAY)
     else:		
          # firewalld is inactive, check the existence of usefirewall
          if "firewall:restrictive" in STATUS:
               FIREWALL = True
               FWButton.configure(text="Return To Original Firewall Daemon")
               FirewallLabel.configure(text="The Crypto Bone's restrictive firewall is active.")
               FirewallLabel.configure(bg=GREEN)
          else:
               # custom or no firewall
               FirewallLabel.configure(text="The Crypto Bone's restrictive firewall is not active.")
               FirewallLabel.configure(bg=GRAY)

     RES=unix("systemctl is-enabled sshd 2>&1")
     if "Failed to get unit" in RES :
          SSHD = False
          SSHDButton.configure(text="Install Secure Shell Daemon")
          RemoteLabel.configure(text="Secure Shell Daemon is not installed but needed.")
          RemoteLabel.configure(bg=RED)
     else:		
          # sshd is installed
          SSHD = True
          RES=unix("systemctl is-active sshd 2>&1")
          if ("inactive" in RES) or ("unknown" in RES) :
               RemoteLabel.configure(text="The Secure Shell Daemon is not running.")
               RemoteLabel.configure(bg=RED)
               showerror( "ERROR","Please enable the secure shell daemon.")
          elif "xactive" in "x"+RES:     
               if "sshdconfig:hardened" in STATUS :
                    REMOTE = True
                    SSHDButton.configure(text="Return To Original SSHD Configuration")
                    RemoteLabel.configure(text="The system's secure shell daemon is hardened.")
                    RemoteLabel.configure(bg=GREEN)
               else:
                    REMOTE = False
                    SSHDButton.configure(text="Harden Secure Shell Daemon")
                    RemoteLabel.configure(text="The system's secure shell daemon is untouched.")
                    RemoteLabel.configure(bg=GRAY)


###############################################################
def terminate_GUI():
     Window.destroy()     


###############################################################
def OpenHelpUrl():
     import webbrowser
     webbrowser.open_new("https://crypto-bone.com/help/external")
     
###############################################################
def activate_external():
     global ACTIVE, STATUS

     clear_error()
     if not ACTIVE:
          print ("Enabling cryptoboneexternd ...")
          print ("This takes about half a minute ...")
          RES=unix("df | grep BOOT")
          if not "BOOT" in RES:
               showerror( "ERROR","There is no USB partition with a BOOT label which is needed to store secrets\n for the main machine. Please insert a  USB key labelled BOOT and enable again.")
               return
          STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone enable")
          if "missing" in STATUS:
               showinfo( "ATTENTION","Please reboot this machine to make sure that the secrets database will be created.")
               return 
          # check the result
          RES=unix("/usr/bin/pkexec /bin/ls /usr/lib/cryptobone/ext/masterkey")
          if "masterkey" in RES:
               showsuccess( "SUCCESS","The external cryptobone daemon on this machine is initialised.\n\nPlease transfer the new secrets to your main machine now,\n if you have enabled the external Crypto Bone for the first time.")
          else:
               showerror( "ERROR","New secrets have been created but there is no USB partition to write to.\n Please insert a  USB key labelled BOOT and reboot this machine to initialise again.")
          ACTIVE = True
     else:
          print ("Disabling cryptoboneexternd ...")
          STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone disable")
          ACTIVE = False
     get_status()

###############################################################
def harden_firewall():
     global FIREWALL, STATUS

     clear_error()
     if not FIREWALL:
          print ("Activating the restrictive firewall for the external Crypto Bone ...")
          showinfo( "ATTENTION","If you activate the restrictive firewall this machine cannot be used as a general purpose computer\n with access to the internet.\nThe restrictive firewall replaces any firewall setting after booting this machine")
          unix("sleep 10")
          STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone usefirewall")
          FIREWALL = True
          showinfo( "REBOOT","You need to reboot your computer to ensure that this change takes effect.")
     else:
          print ("Restoring the original firewalld configuration  ...")
          STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone restorefirewall")
          FIREWALL = False
          showinfo( "ATTENTION","Your original firewall daemon is now active again.")
     if not ACTIVE:
          showerror( "ERROR","You also need to activate the external cryptobone daemon on this machine.")
     get_status()

###############################################################
def harden_sshd():
     global REMOTE, STATUS

     clear_error()
     if not SSHD:
          install_sshd()
     else:	  
          if not REMOTE:
               print ("Hardening the secure shell daemon for the external Crypto Bone ...")
               STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone hardensshd")
               print (STATUS)
               REMOTE = True
          else:
               print ("Restoring the original secure shell daemon configuration ...")
               STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone restoresshd")
               print (STATUS)
               REMOTE = False
     get_status()

###############################################################
def install_sshd():
     global SSHD, STATUS 
     
     clear_error()
     Info = """You are about to install the secure shell daemon on this machine.

You will then be able to access this machine from your main computer through the network interface.

Do you want to install the Secure Shell Daemon now?
"""
     askyesno('SSHD Installation', Info)
     if REPLY:
          print ("INSTALLING SSH DAEMON") 
          STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone installsshd")
          SSHD = True
     get_status()
          
###############################################################
def reset_cryptobone():
     global STATUS

     clear_error()
     Info = """You are about to destroy the EXTERNAL Crypto Bone on this machine.

After completing this step the encryption key database and all access information\n for the EXTERNAL Crypto Bone on this machine will be destroyed.\n
While the external cryptobone software will still be there, you will return to square one\n and this machine will try to create new secrets on the next boot.

Do you want to remove all EXTERNAL Crypto Bone data now?
"""
     askyesno('Destroying an External Crypto Bone', Info)
     if REPLY:
          print ("DESTROYING AN EXTERNAL CRYPTO BONE") 
          STATUS = unix("/usr/bin/pkexec /usr/bin/external-cryptobone reset")
          print (STATUS)
     get_status()	  
     
###############################################################
# Main
###############################################################

ACTIVE=False
DB=False
FIREWALL=False
REMOTE=False
SSHD=False
STATUS=unix("/usr/bin/pkexec /usr/bin/external-cryptobone status")


print ("This is the administration tool for the EXTERNAL Crypto Bone on this machine.")
print ()
print ("In order to collect certain status information root permission is required.")
print ("As this GUI runs non-root, all administrative tasks require root permission too.")
print ("You may be prompted for the super user (root) password several times.")
print ()

if GUI and X11:

     Window = Tk()
     Window.title("External Crypto Bone Administration")

     MainFrame = Frame(Window)
     TopFrame = Frame(MainFrame, pady=10)
     StatusFrame = Frame(MainFrame, pady=5)
     BottomFrame = Frame(Window, pady=5)
     

     Big = tkFont.Font(family="utopia", size=16)
     Title = tkFont.Font(family="utopia", size=12)
     Info  = tkFont.Font(family="arial", size=10, slant="italic")
     Bold  = tkFont.Font(family="arial", size=11, weight="bold")
     Normal  = tkFont.Font(family="arial", size=10, weight="normal")
     BFont = tkFont.Font(family="utopia", size=12, weight="normal")
     TextFont = tkFont.Font(family="arial", size=11, weight="normal")

     STARTINFO = """
The Crypto Bone has two different modes of operation that can be selected\n in the SETUP window of the cryptobone program. 

In its default mode (ALL-IN-ONE) the GUI (cryptobone) uses the internal, encrypted data base
on the main machine through a UNIX socket on the same computer. 

In EXTERNAL mode, a separate device is used to store the data base and the main machine 
establishes contact to this second external device via a secure shell link. If this machine
is going to be the second device, a secure shell daemon must be activated here.

You are now turning this machine into a separate, EXTERNAL Crypto Bone.

Do you wish to do this?  
(for more information: man external-cryptobone-admin and https://crypto-bone.com)

     """
     print (STARTINFO)

     # Status
     Help_btn_img = PhotoImage(file='/usr/share/icons/default/question-mark.png')
     HelpButton = Button(master=TopFrame, image=Help_btn_img, bg=GRAY,  width=60, height=60,  command=OpenHelpUrl)
     BoneLabel = Label(TopFrame, text="EXTERNAL CRYPTO BONE  1.5 (Administration)", font=Big, width=50)
     BoneLabel.bind("<Button-1>",lambda e: OpenHelpUrl())
     ActiveLabel = Label(StatusFrame, text="no information", width=60, height=1, font=Bold, bg=GRAY)
     DBLabel = Label(StatusFrame, text="no information", width=60, height=1, font=Bold, bg=GRAY)
     FirewallLabel = Label(StatusFrame, text="no information", width=60, height=1, font=Bold, bg=GRAY)
     RemoteLabel = Label(StatusFrame, text="no information", width=60, height=1, font=Bold, bg=GRAY)
     InfoLabel = Label(StatusFrame, text=STARTINFO, width=100, height=16, font=Normal, bg="#eee")
     BoneLabel.pack(side=LEFT)
     HelpButton.pack(padx=20, side=LEFT, pady=5)
     ActiveLabel.pack(fill=Y)
     DBLabel.pack(fill=Y)
     FirewallLabel.pack(fill=Y)
     RemoteLabel.pack(fill=Y)
     InfoLabel.pack(fill=Y, pady=20)

     SSHDButton = Button(master=BottomFrame, text="Harden Secure Shell Daemon", width=40, bg=GRAY, font=BFont, command=harden_sshd)
     RESETButton = Button(master=BottomFrame, text="Forget Everything", width=40, bg=RED, font=BFont, command=reset_cryptobone)

     FWButton = Button(master=BottomFrame, text="Activate Restrictive Firewall", width=40, bg=GRAY, font=BFont, command=harden_firewall)
     EnableButton = Button(master=BottomFrame, text="Enable External Crypto Bone", width=40, bg=GRAY, font=BFont, command=activate_external)
     ExitButton = Button(master=BottomFrame, text="EXIT", width=20, bg=YESCOLOR, font=BFont, command=terminate_GUI)
     
     EnableButton.pack(pady=8)
     FWButton.pack(pady=8)
     SSHDButton.pack(pady=8)
     RESETButton.pack(pady=8)
     ExitButton.pack(pady=20)

     # ERROR frame
     ErrorFrame = Frame(Window, bg=DEBUGCOLOR, pady=10)
     ErrorLabel = Label(ErrorFrame, text="", font=Bold,  bg=BACKGROUND)
     ErrorLabel.pack(side=LEFT, padx=10)
     
     TopFrame.pack()
     StatusFrame.pack()
     MainFrame.pack()
     BottomFrame.pack()
     ErrorFrame.pack(fill=X)
     clear_error() 
     get_status() 
     if not "database:initialised" in STATUS and ACTIVE:
          showinfo( "ATTENTION","Please reboot this machine to make sure that the secrets database will be created.")

     Window.mainloop()
else:
     print ("Console version (not yet available)")

##############################################################
