Example #1
0
 def wrapper(*args, **kwargs):
     if not state.initialized:
         if config.get('debug', False) and config.get('debug.secret_key'):
             secret_key_file = config.get('debug.secret_key')
             crypto_util.load_secret_key_file(secret_key_file)
         else:        
             raise exc.CryptoNotInitialized("Crypto engine has not been initialized.")
     return f(*args, **kwargs)
Example #2
0
def init_crypto(options):
    """
    Interactive target to initialize the database with a new crypto passphrase.
    """
    print "Initializing crypto for an empty database."
    if crypto_util.has_encrypted_data():
        raise BuildFailure("Database has existing encrypted contents; use the 'rekey' target instead.")

    passphrase = raw_input("Passphrase: ")
    print "The database will be initialized with the passphrase between the arrows: --->%s<---" % passphrase
    print "The MD5 of the passphrase you entered is: %s" % hashlib.md5(passphrase).hexdigest()
    
    confirm = raw_input("Type 'YES' to confirm passphrase and MD5 are correct: ")
    if confirm != 'YES':
        raise ValueError("You must enter 'YES' to proceed.")
    
    salt = get_random_bytes(16)
    key = crypto_util.derive_key(passphrase=passphrase, salt=salt)
    crypto_util.initialize_key_metadata(key=key, salt=salt, force_overwrite=False)
    
    print "Database key metadata has been initialized.  Your application is ready for use."
    if config.get('debug'):
        print "The new key is: %s%s" % (binascii.hexlify(key.encryption_key), binascii.hexlify(key.signing_key))

    print "*************************************************************"
    print "IMPORTANT"
    print "Make sure your database master passphrase is stored somewhere"
    print "outside of Ensconce."
    print ""
    print "There is no recovery mechanism for this passphrase (or for "
    print "your database, should you lose it.)"
    print "*************************************************************"    
Example #3
0
def rekey(options):
    """
    Interactive target to change the passphrase for the database.
    """
    info("This is an EXTREMELY DANGEROUS activity.")
    info("Backup your database first.")
    
    curr_passphrase = raw_input("Current passphrase: ")
    
    crypto_util.configure_crypto_state(curr_passphrase)
    
    new_passphrase = raw_input("New passphrase: ")
    confirm = raw_input("MD5 of passphrase is %s (type \"YES\" to confirm): " % hashlib.md5(new_passphrase).hexdigest())
    if confirm != 'YES':
        raise ValueError("You must enter 'YES' to proceed.")
    
    if crypto_util.has_encrypted_data():
        confirm = raw_input("There is existing encrypted data in the database.  Type 'REKEY' to proceed with re-encryption: ")
        if confirm != 'REKEY':
            raise ValueError("You must enter 'REKEY' to proceed.")
    
    # Use the same salt as previous key
    new_key = crypto_util.derive_configured_key(new_passphrase)
    
    crypto_util.replace_key(new_key=new_key, force=True)
    
    info("Re-encryption completed successfully.")
    
    if config.get('debug'):
        print "The new key is: %s%s" % (binascii.hexlify(new_key.encryption_key), binascii.hexlify(new_key.signing_key))
Example #4
0
def backup_database():
    """
    Backups entire database contents to a YAML file which is encrypted using the password
    from a specified mapped password in the database.
    """
    try:
        dir_mode = int(config.get("backups.dir_mode", "0700"), 8)
        file_mode = int(config.get("backups.file_mode", "0600"), 8)

        # Before we do anything, validate the configuration.
        if not os.path.exists(config["backups.path"]):
            # Attempt to make the directories.
            os.makedirs(config["backups.path"], mode=dir_mode)

        if not config.get("backups.encryption.password_id"):
            raise exc.ConfigurationError(
                "Cannot backup without configured password_id (backups.encryption.password_id)"
            )

        try:
            pw = passwords.get(config["backups.encryption.password_id"])
        except exc.NoSuchEntity as x:
            raise exc.ConfigurationError(
                "Configured backups.encryption.password_id does not exist in database: {0}".format(x)
            )

        backup_fname = datetime.now().strftime("backup-%Y-%m-%d-%H-%M.gpg")

        msg = "Backing up database to {fname}, secured by password id={pw.id}, resource={resource.name}[{resource.id}]"
        log.info(msg.format(fname=backup_fname, pw=pw, resource=pw.resource))

        exporter = GpgYamlExporter(passphrase=pw.password_decrypted, use_tags=True, include_key_metadata=True)

        encrypted_stream = BytesIO()
        exporter.export(stream=encrypted_stream)
        encrypted_stream.seek(0)  # Just to ensure it's rewound

        backup_file = os.path.join(config["backups.path"], backup_fname)
        with open(backup_file, "w") as fp:
            fp.write(encrypted_stream.read())

        os.chmod(backup_file, file_mode)
    except:
        log.critical("Error backing up database.", exc_info=True)
        raise
