Hub and Spoke Encryption: The overly complicated way.

It’s been three or four years since I wrote it, and I have it on decent authority that the system isn’t in active usage anymore so I think it’s safe to discuss the hub and spoke encryption system I wrote.

If you’re not familiar with the hub and spoke idea, my understanding of it is that when you are creating encrypted data stores it’s best to ensure that no single target has all the information needed to decrypt sensitive data. In a traditional encryption setup this is done by storing the data in a central store, the hub, and the spokes are the clients accessing the data, they are closest to the outside of the network, and the easiest to compromise, so the only thing they store is the keys to decrypt the data.

Ideally the hub should not be connected to the internet at all, and can only be accessed via a direct connection. A well configured database server probably already follows this architecture. The websites/apps connected to the database don’t have direct access to the information. Instead they store access credentials and then request information from the database as they need it. This ensures that the data doesn’t reside in the website in a way that is directly accessible. In order to access the data attackers would have to compromise both the website with the credentials and the database itself. That said, accessing the database on most websites is pretty trivial. Database passwords are very frequently just stored in the file system unencrypted. And if you have access to the website, you have the direct connection needed to access the data.

The Problem

The company I was working for at the time did repair on devices. In order to facilitate this repair process we could ask customers for their user credentials. This way technicians could log in for virus scans, reconfigurations, or full reinstalls as the situation required. The issue with this was data storage. As more and more devices got connected to the internet, as more and more services started requiring stronger and stronger passwords, the likelihood of the same username/password combo getting you into a phone or laptop and someone’s banking information gets higher and higher. Storing this information in plaintext in a database was getting more and more irresponsible, no matter how many times we would recommend customers change their passwords once they got their devices back.

So, we needed to protect their data while still allowing our technicians to store the credentials with the relevant tickets.

The Solution.

An inverted hub and spoke.

Rather than storing the data in a central store, we would ask our central store to generate us a single use encryption key that would be generated from the domain that requested the key, the time of the request, and a salt of random data generated by the central store. This key would be transferred to the requesting party through a double encrypted connection.

Inside a traditional TLS connection, the client and server would negotiate a second level of encryption via the same principles as TLS encryption, just higher in the network stack. Inside that double encrypted connection the central store would send over the encryption key which itself had been encrypted with an encryption key provided by the client at the time of the request.

The client would then store the encrypted data and the key for the encrypted key. The central store would store the encrypted keys. In theory this meant that neither client nor central store could do anything useful with the encrypted information they held. Instead they would need to coordinate to turn the encrypted information into something useful.

And, on top of that. Whenever data was decrypted, it would be re-encrypted with another key so that if a key was successfully scraped from a log or packet sniffing, it would most likely be invalid by the time it could be used.

The Issues.

If you looked at the last section and said “that’s a lot of encryption”, you have already seen the major pitfall of the scheme.

It was incredibly fragile. This was by design. My belief was that by requiring this much authentication you would deter anyone attempting to harvest data without employing significant effort. You had to have a valid api key which could be revoked at any time. You had to be advertising the same domain as you were when the data was encrypted. You had to be advertising the same IP address. You had to know the time the request was made at. You had to know the key associated with the key you were requesting. You had to know how to negotiate the encrypted connection to actually get the key in a usable form. You had to know how to read the resulting data to pull out a variably positioned random salt in all of the encrypted keys designed to make them useless if you didn’t know it was there.

If you missed any of these steps you would have garbage. Useless garbage.

Which is great, that’s exactly what should happen with encrypted data if you don’t have the keys to open it. But I was naive about the bane of every developer on earth. Time keeping differences. All of the encryption/decryption keys for the encrypted connection between servers were based on the current server time. If the connection took to long, if the hub and spoke didn’t agree on the time, they would be sending information back and forth that they had no way of knowing was different than the other side.

The downfall of the scheme was that I rolled my own double encrypted connection. It worked pretty reliably, but all it took was a brief second where the server was under load so that a request took a little too long to process and then suddenly the data would be encrypted with an unintended key that would render it inert, irrecoverable.

It didn’t help that our network admin had a tendency to change static IPs and advertised domains without telling anyone, but the worst issue was the reliance on time.

Lessons

I think a lot of the principles and choices were sound. I play around with revisiting this pretty regularly. To this day vogon uses a rolling salt for user passwords. Every time you log in the salt and hash are regenerated to make it harder to reverse engineer your password if the hash and salt are grabbed at different times.

Honestly, I’m still pretty proud of the scheme, even if it was made useless by something fairly predictable. I think rather than relying on the client and store to agree on a time, I’d have the client timestamp the request. While this does mean that there’s more key information in transit should you successfully decrypt both levels of encryption, there’s enough redundancy that I don’t know that anything practical is lost by doing that.

Additionally I’d want to revisit the way I was communicating the salt position. The salt was randomly generated and of a random length. But at the time the best way I could communicate to the decryption process where the salt was and how long it was, was to append coordinates. If you know what those numbers were and where to find them, it was trivial to remove the salt.

In my experience, despite all the math, engineering, and cleverness that goes into security, most of it boils down to making it so frustrating to break that an attacker moves onto an easier target. Most of the things that get exploited are flaws that are fairly common. The exploits that get really interesting are the ones found for the really big targets. Huge attack surfaces, or those with the biggest most valuable data stores.

I doubt my scheme would be good enough to protect the information banks need to store, but I know it was good enough to keep someone’s cellphone pin out of the hands of someone attempting to prey on a single repair shop.

Domain Changes & Other Housekeeping

If you’ve visited this website before you’ll probably notice that the main domain has changed, 2020 is a year that changed a lot of things for a lot of people and I am not an exception.

In addition to that very obvious change, I’ve actually been meaning to do a full redesign of this entire website for at least a year and a half. So that will probably happen soon.

I have taken down all the contact forms for the foreseeable future, yes the copy still references them, but it will be changing with the design when I can finally get to that.

Ajax Loop Interface

The Problem

PHP is not the best choice for long running processes, and never has been. It has two major shortcomings for this kind of application:

  1. PHP does not support continuous communication with a browser/client on its own.
  2. PHP is designed to run and then close, and while some variables can carry over between runs, you start with basically a clean slate for each run.

So knowing those shortcomings and knowing that the application is mostly PHP, I had to come up with a way to make a useful and informative user interface for Vogon Media Server to import media. In the prototyping stage I simply let the whole import run as a single long-running process that only output after the import was done. While this was fine for a developer interface, I’m planning on handing this software over to my parents, who should be able to add their own media to it without assistance.

The Method

A solution to this problem has to achieve two major goals:

  1. The application must be able to split a task into small repeatable chunks
  2. Those chunks need to be designed to run as completely independent processes.

Add to this that there are a number of features that might rely on similarly large tasks in the future, and it was obvious that rather than writing a single purpose solution, I needed to craft a method that was modular enough to be applied wherever it was needed.

So the first task was to break down what the process was really doing and then using those overarching prototypes to shape whatever process we come up with.

The import was divided pretty easily into two major sections.

The first section was the initialization. This is where we scanned the filesystem to see what files existed in the requested directory, and set up the classes and includes before we went into a foreach loop through the array our filesystem scan created.

Next inside the loop, we test the file to ensure it’s a file we can import, that it hasn’t already been imported, and finally do things like reading the meta data before creating the needed database entries.

So to restructure this, we’ll keep the two basic phases, but we’ll restructure them so that the section inside the loop is designed to be run as an independent process, doing it’s own initialization as necessary. This is slower, but it gives us more flexibility. Then if we utilize AJAX requests to actually run the loop we can get incremental data back to the user so that it’s less likely that a long-running process will be killed by system resource monitoring software, or that the user will accidentally assume a process is finished when it’s still running.

The Solution

The majority of Vogon projects are written entirely in functional programming. The CMV structure I use means that those functional chunks can be modularly re-used, getting around one of the main shortcomings of that approach. But, in certain situations I favor developer usability over code readability. I don’t like to do it most of the time because classes can end up being the equivalent of code black-boxes: you provide input and get output, but who knows what’s happening inside.

In this case, what we want to do is complex but we want to do it easily and quickly so that developers will be incentivized to use it rather than re-inventing the wheel. For this reason I wrote the ajax_loop_interface as a class file. If you’re looking at the Vogon Media Server repository, it can be found at /main/class/class.ajax_loop_interface.php. The class has one public function, the __construct() magic function, it manages everything else for you.

The idea is this, when the class is instantiated the developer provides some initial options:

  • The mode we’re running in (currently it supports two modes, ‘session_array’ and ‘db’)
  • Our initialization method. If we’re in db mode this takes the form of an SQL statement whose results we can loop through, in session_array mode this should be a model that returns an ordered numeric key array we can save to the $_SESSION super global and loop through that way.
  • The model that does the work inside our loop
  • The extension these models can be found in, if any
  • The variable name for data passed into our loop model (this defaults to ‘$row’ if you don’t set this value)
  • A model that does the after-loop cleanup, if there is any to be done
  • And a number of optional options that differ based on the mode (parameters for a prepared sql query for example, or a boolean switch for the offset behavior if you’re doing something that will modify the results of the SQL query, such as deleting values.)

The class then takes those and determines the current state of the loop based on the existence or absence of $_GET variables. It uses the variable “offset” to determine where it is in the process. If offset doesn’t exist we need to initialize our process, otherwise it functions as a marker for where we are in our loop controlling what variables we pass to the provided model. Then, based on what is run the class returns state information and model provided updates as JSON data so that it can be easily processed by JavaScript.

Rather than relying on custom JavaScript implementations, I also wrote a view (/main/view/view.ajax_loop_interface.php) that is designed to be included in other views and contains the required DOM structure, CSS, and JavaScript to create a loading bar and an auto-scrolling message box to keep the user informed.

The intended application is that the developer creates an endpoint that initializes the class with whatever your needed options are, and then creates a view that includes the ajax_loop_interface view and with a ‘route’ option that points at the created endpoint. The two files then abstract the rest of the process so the only thing the developer needs to spend time on is writing the models the class runs.

The Result

The final product as a user sees it.

This approach ended up having pros and cons, but in my opinion the pros far outweigh the cons, but let’s start with the negative points first.

Doing anything this way raises the complexity, and by running scripts via AJAX you’re pushing yourself further from the immediate output which can make troubleshooting code problems a little harder. Not much harder if you know where to find the XHR requests and responses in your developer tools, but harder.

The options are highly variable based on the situation and the selected mode, so a developer will likely have to have the class file open and be somewhat familiar with it in order to utilize the best options available. This is true of all class files, you have to know what input to use to get good output.