Example #5
0
    def setUpClass(cls):
        """
        Initialize the database.
        
        We also read and store the encryption key here (but actual state setup
        happens in in setUp method).
        """
        super(BaseModelTest, cls).setUpClass()
        init_model(config)
        if not  config.get('debug.secret_key'):
            raise Exception("Tests only work with debug.secret_key set to a valid key.")

        cls.SECRET_KEY = CombinedMasterKey(binascii.unhexlify(open(config['debug.secret_key']).read().strip()))
Example #6
0
def test_data(options):
    """
    Erases database and replaces contents with test data.
    (This will only work if debug is on and debug secret key is set.)
    """
    if not config['debug'] or not config['debug.secret_key']:
        raise BuildFailure("This target only works with debug=True and debug.secret_key set")
    else:
        secret_key_file = config.get('debug.secret_key')
        crypto_util.load_secret_key_file(secret_key_file)
    
    #init_model(config)
    data = populate.TestDataPopulator()
    data.populate()
Example #7
0
def run_server(argv=None):
    if argv is None:
        argv = sys.argv
        
    parser = optparse.OptionParser(description='Run the ensconce cherrypy server.')
    init_app()
    
    parser.add_option('-d', '--debug',
                      default=config.get('debug', False),
                      action="store_true",
                      help='Run in debug mode?')
    
    (options, args) = parser.parse_args()
    
    config['debug'] = options.debug
    server.configure()
    server.serve_forever()
Example #8
0
def render(filename, data=None):
    """
    Convenience method to render a template.
    """
    from ensconce import acl
    
    if data is None:
        data = {}
    
    data['title_prefix'] = config.get('ui.title_prefix')
    data['operator_info'] = operator_info()
    
    env = Environment(loader=PackageLoader('ensconce', 'templates'),
                      autoescape=True,
                      finalize=lambda x: '' if x is None else x)
    env.globals['pop_notifications'] = pop_notifications
    
    # Expose the ACL module so that there can be some checking in the templates 
    # (to avoid showing buttons that won't be clickable)
    env.globals['acl'] = acl
    
    try:
        env.globals['app_version'] = pkg_resources.get_distribution("ensconce").version
    except:
        log.exception("Error determining software version.")
        env.globals['app_version'] = '?.?'
    
    # Add an escape filter for when we need to embed values in JS code. 
    env.filters['escapejs'] = escapejs
    
    if operator_info().user_id: # They are logged in, so add the quick-group-nav form.
        form = QuickGroupForm() # Do not initialize we/ request params, since that could be confusing.
        form.group_id.choices = [(0, '[Jump to Group]')] + [(g.id, g.name) for g in groups.list()]
        env.globals['quickgroupform'] = form
    
    return env.get_template(filename).render(data)
Example #9
0
 def export(self, group_id=None, **kwargs):
     form = ExportForm(request_params(), group_id=group_id)
     form.group_id.choices = [(g.id, g.name) for g in groups.list()]
     
     exporter_choices = [('yaml', 'YAML (GPG/PGP-encrypted)')]
     if config['export.keepass.enabled']:
         if not os.path.exists(config['export.keepass.exe_path']):
             log.error("KeePass export enabled, but specified converter script does not exist: {0}".format(config.get('export.keepass.exe_path')))
         else:
             exporter_choices.append(('kdb', 'KeePass 1.x'))
     form.format.choices = exporter_choices
     
     if cherrypy.request.method == 'POST':
         if form.validate():
             group = groups.get(form.group_id.data)
             
             if form.format.data == 'yaml':
                 exporter = GpgYamlExporter(use_tags=False,
                                            passphrase=form.passphrase.data,
                                            resource_filters=[model.GroupResource.group_id==group.id]) # @UndefinedVariable
                 encrypted_stream = BytesIO()
                 exporter.export(stream=encrypted_stream)
                 encrypted_stream.seek(0) # Just to ensure it's rewound
                 
                 return serve_fileobj(encrypted_stream, content_type='application/pgp-encrypted', disposition='attachment',
                                      name='group-{0}-export.pgp'.format(re.sub('[^\w\-\.]', '_', group.name)))
                 
             elif form.format.data == 'kdb':
                 exporter = KeepassExporter(passphrase=form.passphrase.data,
                                            resource_filters=[model.GroupResource.group_id==group.id]) # @UndefinedVariable
                 encrypted_stream = BytesIO()
                 exporter.export(stream=encrypted_stream)
                 encrypted_stream.seek(0) # Just to ensure it's rewound
                 
                 return serve_fileobj(encrypted_stream, content_type='application/x-keepass-database', disposition='attachment',
                                      name='group-{0}-export.kdb'.format(re.sub('[^\w\-\.]', '_', group.name)))
                     
             else:
                 # I don't think we can get here in normal business.
                 raise RuntimeError("Unhandled format specified: {0}".format(form.format.data))
                 
         else: # does not validate
             return render("group/export.html", {'form': form})
     else: # request method is GET
         return render("group/export.html", {'form': form})
Example #10
0
def configure():
    """
    Configures the cherrypy server (sets up the tree, cherrypy config, etc.).
    """
    global configured
    # Setup the session storage directory if it does not exist
    if config.get('sessions.on') and config.get('sessions.storage_type') == 'file':
        path = config['sessions.storage_path']
        if not os.path.exists(path):
            try:
                os.makedirs(path) # By default these will be 0777
            except:
                warnings.warn("Unable to create the session directory: {0}".format(path))
                  
    cherrypy.config.update({
        "server.socket_host": config['server.socket_host'],
        "server.socket_port": config['server.socket_port'],
        
        "checker.on": False,
        "log.screen": False,
        #"engine.autoreload_on": False,
        "engine.autoreload_on": config.as_bool("debug"),
        
        "tools.sessions.on": config.as_bool('sessions.on'),
        "tools.sessions.persistent": config.as_bool('sessions.persistent'),
        "tools.sessions.path": config['sessions.path'],
        "tools.sessions.timeout": config['sessions.timeout'],
        "tools.sessions.storage_type": config['sessions.storage_type'],
        "tools.sessions.storage_path": config['sessions.storage_path'],
        "tools.sessions.secure": config['sessions.secure'],

        "request.show_tracebacks": config.as_bool("debug"),
        "checker.on": False,
        "tools.caching.on": False,
        "tools.expires.on": True,
        "tools.expires.secs": 0,
        "tools.expires.force": True,
        "tools.log_headers.on": False,
        "engine.autoreload_on": config.as_bool("debug"),
        
        "tools.encode.on": True,
        "tools.encode.encoding": "utf8",
        
        "error_page.default": error_handler
        })
    
    if config['server.behind_proxy']:
        cherrypy.config.update({"tools.proxy.on": True})
    
    if config['server.ssl_certificate']:
        # Make this conditional so we can host behind apache?
        cherrypy.config.update({
        "server.ssl_certificate": config['server.ssl_certificate'],
        "server.ssl_private_key": config['server.ssl_private_key'],
        "server.ssl_certificate_chain": config['server.ssl_certificate_chain'],
        })
        
    
    def rollback_dbsession():
        log.info("Rolling back SA transaction.")
        session = meta.Session()
        session.rollback()
    
    def commit_dbsession():
        log.info("Committing SA transaction.")
        session = meta.Session()
        session.commit()
    
    cherrypy.tools.dbsession_rollback = cherrypy.Tool('before_error_response', rollback_dbsession)
    cherrypy.tools.dbsession_commit = cherrypy.Tool('on_end_resource', commit_dbsession)
    
    # This is a "flow-control" exception.       
    class _LoginFailed(Exception):
        pass
        
    # TODO: Refactor to combine with the ensconce.webapp.tree methods
    def checkpassword(realm, username, password):
        auth_providers = get_configured_providers()
        try:
            for auth_provider in auth_providers:
                try:
                    auth_provider.authenticate(username, password)
                except exc.InsufficientPrivileges:
                    # Fail fast in this case; we don't want to continue on to try other authenticators.
                    raise _LoginFailed()
                except exc.AuthError:
                    # Swallow other auth errors so it goes onto next authenticator in the list.
                    pass
                except:
                    # Other exceptions needs to get logged at least.
                    log.exception("Unexpected error authenticating user using {0!r}".format(auth_provider))
                else:
                    log.info("Authentication succeeded for username {0} using provider {1}".format(username, auth_provider))
                    break
            else:
                log.debug("Authenticators exhausted; login failed.")
                raise _LoginFailed()
        except _LoginFailed:
            auditlog.log(auditlog.CODE_AUTH_FAILED, comment=username)
            return False
        else:
            # Resolve the user using the *current value* for auth_provider (as that is the one that passed the auth.
            user = auth_provider.resolve_user(username)
            
            log.debug("Setting up cherrypy session with username={0}, user_id={1}".format(username, user.id))    
            cherrypy.session['username'] = username # @UndefinedVariable
            cherrypy.session['user_id'] = user.id # @UndefinedVariable
            
            auditlog.log(auditlog.CODE_AUTH_LOGIN)
            return True
        
    app_conf = {
        "/static": {
            "tools.staticdir.on": True,
            "tools.staticdir.dir": config['static_dir'],
            "tools.staticdir.index": "index.html"
        },
        "/jsonrpc": {
            'tools.auth_basic.on': True,
            'tools.auth_basic.realm': 'api',
            'tools.auth_basic.checkpassword': checkpassword,
        }
    }
    
    # Add a plugin that will run the Crypto.Random.atfork() method, since this must
    # be called after forking (and we run this as a daemon in production)
    util.RNGInitializer(cherrypy.engine).subscribe()


    # Wire up our daemon tasks
    background_tasks = []
    if config.get('sessions.on'):
        background_tasks.append(tasks.DaemonTask(tasks.remove_old_session_files, interval=60))
        
    if config.get('backups.on'):
        backup_interval = config['backups.interval_minutes'] * 60
        background_tasks.append(tasks.DaemonTask(tasks.backup_database, interval=backup_interval, wait_first=True))
        background_tasks.append(tasks.DaemonTask(tasks.remove_old_backups, interval=3600, wait_first=True)) # This checks a day-granularity interval internally.
    
    
    # Unsubscribe anything that is already there, so that this method is idempotent
    # (This surfaces as nasty bugs in testing otherwise.)
    for channel in cherrypy.engine.listeners:
        for callback in cherrypy.engine.listeners[channel]:
            log.debug("Unsubscribing {0}:{1!r}".format(channel, callback))