But the positives are pretty strong.

The design of the class means that any long-running loop based process can be adapted with just a little restructuring. This is made doubly true if you utilize the ‘var_name’ option of the class to rename the variable to whatever you were using inside the original loop. Then the majority of the adaptation can be as easy as just copy-pasting and removing excess whitespace characters.

Communicating the current state of a long running process is a big boon both in UX and in getting real-time information back to developers who are still debugging their process.

Structuring loops this way automatically creates some error resilience. Even if an unhandled error or exception occurs during one of the runs, the process as a whole will simply report that error to the user and then run the next item in the loop.

Having modular UI element generators makes the process of enforcing UI consistency much easier than situations where developers are expected to read CSS sheets and other implementations in order to structure their work. There’s a reason OS GUIs usually include an API for common elements that OS uses elsewhere, and by making sure we’re utilizing the visual language of the application we can ensure that a user has a reasonable chance at being able to understand what they’re seeing, even if they’ve never seen this particular interface before.

While the class that runs the loop functions as a black box, the actual work being done are just model files. This means that any developer familiar with how Vogon models are written can easily utilize this class by simply looking at an example and poking around. Since Vogon started out as a prototyping framework I think it’s important to enforce readable structures as much as possible, especially since I have a bad habit of inconsistently commenting my files.

Still working

It’s been a while since I’ve written a post for the website. I’ve been keeping it updated on the backend (much to the chagrin of those who have been attempting to break in), but I’ve mostly been radio silent.

Most of that has been thanks to a steady freelance contract that I picked up in October and have been working since. I’ve got a Wix to WordPress import blog post in me that I need to finish editing that should be pretty interesting, but I wanted to do a couple of small updates on my personal projects here as kind of a “I’m Still Alive, I Promise!” style post.

First, Vogon and Vogon Media Server now have icons:

The Vogon Logo. The letter V super imposed on a quarter of a star.
Vogon’s logo.
Vogon Media Icon. The Vogon icon rotated 90 degrees counter clockwise and super imposed on a media play symbol.
Vogon Media Icon

I’m no graphic designer, but it’s nice that they have some visual identity to them. Speaking of Vogon Media Server, I still use it everyday so it’s been getting regular updates. The most recent was a reworking of the audio visualization code to allow rotating colors and a better sampling of the audio data. I’ve also been working on a BASH script to install VMS on a clean Debian based Linux install. The script configures everything for you so you can just upload your media and go. It’s still nowhere near production, but having the installation process solved is a big weight off my shoulders.

Here’s a demonstration of the new visualizer code. All the music is from the YouTube audio library so they shouldn’t copyright claim the video but who knows in 2021.

Dungeon Master Tools has been updated several times without accompanying blog posts, but the biggest news is that I received permission to serve copyrighted tilesets with the application (they’re encrypted on my server) meaning the tools can create more colorful and visually interesting maps. It’s been really useful for my current campaign group.

I’ve updated my SEO helper with a number of new features to help speed up large scale SEO reworks and things like URL changes that WordPress core doesn’t handle very well out of the box.

And I’m working on an adaptation of Vogon to be a fully featured and secure CMS. My goal is eventually to import this entire site into it, so when you see a large redesign know that Vogon might just be beating at the heart of it.

Vogon & Vogon Media Server

It’s been a surprisingly busy few months between freelance and personal projects, I’ve been mostly working inside of a PHP Framework I wrote a few years ago called Vogon.

Vogon isn’t anything overly special, but that’s part of why I like it so much. It’s a prototyping framework, designed to do just enough to bootstrap a project that I can immediately start writing logic code. If you’re interested at all in the framework you can find it on GitHub (https://github.com/stephentkennedy/vogon), it has a small wiki that explains most of the moving parts, but there aren’t a lot and you should be able to glean what’s going on by just scanning the files if you’re familiar with PHP. I’ve used it a few times over the years for a bunch of different styles of projects, from data-aggregation, to my own personal CRM.

The bigger project of the two in the title, and the reason I wanted to write a blog post and document the progress is the media server web interface I am writing on top of the Vogon framework. You can also find it on GitHub (https://github.com/stephentkennedy/vogon_media_server).

The Problem

The person I’m writing the server software for has a collection of DVDs they’ve invested a great deal of time and energy collecting over the years, but they lack the convenience of streaming and their current BlueRay player has pretty laughable up-scaling performance, making their movies a pain to watch on modern resolution TVs.

They approached me to ask if I could fix up a solution for them as far as a home streaming server was concerned.

The Solution

Well, there are a lot of solutions already on the market. Flex immediately sprung to mind, but since I was doing this project as a favor, and I wanted to cut a few corners on the cost of the server itself (transcoding is expensive, if you didn’t know), I decided I would write my own solution on top of a bog simple DLNA server.

The DLNA format is one of those secret formats that’s not really supported by anything, but is strangely supported by everything. PS4 and Roku for example both support DLNA out of the box through their media players. And the custom web interface I was writing could expand compatibility to anything with an HTML5 compliant web browser.

So, Vogon Media Server was born.

Vogon Media Server

It’s not finished yet, but it does have a pretty good feature list so far:

Meta Data Aware Mass Import

  • Auto-generate Thumbnails for imported videos
  • Auto-populate meta data required for history tracking features
  • Auto-import common meta data fields for supported audio formats (Artist, Year, Composer, Album, Track Number)

User Profiles

  • Support for multiple user profiles
  • History tracked individually between profiles

Audiobook Support

  • Turn individual audio tracks into a coherent audio book
  • History tracking allows resuming across devices

Custom HTML5 Video Player Interface:

  • Autoplay next episode for items categorized as part of a TV series
  • History tracking for media resuming across devices

Custom HTML5 Audio Player Interface:

  • Playlists (Ongoing development)
  • Shuffle Play (client-side and client/server hybrid)
  • Visualizers (3 currently, but hopefully the vizualizer code will be rewritten to be module and these can be developed as plugins)
  • Media key support
  • Partial Media Meta Data support (Depending on your browser and device, you will get media controls on lock screens or when the browser is not the active window)
  • Sleep timer

And I’m gonna be honest, I’ve got the prototype running on a Raspberry Pi 3B, and I’ve been using it every day. It’s very quickly replacing Spotify as my primary media player simply because when something annoys me about it, I can just change it myself instead of complaining to support.

DM Tools 0.6a and 0.6.1a

First of all, I’ve started hosting DM Tools, The Worst Calculator, and GPX on Itch.io as well as here on my website. It’s been garnering a lot more traffic and hopefully will garner more use and feedback. You can find it there at: https://goblinpuncher.itch.io/dm-tools

Additionally, I’ve put up two updates in the last few weeks that I’ve not really noted here, one big and one much much smaller.

First of all, the big push to 0.6a is that I added a tile renderer to the map editor. This means that the map renderer is using a tileset to display the map data instead of my solid colors. Right now it’s just the one tileset, but I’ve tried to leave myself a lot of room to add in tilesets in the future and allow the user to swap them out so that there can be a little more color and variety to maps. From a user standpoint, it may not be that big a change, but from the backend there’s a lot of new code, a lot of rewritten and tweaked code, mostly because I wanted to add support for variable tile display based on what is around it. Before the code didn’t really worry about tile position, just the current state of that tile. I’m pretty proud of the results, though they are certainly far from perfect.

A map divided into four connected quadrants, divided by water and walls.
This is from a battle royale I recently ran for my players. On Roll20 with Dynamic Lighting enabled it was a blast, lots of back and forth and surprise attacks from players.

The smaller change to 0.6.1a is one that honestly might be more important to users, I modified the code that changes the state of tiles so that you can now click and drag to draw tiles across the map. This makes it a lot faster to put maps together, and takes a lot of strain off the fingers and wrists (as someone who suffers from RSI, I feel I should apologize, the previous method was just a lot faster to prototype).

The next big goal is going to be to reign in some of the more… esoteric choices I made in the UI and wrangle it to be a little more responsive and mobile friendly. I think this is going to involve a small rewrite to the Modal code I’m using right now. It’s easy to use from a code perspective, but it needs a little more flexibility for the user. In addition I want to add the ability to zoom and pan the maps because fingers a big and I’d love to be able to run games almost completely from a phone or tablet.

After that I would really like to take some of my DM notes from the homebrew setting and campaigns I’m running and offer them as “supplements” for the tool, make it super easy for people to use or modify the material.

A final word before I let you go. This tool is free, and I have a blast putting it together, but what with the pandemic and a lot of personal stuff I don’t want to get into, I would really appreciate it if you would consider donating some funds to help support its continued development. I’m not looking to make a wage or anything, just cover things like hosting costs and the like for another year. If you use the tool and want to contribute, you can donate on the Itch.io page: https://goblinpuncher.itch.io/dm-tools

And thank you.

DM Tools 0.5.9a

This was a relatively small update on my end, but it’s a big performance improvement for users so I’m pushing it live to the site.

The only user facing change is the Map Generator now uses a Canvas Renderer for the editor. Previous versions used a DOM elements to make the interactions easier to build and prototype, but browsers start to bog down when there are too many DOM elements on a page.

The new editor renderer means that the editor should feel snappier to use, and can generate larger maps before creating slowdown. On my end it should also build the foundations of what I need to do to add new editor features like draw, drag, zoom, and pan that will make using it on mobile a more viable option.

If you dig through the code there are also several new “drawing” and “compositing” functions, but they are not yet available to the user. My next goal for improvement is some alternative generation algorithms so that it can create different types of dungeons.

Changelog:

  • Map Editor now uses HTML Canvas renderer
  • Default editor mode is now click (cycle only really made sense in the first prototype because there were only two options)

DM Tools 0.5.8a

Lot of incremental changes, and several versions that didn’t get uploaded directly to the website.

  • Added several new tiles to the map generator (it doesn’t use most of them automatically, but it will generate the overall map for you).
  • Added a new edit mode to make the extra tiles easier to access. Added a help screen. Added generation parameters for water.
  • Added ability to save maps as notes (which can be edited later)
  • Fixed issue with treasure generation that the new code created

Additionally, since this tool has grown a lot in the last week or so, I went ahead and threw together a little one-shot adventure to show off how I personally use it.

To use the adventure, import the following string (triple click to quickly select the whole string):

N4IgTg9gNlDOIC4DaoB2BDAtgU0SAYujAJaoDmABAMLbFSmUAiW6ZuANCKhAC7o-EIqPACYAJgBYQAX3ZosuBCAAy2AGY8IAN2xhqEAA4HdFAArFsAY2zxO3PgKF4AzJJkBdOwcvxk8nHgAkpiwABYgnJZCPNioPHgA8paWAK4G-IKoCBSBqKgAtADS2NjGYAA6qJUASujW2QASRGr5CWCWlZUA4rFiutkAskTYnagNxGShNjwUAIIARnTEPACe2bkxJGyo1hT5+RSwPCligimw7BRQ2OhgqNhil6QAjinEsMvEOgB0o8oQAHdpnNFvRVtkqEIjssUo5UHsDh9LABrKArS7pa6-KqoQJ9IjZWawAzEMAZISjABCQjEsAh6AMAi0-AeFHmKwo6AokEw6DixEsFFIMTA02xoyGeV070w2QAKgDSXAKKF0KSKBA9McyLA2TcwGJxag5cM4tlTGq4rr5jdYcQ1CkYCsjRtdHU4XSKA1eBQYpgyg9RrMjPq+fUKABVVDnFJEQ7I0gUKJQTVG-BQdAAihtCgAZSsop45ogsFgNg+Qg1agoak1izEfXhKdFRo8nC0dAzpF80Zgdl4NkQKBAUTisXiSgAPAYAHxy0LvCivS0zXLwxf8X1TFUpeZVijVL66ADkus0ANQ32zF9Z7K32BVzXymsFwTClx424+5Guh00YA5Uh111IgAXQFZrXOHh6HIb5JwAelnacZ0neYwBQxCUNnecHyAigyCPXUIDUasuTUMALFQMQ0U5Uknl1IEYCTW5WVrMBLlVECKEYqA-wgFFWQTHhLCmS8EKQpC0IwiSZwARm+ABKCgcJyPJT247AmKOfjkUE5YRNiK8Gl0B8DAzFZdF1Sw+RrUgxE5Ch0jiAC+IE+zFU-TktM1HgiOrWA0jMiwz1VGZMAmUIZgEPp70wLdIBSSYHLEFJyGwStLDJAEoCeatP2wDkgVFX0IDZW4RW45ZQhyEJqrYoUeEuTUKF5FYbT-G5eOWbiF1-bcwNgVBjxmFMIATWCKAAClye9HL5HgXMXWIIESuqIAgez6oAVgABhVCKl1jMEORwIgLk5KiarCR9dQXXUnIWjlRW0tyNOwVEVgU8SsJQqTvuQkRFOU7c1yFXV7CuW42EueZYXvYqWPikpuXWkIa2a64Zk3fKypRfccbXS56q5HQXNFdBYCEdB5l-AwKOsODMOQ1D0P+2dnCBlS13UqJMEwStIAgOKNygfmji8sCIJrR0co1PRN2uCmsaslMy05XVD1J9S5UBeFrPhXkMDYK8cIR4rP1FB8+BpmwSsObrbhWqjLi5eZbhdy6uUgW4GFslsruqxdiJieEwFS1BfcTPl7JWmZiNmoSDPhDzqoW8DdQzRtKs8o5sABS5vdz+z5ggd2Ls257qrd3Ti-Jw0ptN3CuNgdA1AfVK+juG5PJxt2wCvUxzMs5j4RJPHupThzGCoChZL23N6EmOPqyaKj4Jwo9tfW5UDJRL7MIiEAAr524VjwedF2XYVVOAzkou3UJd33TWTzPXWrwSG9i45HHVSgFoXwBw-F+Bgv5tKLTyGDCW6c2RQRgmQb4Kk8IER0H5UiNYKK9BomqdiUCeLMVFJtTUHEKYOXwS9GuFBE6iW+PJJSXM1IMU0rxChelhI0K9CZRyQ8wBWRsmoYgh8+A6iCHkS4vIDBCOWNcPAL8wBbwvDfQ+YgWTnxSA+YMegRAAA4KAiB2vooRKxjB4AkYfMxCBQAqL4EOJAO12D2McQ45xTjXEuPcW42S7AvFeMBltdgIgAkBM8HY5xPj2ASG8REqJ4TYkxOcf4+xcS-FBMCSIEJTjwlJLCfE8JAA2eJkSvHZPCSktJwT2ChLcdkkpuS6k1LqWU1JGScleMiQUtxkTqkeNaVEppaSWm1Iaf4uJoz4m1N8d8fx5T0mVO6Q0zJ9SokFLGZM6ZzS5m9KKdEsZEycnbNKVM1JFSqnjKiXs1ZZzklHJmYMnp8z7mPOuesgZngQAAA9GDEEwHgOeh8VhfJ+UoEQW1D6ajIEOAA7N4na7hpCyFAKOEOE4QDIXKCAORCiqggFmh8Mg8JKw4zBNBB8FFSYfBiqqTAmNDjpyvP8ayvFrjbHsi3KWOMGQlgWoYbci4ohgFFJYRq5ctyLjFfCNQ7oHLngJV-NkP9txkt0IzGSLNpI-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-D4FJELx5H3zfMVm9VKhskYKUISkug7TqCFj3E-E3DpzRh9XagwHmJnwIs1BwDi2uu9S-DpgB03Fly-FK2KzykVXQyMi4U1L0M7EaJ2CdncLJAMHOgPSEEoDNJMFSD7ytAqx3L3L-RjPXx+k3wvizJ6i4QK32sxMIpTIFOJyOIQy1F90pxyAhtMns2rzWz3UULinwK6nUId2AS4QwPajOItMJvC0+GE3qnykiP2hPMoEdjG17hgnsn5XuHlndJuFzm9O3WrOOClHKKBNEh2zENtlht4SeBmAwN5B81dnJgSzlsVWBGrIJpgmzOuFKCwJmAAGJ2SWTk1KCt8kwq1RQrQVQuF9ztw58ciryRcmCQJipRQMwmQGjGVMAB4PjGb7aHxsDsB3kRRlgOQW440USZ5Vz4cphS6YgTC6hIBrS+R9iObr8Z9sssatQrC1DRVk6bgrwAApD7EkWWo6rkMIduyXKwu2ceugog6excFcirYXOgKtNg+GlKVABLeW21H1HGPoUocLCqSwVWYEPeqYLOigfAI604M9GiS65gnubcP2kQSGfA94jEk9WLO+P0RkWaLaDQGwhkEqMCAvWaBaXWtekYp2f6vkzULWkVEXCAKM2aXYrQJ6BkKMnuliLkP2jo7MYwIW4EDA0UHQOMZaMw+gNufIdTQvZqG4WzRHE6TzG0LUUqIEW4O7IY2de0bAGh6YBieGjM1vLkMoQ2FsgAalXJLmjD3z6CLulGU0XJAldL-RmFd3js7uJt8OQn8N-ybhuNjkykMBHsoC9LYPMYUxzs3MejtkgP9A1Ga33pKBMM7o4idWa2roKgvPMM0MxqEaPA1FhALmwFeFJG1NeIkGXMLuLuUzLsMM5DSxQfECicpDqLEDYCEF9mYF5GNjmAbE+CpidBFrbpgA7pZD0BjvzSsAeMXH5g7FlqVl0Aq1FDCeeinhnl0XkdiY5EXLtgCmSHLAdCdHCxigtnkNCG0dZJnA6KUkHk-q3E3ELNxgS0sbFs5NzXlT0QAf1TDjCF9hRqwZNlKmMetJ3tabeHafHpSEGdLGGeXJnlklXMYBicUdLsTu0JzowPd0OHOpmD-n8mMFZEmm8waL+yVt9HYN9Ge3OmamwIphrSFUyDth+cpiFHDz1nuv4AUimeTXZICMRKwFYagWsjAB0DQIhjvDcZYqfW3jXVDs0GgF9lGm7JKlKlSntG+rtnag9sTF5D6CiIEWq13M9pGPbm-EoE-ADyJZMFIWIvfR1QXEAesn9EQNbt5b-TFeYlWhmFSkzERmrKYZFtf1MnMm1M0x60es7LGl1DNRahueqlBfwggHyAETCAAoogAC8DGuQAX8gyT0AxBBN5jX45oURLhoRLMeA+ADIzrojnD9wbJOxXWWQ4sg3xw17vDG9YyZJ4yCXDhFQNAaJ6mDnMG8KMCBoa1ZDm6ziARPVRnnVQgEokp5amo9B7HUbd6pCL7LgcTywI0sS86OnZ4unXmKIlGkTkRs6Fm63zripYAgQ-79XCp4b2pK3bxzyfU-aOZQ7rJSZNXLHEYcyBBTywhss8VNXO9t46TkRLg62qaa7Hw414jk4ozYgHhcXG9IUgYZpRG062JhYq8D7XGKn3yIGKYoGDIjhp31zPif6mi7Hh7ORTn6WkCB3UXu2+TM880GDUBfi0DYYsYQxbg98DC9R58miLB655nYOsSCT7JZJJA+cQwSKLBrAjzAGbRE1DQg7G9tEgZjJzZtwNXli41wFWQcZwOAp2n8oOQyPtJrgJUjrn03bW692k6XYmFLNYNy9SGzhdRC6ldlh6WcIORCdYpoCvYJmaJYY6AZgoP9V5X70rSp0-SHOrguyJD-QSxQLCO7saILnSRP01yTAfUBn2PYBhni2P1h28k8xrhwT9xITYB14Fwgd4Sd5J3EDIyxnH2MD6OHIqBqhZ5fRyZpOl1NQYpJpqhoBeJJ4cY5QyuoGegtb+BiFsS1YcZvQDQHwTQbZDrS8cZGABgug22Ou+zt8sAgsvRKvTJIAOw+h7IY6bAHxu1YAcX8nTg4RPqP7aOrXbU7jtt8vviiVttTBej1IB7SsMAqTc01RTttx7hJUKIY8jj+YI5BR8R4EGIFxbNrg427oSm+LTwKsgc4gjoaIwfZpod1J3cLxJVfSppB5lcuzdt40YnOR03wfRQqDgz864AjCaagz-ZTOgG42zjqyoho3WBbZluQHLxlyUHZOqFuBJdH3VQ4sDVMpNn0hSwafUNtw+LDsvlYGOQUFt8b93DMPHJej1Yo7wtC9vahzFxJUYh9teOZIABODk51wm0TubgcIVCTqYV1HBbOacyVJUXpsOGKPz9QNuJFuNe4LGT2Pm0rhkGDuGpie4CT0qIdrkSJo4hR8d95nQMb9qayHV7Ekwun53nqaemyIgW4OKYYJTbTET6AgO3td7T7M+KjPa3fGAoiwXhi06me+ES6kssbW63mB60KDzF6-M1b-gKtT63bH66B1WgGiiHm4G7TQrdoY01ul9ThYqK2wRpiWIGLMcLUFGtG7lYTTusO3G3yfGkV7sWhTosVH7h97fInovrcLi0xBkGGCXY66tNXGRJQTLR+6qFSV8i-zgaxSSmjTRejY3QxIjYxISpSkAFSkAaxOpWQCnIHkVVLJLFUGpgC7koA5KuAPsQzJRqQSAqg8lMorJMqlyFJAUgQHlJkByVQKosmyTYDEBfiO5EQLIHECbkg1cgTMimTQC8B7iBAU5UCSBUSBmyfAT0kYE5JAk0KMZHVXWpsD6BbiTgYQOSppJaBAg9gTAJGpoCdKTiPJHQMkGwDIB6A+KooMUE4CkBiA3AWoMEGaCGkOAhQboJYGRUek2g9xGAPiSWUnEgSAKuYJCQ2CVBFgoQSYM8T2C+kXA4gZlSIG+I7KFVSpA4OYG+DTBSyawY4NUHuCFkTyIIaEN8p2D-B7ghAVIOcSBD4BIQsypsh8GJCohI1PgYNRkH1IWksg6IeYMaRdUqBrVTSi0nKq6UwhpSLQY4L6pwClBWAiAeUk8RDV8kEAooUkKIHtCdKsyJAKZU8qBULB0KfIfAImqcApqQKEACCn+SAoZqYKJQkOH0TeJnAG1c3FtStw7Um82-GXtoDfZxZm4U3dnGMU0gYJkYPqBPBmF2CTwHsD6UOLVwQAa8foSAKgKYHcDZAvEmvHaDtGTRIBcwHw7ILtD+GN4kAN2T4bPDySgiZI4IzzJCNzBqkUYasZcEpk9ZTQ04f6OotYFbyN1a6tba4nW2WB79bg+QCeskSTDjhdACkS4LtDIAGB-hPQTAJCPtwUlDAtZB8JNExGwBsRLZGrCUFQA0iKAdIhkWCKZGQiB6FMInJNEMDoBXglqOosFjZA3Db2ImHgheCFEijGR8I+kF3HoA2QZR6QeUXLBWIJNCE+QdUYKNpE7R6R2o5kdkFzC3B3cqAFYO8imiyjjRY5PCvHDDIBoiRMQTUTaNFGwjxR2QNJl6m0ggZDRcomjCojAAJYMyK7TyGGSLxPF1u1o20WKJ1HUA-41gZ0RyGjHGj-R2AQMZmJDHZiM0UITQFGO5G8jW8xY83nNF-A0waMZAFMACFLHBjXhoYjLKNFpDVjORHo2MbcATFkgkx1UFMdcEPKdi7REoqUSYELE0YWxSommMPnD61YrRwooMbOIdFIjG6D4VETwHRFciyQWIm5i2TxF8RSihIhcDEHgJgAyRf5VkDiJFAziwRQwAiK+D9AIiBSLCYxkxEmiyRxY1wd1Di3+EfiBQOQb8Q6N-GHB-xvESaDSRQJgT3xrASCZqmzG5hYJsAeCVNCQkgEUJsIiCV+OzGmBeAyLeOPyNA67F4En7WcDn0HR58QA5NKBNZgW6shSE49E4UqLbjnDyIlw0qNcLqDr1PI9w+9JABgDIB3hkI74b8IBFAitxO0cEQpNkjQi4R9ovMHuJLAHjYwaIjEaeJ5HnjcRJYfEdeM8gNicyj4r3BrSpFgBOx6klkU2wgjsiKYnI2sUZJmD8jYg9knsZKJ4x6BFxlqI-lRia6ihL+QlSWjBGUihSHw3XPoBJX1y0YZKH-U3NIDhRAA

You can access the DM Tools here: https://stephkennedy.dev/experiments/dm_tools/

DM Tools 0.5a

Big feature this time, dungeon generation.

There’s a new tab on the right hand side, you’ll recognize it by the Treasure Map icon. This tray is full screen no matter what your screen size is. I made this decision because of the complexity of the controls (just click the icon again to close the tray).

Once you have generated your map, click on any of the tiles to switch their state from open to closed (black to white and vice versa), so you can tweak any maps you generate to fit exactly what you’re looking for.

I have set the defaults to be Roll20 friendly making my maps match up with the Roll20 defaults as closely as possible so that dropping a map into place should only take a minute or two. The goal of this project is to create a bunch of tools to help me DM entirely by the seat of my pants, so hopeful it should be easy enough to do while dropping exposition on my players.

This one is a little more complex, so I’m going to go into more detail on how I went about doing this below. All of the images used to illustrate the text are generated by the DM Tools.

Dungeon generation is something I’ve been trying to do (with only some success) for several years for my other project, Goblin Puncher, but as that was the project I used to teach myself JavaScript and programming in general, I had a lot of failures and misunderstood successes with that.

But, something clicked the other night. I came across this post, disagreed strongly with significant portions of the implementation, but it had a nugget of knowledge I hadn’t come across yet, and it clicked with me.

I think a big piece of it is that when developing most business applications or even websites, you don’t really have to stretch the math or spacial reasoning muscles. Most business applications are about storing, finding, editing, and deleting data. So I’ve not had an excuse to spend much time brushing up on things I’ve always been interested in like pathfinding algorithms.

In this case, the tutorial introduced me to one of the oldest map generation algorithms: The Random Walk (aka The Drunken Walk). The algorithm itself is so simple that I’m a little dumbfounded I didn’t stumble onto it myself (mostly because I was over-complicating the issue). The gist is that you have four cardinal directions. You choose a random starting position and at any decision point, you randomly choose a direction that is not the way you came or the way you’re currently going. By staggering segment lengths you can end up with a very winding pattern, something like this.

A Single Random Walk Pass on a 25 x 25 grid

Add another pass or two and you get slightly more complex patterns.

Three Random Walk Passes on a 25 x 25 grid

This is a passable dungeon on its own, but there isn’t a lot of variety. What few chambers are made are all irregular. The fix to this was rather simple. I had the script choose a constrained random height and width for a rectangle and then choose one of the points we’ve already drawn on as the origin (upper left-hand corner) of our rectangle.

It’s my understanding that this is the reverse of how a lot of dungeon generators work, as they build the chambers and then the paths between them, but this is the result it creates.

Three Random Walk Passes with generated rooms on a 25 x 25 grid

This gives a better feeling of a spiderweb of passages between rooms.

Once the actual generation of the dungeon was working, the rest was easy. The preview is a set of <div> elements with a percentage width and the same percentage top (this is a CSS trick for making responsive perfect squares with nothing in them). I just tied a click even to the <div> elements and toggled the state of the corresponding bool in the 2d array that is the map data.

Exporting to PNG was just as easy, just create a canvas and draw the squares at the scale specified by the user, export that to a dataURL and set it as the source of an <img> element. I could trigger a file download, and may in the future, but something just feels wrong about triggering a file download with JavaScript, even if it is at the response of a user. Gives me flashbacks to my hack of a file manager I wrote to extend CKEditor.

An Honest Message About Covid-19

It’s been a few weeks since I’ve really done anything for my business. I count myself as incredibly lucky in that I have that option. But I also know there are a lot of small businesses in crisis right now, so I want to write a quick blog post to share some advice on how to make it through this. I have always believed that the continued success of your business is the key to my own success, so I want to be honest with you.

Prepare for the worst.

We’re all optimistic that this will only be our reality for another month or so, but the reality is that there are some models predicting we could be dealing with the ramifications of this for up to two years. The best chance of surviving is to accept that and make plans accordingly. Your sales will be down, if they aren’t already. Your staff will be less productive. It will be harder for you to get loans. Many of your outstanding balances will probably default, and you may never see that money. Your job as a business owner is to accept all that now, and prepare your business to survive despite that. If you’re wrong, you’ll end up with a little more money than you expected, which is never a bad thing.

Cut unnecessary spending.

I honestly can’t believe I’m about to write this but if you’re not Amazon, Zoom, or a business very much like them, if you are spending money on marketing right now, you’re essentially setting it on fire. A lot of unscrupulous marketing companies are arguing that all the extra eyes on social media right now make it a prime opportunity. Unfortunately, this is a lie.

Many consumers are on extremely limited funds, and will be for months. As a country our buying power has diminished exponentially. There just isn’t as much room in the market anymore, and we will have to work hard to rebuild the market to where it was. There’s also a huge chance for marketing blowback at the moment. If you don’t fully understand what I mean, take a look at these stories about Gamestop and Hobby Lobby. In a normal market missteps like this would be quickly put out of consumer minds, but this isn’t a normal market, and with all the extra social media time every mistake has a chance to ferment in the minds of your customers.

If you do plan on continuing to do social media marketing, be smart. Focus on uplifting or amusing content. Everyone needs a light at the end of the tunnel right now, and will likely fondly remember who and what helped them. That may not work for you, but it is honestly one of your best bets as a strategy in this moment.

Focus on your infrastructure

Build your audience, work on your SEO strategies, figure out remote work, make sure your file server works and is getting backed up. While sales are going to be down, you can use this time to focus your work on making sure your business is ready to hit the ground running as soon as it can. Not only that, but this kind of work is going to be exceedingly cheap to accomplish right now. You’ll have extra time to do it yourself, or you’ll be able to get good deals from freelancers who desperately need work. If you are working at all, you should be strengthening the foundations of your business to weather this storm.

Be kind.

This has hit everyone. This is a global issue. There are going to be many people who can’t pay their bills. Kindness is your best bet to ensure that you will be paid, eventually. But more than that, be understanding that there are some people who just can’t pay you ever anymore. It’s not a great place to be, but here we are. You’ll burn more bridges harassing these people, and potentially have social media blowback like I mentioned above.

Remember, there is no great achievement in history that was accomplished by one individual on their own. There is always a group working towards a goal. But with kindness and understanding we will all weather this storm.

Wash your hands.

This is a serious issue. Most models predict that everyone on the planet will likely get this at some point. Everything, every action we are being advised to take, is about slowing that rate down so that our medical infrastructure can manage without collapsing. If that happens, there may not be an economy to help recover.

It doesn’t matter if you are young, old, symptom free, or coughing up a storm. You must do your part to manage this illness. Wash your hands. Wear a mask if you are coughing. Keep at least 6 feet away from people. Let your employees work from home if at all possible. Do not encourage consumers or employees to gather in your business. Local governments are pulling business licenses for violating stay at home and essential business orders, so always remember that a dollar today is never worth losing the ability to make dollars tomorrow.