#            log.debug("Unsubscribing {0}:{1!r}".format(channel, callback))
#            cherrypy.engine.unsubscribe(channel, callback)
        
    
    for task in background_tasks:
        cherrypy.engine.subscribe("start", task.start, priority=99)
        cherrypy.engine.subscribe("stop", task.stop)
    
    # Setup the basic/top-level webapp API
    root = tree.Root()
    
    # Iterate over all the modules in the ensconce.webapp.tree package and add
    # their 'Root' classes to the tree
    pkgpath = os.path.dirname(tree.__file__)
    for modname in [name for (_, name, _) in pkgutil.iter_modules([pkgpath])]:
        module = __import__("ensconce.webapp.tree." + modname, fromlist=["Root"])
        module_root = module.Root()
        setattr(root, modname, module_root)
    
    # I think this is here because we want to explicitly specify the ServerAdapter below
    # rather than use a default one.
    cherrypy.server.unsubscribe()
    
    app = cherrypy.tree.mount(root, "/", app_conf)
    app.log.error_log.level = cherrypy.log.error_log.level  # @UndefinedVariable
    app.log.access_log.level = cherrypy.log.access_log.level  # @UndefinedVariable
    
    addr = (config["server.socket_host"], config["server.socket_port"])
    server = CherryPyWSGIServer(addr, app, numthreads=50, timeout=2)  # TODO: make numthreads and keepalive timeout configurable
    
    
    # TODO: This is also mentioned in the cherrypy config above .... ?  One of these is probably redundant.
    server.ssl_certificate = config["server.ssl_certificate"]
    server.ssl_private_key = config["server.ssl_private_key"]
    if config["server.ssl_certificate_chain"]:
        server.ssl_certificate_chain = config["server.ssl_certificate_chain"]
        
    adapter = ServerAdapter(cherrypy.engine, server, server.bind_addr)
    adapter.subscribe()
    
    configured = True