Random Thoughts2023-08-01T15:13:20+00:00https://pen.so/Fabien Pensohttps://pen.soAsset pipeline for Rust2023-07-31T11:30:00+00:00https://pen.so/2023/07/31/asset-pipeline-for-rustFabien Pensohttps://pen.so
<p class="has-drop-cap">Rust’s performance is insane but requires a significant amount of manual work,
unlike Rails which is highly opinionated but gives you everything.</p>
<p>My server-side HTML templates for
<a href="https://www.constellations.zone">Constellations</a> used
<a href="https://www.getbootstrap.com">Bootstrap</a> with a CDN but I wanted to move it to
a classic asset pipeline. This post explains how I built it.</p>
<p>The way it works:</p>
<ol>
<li>All needed assets are built by your asset builder then copied in <code class="language-plaintext highlighter-rouge">assets</code></li>
<li>Assets are then copied to <code class="language-plaintext highlighter-rouge">public/assets</code></li>
<li>Assets are delivered by Actix</li>
<li>An helper for templates allows to link asset files</li>
</ol>
<p>I’d be very interested if you found this helpful or have improvement
suggestions.</p>
<!--more-->
<hr />
<h3 id="1-build-assets">1. Build assets</h3>
<p><code class="language-plaintext highlighter-rouge">build.js</code> builds my own SASS and Typescript assets then save them into
<code class="language-plaintext highlighter-rouge">assets</code>. Javascript/Typescript files are stored in <code class="language-plaintext highlighter-rouge">javascript</code> and CSS in
<code class="language-plaintext highlighter-rouge">css</code>.</p>
<p>It uses <a href="https://esbuild.github.io">esbuild</a> but you could probably adapt it
to use <a href="https://webpack.js.org">webpack</a>.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#!/usr/bin/env node
</span>
<span class="kd">const</span> <span class="nx">esbuild</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">esbuild</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">glob</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">tiny-glob</span><span class="dl">"</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">sassPlugin</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">esbuild-plugin-sass</span><span class="dl">'</span><span class="p">);</span>
<span class="p">(</span><span class="nf">async </span><span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">entryPoints</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">glob</span><span class="p">(</span><span class="dl">"</span><span class="s2">./javascript/*.{ts,js}</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">esbuild</span><span class="p">.</span><span class="nf">build</span><span class="p">({</span>
<span class="na">entryPoints</span><span class="p">:</span> <span class="nx">entryPoints</span><span class="p">,</span>
<span class="na">bundle</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">outdir</span><span class="p">:</span> <span class="dl">'</span><span class="s1">assets/</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}).</span><span class="nf">catch</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">message</span><span class="p">))</span>
<span class="nx">entryPoints</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">glob</span><span class="p">(</span><span class="dl">"</span><span class="s2">./css/*.css</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">esbuild</span><span class="p">.</span><span class="nf">build</span><span class="p">({</span>
<span class="na">entryPoints</span><span class="p">:</span> <span class="nx">entryPoints</span><span class="p">,</span>
<span class="na">bundle</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="na">outdir</span><span class="p">:</span> <span class="dl">'</span><span class="s1">assets/</span><span class="dl">'</span><span class="p">,</span>
<span class="na">loader</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">.woff</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">file</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">.woff2</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">file</span><span class="dl">'</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">plugins</span><span class="p">:</span> <span class="p">[</span><span class="nf">sassPlugin</span><span class="p">()],</span>
<span class="p">}).</span><span class="nf">catch</span><span class="p">((</span><span class="nx">e</span><span class="p">)</span> <span class="o">=></span> <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">message</span><span class="p">))</span>
<span class="p">})();</span>
</code></pre></div></div>
<p>You’ll want to have a <code class="language-plaintext highlighter-rouge">package.json</code> and run <code class="language-plaintext highlighter-rouge">npm install</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"esbuild"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.18.17"</span><span class="p">,</span><span class="w">
</span><span class="nl">"esbuild-plugin-manifest"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.6.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tailwindcss"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^3.3.3"</span><span class="p">,</span><span class="w">
</span><span class="nl">"sass"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.64.1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"esbuild-plugin-sass"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^1.0.1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tiny-glob"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^0.2.9"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h3 id="2-copy-assets-and-add-a-hash">2. Copy assets and add a hash</h3>
<p>This will copy all assets in <code class="language-plaintext highlighter-rouge">assets</code> to <code class="language-plaintext highlighter-rouge">public/assets</code> including a SHA1 hash
of the file content in its filename to prevent caching issue on new deploys,
then add a <code class="language-plaintext highlighter-rouge">manifest.json</code> file.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// build.rs</span>
<span class="k">use</span> <span class="nn">sha1</span><span class="p">::{</span><span class="n">Digest</span><span class="p">,</span> <span class="n">Sha1</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">collections</span><span class="p">::</span><span class="n">HashMap</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="n">fs</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">io</span><span class="p">::</span><span class="n">Write</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">path</span><span class="p">::</span><span class="n">Path</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">walkdir</span><span class="p">::</span><span class="n">WalkDir</span><span class="p">;</span>
<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="p">(),</span> <span class="nn">anyhow</span><span class="p">::</span><span class="n">Error</span><span class="o">></span> <span class="p">{</span>
<span class="c1">// Place the directory containing your asset files here</span>
<span class="k">let</span> <span class="n">assets_dir</span> <span class="o">=</span> <span class="nn">Path</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"assets"</span><span class="p">);</span>
<span class="c1">// The output directory</span>
<span class="k">let</span> <span class="n">dest_path</span> <span class="o">=</span> <span class="nn">Path</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="s">"./"</span><span class="p">)</span><span class="nf">.join</span><span class="p">(</span><span class="s">"public/assets"</span><span class="p">);</span>
<span class="nn">fs</span><span class="p">::</span><span class="nf">remove_dir_all</span><span class="p">(</span><span class="o">&</span><span class="n">dest_path</span><span class="p">)</span><span class="nf">.ok</span><span class="p">();</span>
<span class="nn">fs</span><span class="p">::</span><span class="nf">create_dir_all</span><span class="p">(</span><span class="o">&</span><span class="n">dest_path</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't create directory"</span><span class="p">);</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">asset_map</span> <span class="o">=</span> <span class="nn">HashMap</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="nn">WalkDir</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">assets_dir</span><span class="p">)</span><span class="nf">.into_iter</span><span class="p">()</span><span class="nf">.for_each</span><span class="p">(|</span><span class="n">entry</span><span class="p">|</span> <span class="p">{</span>
<span class="k">let</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span> <span class="o">=</span> <span class="n">entry</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span><span class="p">;</span> <span class="p">};</span>
<span class="k">if</span> <span class="o">!</span><span class="n">entry</span><span class="nf">.file_type</span><span class="p">()</span><span class="nf">.is_file</span><span class="p">()</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">};</span>
<span class="k">let</span> <span class="n">file_name</span> <span class="o">=</span> <span class="n">entry</span><span class="nf">.file_name</span><span class="p">()</span><span class="nf">.to_string_lossy</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
<span class="k">let</span> <span class="n">root_file</span> <span class="o">=</span> <span class="n">entry</span>
<span class="nf">.path</span><span class="p">()</span>
<span class="nf">.file_stem</span><span class="p">()</span>
<span class="nf">.unwrap</span><span class="p">()</span>
<span class="nf">.to_string_lossy</span><span class="p">()</span>
<span class="nf">.to_string</span><span class="p">();</span>
<span class="k">let</span> <span class="n">extension</span> <span class="o">=</span> <span class="n">entry</span>
<span class="nf">.path</span><span class="p">()</span>
<span class="nf">.extension</span><span class="p">()</span>
<span class="nf">.unwrap</span><span class="p">()</span>
<span class="nf">.to_string_lossy</span><span class="p">()</span>
<span class="nf">.to_string</span><span class="p">();</span>
<span class="k">if</span> <span class="p">[</span><span class="s">"woff"</span><span class="p">,</span> <span class="s">"woff2"</span><span class="p">]</span><span class="nf">.contains</span><span class="p">(</span><span class="o">&</span><span class="n">extension</span><span class="nf">.as_str</span><span class="p">())</span> <span class="p">{</span>
<span class="c1">// Those file already have hashes in their filenames</span>
<span class="k">let</span> <span class="n">source</span> <span class="o">=</span> <span class="n">entry</span><span class="nf">.path</span><span class="p">()</span><span class="nf">.to_string_lossy</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
<span class="k">let</span> <span class="n">dest</span> <span class="o">=</span> <span class="n">dest_path</span><span class="nf">.join</span><span class="p">(</span><span class="o">&</span><span class="n">file_name</span><span class="p">)</span><span class="nf">.to_string_lossy</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
<span class="nn">fs</span><span class="p">::</span><span class="nf">copy</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't copy file"</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">let</span> <span class="n">file_content</span> <span class="o">=</span> <span class="nn">fs</span><span class="p">::</span><span class="nf">read_to_string</span><span class="p">(</span><span class="n">entry</span><span class="nf">.path</span><span class="p">())</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't read file"</span><span class="p">);</span>
<span class="k">let</span> <span class="n">hash</span> <span class="o">=</span> <span class="nf">calculate_sha1</span><span class="p">(</span><span class="n">file_content</span><span class="nf">.as_str</span><span class="p">());</span>
<span class="k">let</span> <span class="n">new_filename</span> <span class="o">=</span> <span class="nd">format!</span><span class="p">(</span><span class="s">"{}.{}.{}"</span><span class="p">,</span> <span class="n">root_file</span><span class="p">,</span> <span class="n">hash</span><span class="p">,</span> <span class="n">extension</span><span class="p">);</span>
<span class="k">let</span> <span class="n">source</span> <span class="o">=</span> <span class="n">entry</span><span class="nf">.path</span><span class="p">()</span><span class="nf">.to_string_lossy</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
<span class="k">let</span> <span class="n">dest</span> <span class="o">=</span> <span class="n">dest_path</span><span class="nf">.join</span><span class="p">(</span><span class="o">&</span><span class="n">new_filename</span><span class="p">)</span><span class="nf">.to_string_lossy</span><span class="p">()</span><span class="nf">.to_string</span><span class="p">();</span>
<span class="nn">fs</span><span class="p">::</span><span class="nf">copy</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't copy file"</span><span class="p">);</span>
<span class="c1">// Keep track of old and new filenames</span>
<span class="n">asset_map</span><span class="nf">.insert</span><span class="p">(</span><span class="n">file_name</span><span class="p">,</span> <span class="n">new_filename</span><span class="p">);</span>
<span class="p">});</span>
<span class="c1">// Write the map to a manifest.json</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">file</span> <span class="o">=</span> <span class="nn">fs</span><span class="p">::</span><span class="nn">File</span><span class="p">::</span><span class="nf">create</span><span class="p">(</span><span class="nn">Path</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">&</span><span class="n">dest_path</span><span class="p">)</span><span class="nf">.join</span><span class="p">(</span><span class="s">"manifest.json"</span><span class="p">))</span>
<span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't write asset_map.rs"</span><span class="p">);</span>
<span class="k">let</span> <span class="n">data</span> <span class="o">=</span> <span class="nn">serde_json</span><span class="p">::</span><span class="nf">to_string</span><span class="p">(</span><span class="o">&</span><span class="n">asset_map</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="n">file</span><span class="nf">.write_all</span><span class="p">(</span><span class="n">data</span><span class="nf">.as_bytes</span><span class="p">())</span>
<span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't write content"</span><span class="p">);</span>
<span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
<span class="k">fn</span> <span class="nf">calculate_sha1</span><span class="p">(</span><span class="n">input</span><span class="p">:</span> <span class="o">&</span><span class="nb">str</span><span class="p">)</span> <span class="k">-></span> <span class="nb">String</span> <span class="p">{</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">hasher</span> <span class="o">=</span> <span class="nn">Sha1</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="n">hasher</span><span class="nf">.update</span><span class="p">(</span><span class="n">input</span><span class="p">);</span>
<span class="k">let</span> <span class="n">result</span> <span class="o">=</span> <span class="n">hasher</span><span class="nf">.finalize</span><span class="p">();</span>
<span class="nd">format!</span><span class="p">(</span><span class="s">"{:x}"</span><span class="p">,</span> <span class="n">result</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You also need to change your <code class="language-plaintext highlighter-rouge">Cargo.toml</code> file:</p>
<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Cargo.toml</span>
<span class="nn">[build-dependencies]</span>
<span class="py">sha1</span> <span class="p">=</span> <span class="s">"0.10"</span>
<span class="py">walkdir</span> <span class="p">=</span> <span class="s">"2.3"</span>
<span class="py">anyhow</span> <span class="p">=</span> <span class="s">"1.0"</span>
<span class="nn">serde_json</span> <span class="o">=</span> <span class="p">{</span> <span class="py">version</span> <span class="p">=</span> <span class="s">"^1"</span> <span class="p">}</span>
</code></pre></div></div>
<h3 id="3-deliver-asset-with-actix">3. Deliver asset with actix</h3>
<p><a href="https://actix.rs/docs/static-files/">The static files</a> feature for Actix
allows you to easily deliver those assets yourself.</p>
<h3 id="4-helper-method">4. Helper method</h3>
<p>The following code helps me linking assets in HTML templates.</p>
<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// assets_decorator.rs</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::</span><span class="nn">path</span><span class="p">::</span><span class="n">Path</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::{</span><span class="nn">collections</span><span class="p">::</span><span class="n">HashMap</span><span class="p">,</span> <span class="nn">fs</span><span class="p">::</span><span class="n">File</span><span class="p">,</span> <span class="nn">io</span><span class="p">::</span><span class="n">Read</span><span class="p">};</span>
<span class="k">const</span> <span class="nb">DIR</span><span class="p">:</span> <span class="o">&</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"public/assets"</span><span class="p">;</span>
<span class="k">const</span> <span class="n">PUBLIC_DIR</span><span class="p">:</span> <span class="o">&</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"/assets"</span><span class="p">;</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">asset_path</span><span class="p">(</span><span class="n">filename</span><span class="p">:</span> <span class="o">&</span><span class="nb">str</span><span class="p">)</span> <span class="k">-></span> <span class="nb">Result</span><span class="o"><</span><span class="nb">String</span><span class="p">,</span> <span class="nn">anyhow</span><span class="p">::</span><span class="n">Error</span><span class="o">></span> <span class="p">{</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">file_content</span> <span class="o">=</span> <span class="nn">String</span><span class="p">::</span><span class="nf">new</span><span class="p">();</span>
<span class="k">let</span> <span class="k">mut</span> <span class="n">file</span> <span class="o">=</span> <span class="nn">File</span><span class="p">::</span><span class="nf">open</span><span class="p">(</span><span class="nd">format!</span><span class="p">(</span><span class="s">"{}/manifest.json"</span><span class="p">,</span> <span class="nb">DIR</span><span class="p">))</span><span class="o">?</span><span class="p">;</span>
<span class="n">file</span><span class="nf">.read_to_string</span><span class="p">(</span><span class="o">&</span><span class="k">mut</span> <span class="n">file_content</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="k">let</span> <span class="n">data</span><span class="p">:</span> <span class="n">HashMap</span><span class="o"><</span><span class="nb">String</span><span class="p">,</span> <span class="nb">String</span><span class="o">></span> <span class="o">=</span> <span class="nn">serde_json</span><span class="p">::</span><span class="nf">from_str</span><span class="p">(</span><span class="o">&</span><span class="n">file_content</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
<span class="k">match</span> <span class="n">data</span><span class="nf">.get</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">Some</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">=></span> <span class="nf">Ok</span><span class="p">(</span><span class="nd">format!</span><span class="p">(</span><span class="s">"{}/{}"</span><span class="p">,</span> <span class="n">PUBLIC_DIR</span><span class="p">,</span> <span class="n">filename</span><span class="p">)),</span>
<span class="nb">None</span> <span class="k">=></span> <span class="nf">Err</span><span class="p">(</span><span class="nn">anyhow</span><span class="p">::</span><span class="nd">anyhow!</span><span class="p">(</span><span class="s">"Asset not found"</span><span class="p">)),</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">pub</span> <span class="k">fn</span> <span class="nf">asset_tag</span><span class="p">(</span><span class="n">filename</span><span class="p">:</span> <span class="o">&</span><span class="nb">str</span><span class="p">)</span> <span class="k">-></span> <span class="nb">String</span> <span class="p">{</span>
<span class="k">let</span> <span class="n">filename</span> <span class="o">=</span> <span class="k">match</span> <span class="nf">asset_path</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">Ok</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">=></span> <span class="n">path</span><span class="p">,</span>
<span class="nf">Err</span><span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">=></span> <span class="k">return</span> <span class="nn">Default</span><span class="p">::</span><span class="nf">default</span><span class="p">(),</span>
<span class="p">};</span>
<span class="k">let</span> <span class="n">extension</span> <span class="o">=</span> <span class="nn">Path</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">&</span><span class="n">filename</span><span class="p">)</span>
<span class="nf">.extension</span><span class="p">()</span>
<span class="nf">.expect</span><span class="p">(</span><span class="s">"Can't get filename extension"</span><span class="p">)</span>
<span class="nf">.to_string_lossy</span><span class="p">()</span>
<span class="nf">.to_string</span><span class="p">();</span>
<span class="k">match</span> <span class="n">extension</span><span class="nf">.as_str</span><span class="p">()</span> <span class="p">{</span>
<span class="s">"js"</span> <span class="k">=></span> <span class="nd">format!</span><span class="p">(</span><span class="s">"<script src=</span><span class="se">\"</span><span class="s">{}</span><span class="se">\"</span><span class="s">></script>"</span><span class="p">,</span> <span class="n">filename</span><span class="p">),</span>
<span class="s">"css"</span> <span class="k">=></span> <span class="nd">format!</span><span class="p">(</span><span class="s">"<link rel=</span><span class="se">\"</span><span class="s">stylesheet</span><span class="se">\"</span><span class="s"> href=</span><span class="se">\"</span><span class="s">{}</span><span class="se">\"</span><span class="s">>"</span><span class="p">,</span> <span class="n">filename</span><span class="p">),</span>
<span class="n">_</span> <span class="k">=></span> <span class="nn">Default</span><span class="p">::</span><span class="nf">default</span><span class="p">(),</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>I currently use <a href="https://github.com/djc/askama">askama</a>, the template looks like:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"><!-- layout.html --></span>
<span class="nt"><head></span>
{{ crate::decorators::assets_decorator::asset_tag("custom.css")|safe }}
<span class="nt"></head></span>
</code></pre></div></div>
<p>and the generated output will look like:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><head></span>
<span class="nt"><link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"/assets/custom.617aacf2e80dea7b5970121248dfe3aadcd844ef.css"</span><span class="nt">></span>
<span class="nt"></head></span>
</code></pre></div></div>
<h3 id="automate-it-all">Automate it all</h3>
<p>I automate all this with a <code class="language-plaintext highlighter-rouge">Makefile</code> entry. I first manually copy static
assets from node packages like <a href="https://preline.co">preline</a> or
<a href="https://flowbite.com">flowbite</a>. I also use the
<a href="https://tailwindcss.com">tailwind</a> cli tool builder.</p>
<div class="language-makefile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Makefile
</span><span class="nl">build_assets</span><span class="o">:</span>
<span class="nb">rm</span> <span class="nt">-rf</span> assets public/assets
<span class="nb">mkdir </span>assets
<span class="nb">cp </span>node_modules/preline/dist/preline.js assets/
<span class="nb">cp </span>node_modules/flowbite/dist/flowbite.min.js assets/
<span class="nb">cp </span>node_modules/bootstrap-icons/bootstrap-icons.svg assets/
./build.js
npx tailwindcss <span class="nt">--minify</span> <span class="nt">-i</span> ./css/tailwind.css <span class="nt">-o</span> ./assets/tailwind.css
<span class="nb">touch </span>build.rs <span class="c"># force rebuilding</span>
</code></pre></div></div>
<h4 id="ci">CI</h4>
<p>I use <a href="https://www.drone.io">Drone</a> for the CI and the asset building is done with the following:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">build assets</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">node:18-bullseye</span>
<span class="na">commands</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">npm install</span>
<span class="pi">-</span> <span class="s">make build_assets</span>
</code></pre></div></div>
Memory usage of Ruby vs Rust2022-05-30T16:30:00+00:00https://pen.so/2022/05/30/ruby-vs-rust-memory-usageFabien Pensohttps://pen.so
<p>I currently work at <a href="https://www.beamapp.co">Beam</a>, and more specifically on
the server-side API allowing our users to synchronize their data on all their
devices. For privacy reasons, this API is E2EE and we don’t see anything except
encrypted blobs. The private keys are stored on the user’s device.</p>
<p>This API is currently in Ruby and called through GraphQL and REST endpoints.
This API will potentially manage a lot of data, and it became clear Ruby
wouldn’t fit the need. It’s very hard to stream response, and Ruby’s memory
usage is usually too high. And as a Ruby coder since 2005, it pains me to say
it.</p>
<p>I spent the last year reading about Rust (and Go) on evenings and weekends, and
prototyping code to get a feeling of the language and what it promises to
deliver. We recently decided it would be time to now work on it day-time and
start replacing endpoints. It’s far from being over yet, but I already have
some feedback.</p>
<p>The first endpoint I’m rewriting is fetching rows from a <em>pgsql</em> database,
and streaming results as JSON to the HTTP client. I’m testing this for over
50,000 rows and over 200MB of data. We are currently hosted on Heroku, and the
Ruby instance has the following memory metrics.</p>
<p class="post_photo"><a href="/img/ruby-vs-rust-memory-1.png"><img src="/img/ruby-vs-rust-memory-1.png" alt="/img/ruby-vs-rust-memory-1.png" /></a></p>
<p>It uses up to 2GB of memory, which is expected for a Rails stack.</p>
<hr />
<p>And now the Rust metrics.</p>
<p class="post_photo"><a href="/img/ruby-vs-rust-memory-2.png"><img src="/img/ruby-vs-rust-memory-2.png" alt="/img/ruby-vs-rust-memory-2.png" /></a></p>
<p>The Rust endpoint has been done using <strong>actix</strong>, <strong>sqlx</strong>, <strong>serde</strong>, and a few
others.</p>
<p>It uses up to 4MB… About 500x time less. The benchmarks I did shows 30%
speed improvements as well, which to be honest was deceptive. <strong>But</strong> the Rust
instance runs on a different Heroku dyno, with a 10x smaller cost per month.
Moving the Rust instance to the same Ruby instance didn’t improve speed, I
guess the bottleneck is on our pgsql instance.</p>
<p>Our Ruby stack is doing a lot more for now, but it’s still impressive
nonetheless. I have the feeling Rust will become very popular among server-side
micro-services.</p>
<hr />
<p><strong>Update on June 4th</strong>: after releasing this endpoint in production, as stated
in <a href="https://twitter.com/fabienpenso/status/1532682979662872576">this tweet</a> I
actually saw a x12 performance increase, while being on a 10x cheaper Heroku
dyno instance. Memory stayed around 4MB as well. I suggest to read
<a href="https://www.reddit.com/r/rust/comments/v3xgqp/rust_vs_rubyror_performances_for_api_calls/">comments</a>
of this reddit post.</p>
Improve Docker performance on macOS by 20x2021-09-02T00:00:00+00:00https://pen.so/2021/09/02/docker-on-macosFabien Pensohttps://pen.so
<p><strong>TL;DR</strong>: — Docker is great to manage your code, but it’s painfully slow on
macOS <sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup>. By using Virtualbox or Parallels, you can make your Rails on
Docker on macOS going way faster (<strong>x20</strong> on M1…). I ran benchmarks so you
don’t have to.</p>
<p>For years, when working on a Rails app, I would embed a <code class="language-plaintext highlighter-rouge">Vagrantfile</code> in the
repository so anyone joining the project could do a <code class="language-plaintext highlighter-rouge">vagrant up</code> and start
coding.</p>
<p>Docker became more famous since, and the convenience of Docker Compose to start
dependencies like a database, a Redis, or a mail server made it become a solid
contender for Vagrant. I use both on macOS because of how slow Docker is.</p>
<p>Docker is <strong>great</strong> on Linux but <strong>painfully slow</strong> on macOS and even more on
M1. How slow? And how to make it way faster? Run it on a Linux VM. I
benchmarked the same code and got the following results.</p>
<!--more-->
<h4 id="benchmark-results">Benchmark results</h4>
<p class="post_photo"><a href="/img/docker-benchmark.png"><img src="/img/docker-benchmark.png" alt="/img/docker-benchmark.png" /></a></p>
<h4 id="environment">Environment</h4>
<p>The two different computers I used to compare test execution time:</p>
<ol>
<li>A MacBook Pro 16” 2019, 32G RAM, 8-core Intel i9 2.3Ghz</li>
<li>A Mac mini M1 2020, 16G of RAM</li>
</ol>
<p>The three different environments I used on each computer:</p>
<ol>
<li>Docker Desktop</li>
<li>Parallels (M1) and VirtualBox (Intel)</li>
<li>Native (with homebrew)</li>
</ol>
<p>I bootstrapped an <a href="https://github.com/penso/vagrant-vs-docker-rails">empty Rails
repository</a>, then ran a <code class="language-plaintext highlighter-rouge">rails
g scaffold post</code> to have a default test suites, minimal but running. Bundle is
then used to run tests:</p>
<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span>git clone git@github.com:penso/vagrant-vs-docker-rails.git
<span class="nv">$ </span>docker-compose up
<span class="nv">$ </span><span class="nb">time </span>docker-compose run app bundle <span class="nb">exec </span>rake <span class="nb">test</span></code></pre></figure>
<h4 id="conclusion">Conclusion</h4>
<p>Don’t use Docker Desktop on macOS. If you must, use it within a Linux VM. While
Docker is very convenient to automate things, it has an <strong>x20</strong> incidence on
running tests on such a simple code. I noticed the difference might increase
with more significant projects.</p>
<p>I’m currently working at <a href="http://www.beamapp.co">Beam</a>, and I moved Docker from
our macOS CI servers to Linux servers instead because it would generate too
many issues. It also required too much memory.</p>
<p><a href="https://news.ycombinator.com/item?id=28398018">Read hackernews comments</a>.</p>
<div class="footnotes" role="doc-endnotes">
<ol>
<li id="fn:1" role="doc-endnote">
<p>I believe because of the way Docker does virtualization on MacOS. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">↩</a></p>
</li>
</ol>
</div>
Publish and host your Jekyll website on IPFS2021-08-21T00:00:00+00:00https://pen.so/2021/08/21/publish-on-ipfsFabien Pensohttps://pen.so
<p><strong>TL;DR</strong> — How I published this Jekyll website on IPFS within minutes, then
spent a few more hours improving the user experience.</p>
<p>A few weeks back, I wanted to play with <a href="https://ipfs.io">IPFS</a> and decided to
deploy my website on it. It was way easier than I anticipated, and within
hours, I had a pretty good understanding of what IPFS is, how to leverage it
and published this website on it.</p>
<h4 id="what-is-ipfs">What is IPFS?</h4>
<p><a href="https://docs.ipfs.io/concepts/what-is-ipfs/">IPFS</a> is a decentralized system
for storing and accessing files, websites, applications, and data. Contrary to
<a href="https://freenetproject.org">Freenet</a> where you keep others’ content on your
node, IPFS will only store whatever you uploaded to it or manually decided to
cache (<em>pin</em> in IPFS language).</p>
<p>Running a node is super easy. You just need to download their desktop app. You
will then be able to make any files available on the network by drag&dropping
them.</p>
<p>It seems IPFS will become one of the default storage layer of the Web 3.0, and
many NFTs are hosted on it.</p>
<!--more-->
<h4 id="build-your-website">Build your website</h4>
<p>I use <a href="https://jekyllrb.com">Jekyll</a>, but any static website generator will
work. With IPFS, your website might be browsed through a gateway, and you don’t
control the path under which it might be browsed at. For example, on the
<a href="https://ipfs.io/">ipfs.io</a> gateway, your root <em>index.html</em> file for your
website would be accessed at https://ipfs.io/ipfs/<em>cid</em>/index.html under
/ipfs/<em>cid</em>/.</p>
<p>This reminds me of a very long time ago, when your website could be hosted on
<em>http://something.com</em> as well as <em>http://something.com/~your_username</em>,
relative links were then mandatory.</p>
<p>With IPFS, you must make sure all links are relatives and not absolute. Jekyll
doesn’t easily do that, but the
<a href="https://www.npmjs.com/package/all-relative">all-relative</a> npm package will do
that for you. My <code class="language-plaintext highlighter-rouge">build</code> Makefile rule is:</p>
<figure class="highlight"><pre><code class="language-make" data-lang="make"><span class="nl">build</span><span class="o">:</span>
<span class="err">bundle</span> <span class="err">exec</span> <span class="err">jekyll</span> <span class="err">build</span>
<span class="err">cd</span> <span class="err">_site/</span> <span class="err">&&</span> <span class="err">npx</span> <span class="err">all-relative</span></code></pre></figure>
<h4 id="install-ipfs">Install IPFS</h4>
<p><img src="/generated/img/ipfs-desktop-800-bf58b7d20.png" srcset="/generated/img/ipfs-desktop-400-bf58b7d20.png 400w, /generated/img/ipfs-desktop-600-bf58b7d20.png 600w, /generated/img/ipfs-desktop-800-bf58b7d20.png 800w, /generated/img/ipfs-desktop-1000-bf58b7d20.png 1000w" /></p>
<p class="post_photo">Download and install <a href="https://ipfs.io/#install">IPFS Desktop</a> or use their
<a href="https://docs.ipfs.io/how-to/command-line-quick-start/#prerequisites">CLI</a>. If
using the CLI, you must run <code class="language-plaintext highlighter-rouge">ipfs init</code>.</p>
<h4 id="publish-content">Publish content</h4>
<p>Drag and drop your <code class="language-plaintext highlighter-rouge">_site</code> folder to the IPFS desktop application using the <strong>+
Import</strong> button, or use <code class="language-plaintext highlighter-rouge">ipfs add -r _site</code> with the CLI. That’s it. Your
website is now available online (check the <em>Share link</em> option on your folder
on the desktop app).</p>
<p>The <em>Share link</em> will give you a URL with the following format:
<em>https://ipfs.io/ipfs/*cid*</em>. When you visit this URL, ipfs.io will search for
the content based on the <em>cid</em>. It will fetch it from your local node which is
hosting a copy of the files.</p>
<p>ipfs.io is being used as an IPFS gateway. Gateways usually cache content for a
while, but the only real copy will be on your node. If you stop your node or
your computer isn’t connected anymore, gateways or other IPFS nodes won’t fetch
its content.</p>
<h4 id="pin-your-files-on-another-node">Pin your files on another node</h4>
<p>To increase reliability, you want to pin your files on another IPFS node. Pin
means that the node will cache those files locally and host a copy of them. You
can also pin other cids on your local IPFS node.</p>
<p>Public pin services are available like <a href="http://pinata.cloud">pinata.cloud</a> or
<a href="https://nft.storage/">nft-storage</a>. I use both and a local IPFS node
on my Synology. My files are replicated on 3 locations minimum.</p>
<p><a href="https://cluster.ipfs.io">IPFS Cluster</a> and <a href="https://cluster.ipfs.io/documentation/collaborative/setup/">Collaborative
clusters</a> are
something I haven’t played with yet, but they will also give you redundancy.</p>
<h4 id="ipns">IPNS</h4>
<p>The <em>cid</em> hash depends on the content of your files, meaning every time you
change your website, that <em>cid</em> will change, and the gateway link will
change. This is not really efficient, so you will use
<a href="https://docs.ipfs.io/concepts/ipns/">IPNS</a>.</p>
<p>IPNS are hashes of a public key, and only the owner of the private key (you)
can sign it and link to the most version of your website.</p>
<p>Every time you store a new version of your website to your local node with <code class="language-plaintext highlighter-rouge">ipfs
add _site</code>, you will also publish it using <code class="language-plaintext highlighter-rouge">ipfs name publish *cid*</code>. This will
give you an IPNS address that will never change, based on your private
key stored in <code class="language-plaintext highlighter-rouge">~/.ipfs/config</code> and used to sign the files’ content.</p>
<p>If you change that private key (for example, if you use another computer), you
will have a different IPNS address. IPNS names always start with <code class="language-plaintext highlighter-rouge">k51...</code>. Once
you published those files, the URL to view them will be
<code class="language-plaintext highlighter-rouge">https://ipfs.io/ipns/k51...</code>, and this won’t change any more.</p>
<p>As an example, this website is available at
<a href="https://ipfs.io/ipns/k51qzi5uqu5dge5aqz5j93qml6s2sfjpg9qzc9wpcqv67ddvzbjz86c7fb35hk/">https://ipfs.io/ipns/k51qzi5uqu5dge5aqz5j93qml6s2sfjpg9qzc9wpcqv67ddvzbjz86c7fb35hk/</a></p>
<h4 id="dnslink">DNSLink</h4>
<p>Linking to IPNS names is a pain, it’s too long and hard to remember.
<a href="https://www.dnslink.io">DNSLink</a> allows you to connect your DNS to your current
IPFS <em>cid</em>. Just add a DNS entry under <code class="language-plaintext highlighter-rouge">_dnslink.domain.com</code> with a TXT value
of <code class="language-plaintext highlighter-rouge">/ipfs/*cid*</code> or <code class="language-plaintext highlighter-rouge">/ipns/k51...</code> and any IPFS gateway will allow you to link
to <em>https://ipfs.io/ipns/domain.com</em> instead:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>dig +short TXT _dnslink.pen.so
<span class="s2">"dnslink=/ipns/k51qzi5uqu5dge5aqz5j93qml6s2sfjpg9qzc9wpcqv67ddvzbjz86c7fb35hk/"</span></code></pre></figure>
<p>As an example, this website is available at
<a href="https://ipfs.io/ipns/pen.so/">https://ipfs.io/ipns/pen.so/</a></p>
<p>You’ll obviously want to use your IPNS and not your IPFS <em>cid</em> so you don’t
have to update your DNS anymore.</p>
<h4 id="ipfs-gateways">IPFS Gateways</h4>
<p>If you use Firefox and the <a href="https://addons.mozilla.org/en-US/firefox/addon/ipfs-companion/">IPFS Companion
extension</a>,
going to <a href="https://ipfs.io/ipns/pen.so">https://ipfs.io/ipns/pen.so</a> will automatically redirect
you to
<a href="http://pen.so.ipns.localhost:8080/">http://pen.so.ipns.localhost:8080/</a>.
<em>ipns.localhost:8080</em> is your local IPFS gateway, using IPFS Desktop.</p>
<p>You just accessed the
IPFS hosted files without a central authority (except the DNS query).</p>
<p>But you can’t expect users to have IPFS installed. A user without a local IPFS
node can view any IPFS file through an IPFS gateway. There are <a href="https://www.reddit.com/r/ipfs/comments/lvwn4o/ipfs_http_gateways_ranked_by_performance/">many public
IPFS
gateways</a>
like <a href="https://gateway.pinata.cloud/ipns/pen.so">pinata</a>,
<a href="https://ipfs.io/ipns/pen.so/">ipfs.io</a>,
<a href="https://ipfs.infura.io/ipfs/QmSzii2DzscT6B72DdCJB5VJq718bW9QV55J7k5UeqRjve">infura</a>,
<a href="http://ipns.co/pen.so">ipns.co</a>, <a href="https://ipfs.fleek.co/ipns/pen.so">fleek</a>
but not all support IPNS naming, and some have very poor connectivity.</p>
<p>If you want to host your website on IPFS long-term, it’s best to just update
your DNS and point it to Cloudflare.</p>
<h4 id="dns-settings">DNS Settings</h4>
<p><a href="https://www.cloudflare.com/en-gb/distributed-web-gateway/">Cloudflare</a> offers
a way to host your IPFS content on your own domain instead, for free. Just add
a CNAME to <code class="language-plaintext highlighter-rouge">www.cloudflare-ipfs.com.</code> and submit your domain at the bottom of
<a href="https://www.cloudflare.com/en-gb/distributed-web-gateway/">this page</a>, so they
generate an SSL certificate:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>dig +short CNAME ipfs.pen.so
www.cloudflare-ipfs.com.</code></pre></figure>
<p>As an example, this website is available through the Cloudflare gateway at
<a href="https://ipfs.pen.so">https://ipfs.pen.so</a>. I decided to use the subdomain
<code class="language-plaintext highlighter-rouge">ipfs.pen.so</code> as a test, but I’m considering moving <code class="language-plaintext highlighter-rouge">pen.so</code> from Netlify to
IPFS. I’ve checked the past weeks, Cloudflare is very stable, and I see no
point in not going fully decentralized.</p>
<h4 id="ens">ENS</h4>
<p><img src="/generated/img/ens-domains-800-2e267ee8c.png" srcset="/generated/img/ens-domains-400-2e267ee8c.png 400w, /generated/img/ens-domains-600-2e267ee8c.png 600w, /generated/img/ens-domains-800-2e267ee8c.png 800w, /generated/img/ens-domains-1000-2e267ee8c.png 1000w" /></p>
<p class="post_photo">DNS is the last step from being entirely decentralized. You’ll
want to use <a href="https://ens.domains">https://ens.domains</a> instead, a decentralized
naming over Ethereum, used by many. It will use the Etherum
blockchain to store your naming.</p>
<p>I bought <code class="language-plaintext highlighter-rouge">penso.eth</code> and, using their interface, set my content to my IPNS
hash <code class="language-plaintext highlighter-rouge">/ipns/k51...</code>. When using Firefox, using <code class="language-plaintext highlighter-rouge">penso.eth/</code> in the URL will
know where to fetch the content on IPFS.</p>
<h4 id="link-domain">.link domain</h4>
<p>However since you can’t expect everyone to have the IPFS extension installed,
you can also use <a href="https://penso.eth.link">penso.eth.link</a>. Any <code class="language-plaintext highlighter-rouge">.eth</code> ENS
domain has a <code class="language-plaintext highlighter-rouge">.link</code> as a free IPFS gateway.</p>
<p>Users with the IPFS extension will automatically use their local IPFS gateway
for any <code class="language-plaintext highlighter-rouge">.link</code> domains.</p>
<h4 id="more">More</h4>
<p>The following links helped me understanding IPFS:</p>
<ul>
<li><a href="https://anandology.com/blog/website-on-ipfs/">Deploy a static website to IPFS</a></li>
<li><a href="https://vinta.ws/code/ipfs-the-very-slow-distributed-permanent-web.html">IPFS: The (very slow) distributed permanent web</a></li>
<li><a href="https://tbking.eth.link/blog/decentralization/Deploy-your-website-on-IPFS-Why-and-How/">Deploy your website on IPFS: Why and How</a></li>
<li><a href="https://medium.com/textileio/whats-really-happening-when-you-add-a-file-to-ipfs-ae3b8b5e4b0f">What’s really happening when you add a file to IPFS?</a></li>
<li><a href="https://stackoverflow.com/questions/47450007/where-does-ipfs-store-all-the-data">Where does IPFS store all the data?</a></li>
<li><a href="https://eth.link">eth.link</a></li>
<li><a href="https://fleek.co">Fleek</a>: Easy deploy website to IPFS. Feels like Netlify for IPFS.</li>
<li><a href="https://docs.ipfs.io/concepts/merkle-dag/">Merkle Directed Acyclic Graphcs</a></li>
<li><a href="https://github.com/ipfs/ipfs-docs/issues/801">IPNS, explanation needed about k51 key</a></li>
<li><a href="https://torrent-paradise.ml/">Torrent Paradise</a>: Torrent index on IPFS</li>
<li><a href="https://docs.ipfs.io/how-to/best-practices-for-nft-data/#types-of-ipfs-links-and-when-to-use-them">Best Practices for Storing NFT Data using IPFS</a></li>
</ul>
Swift CryptoKit and Browser2021-04-06T19:30:00+00:00https://pen.so/2021/04/06/aes-gcmFabien Pensohttps://pen.so
<p><a href="/2021/03/18/cryptokit-using-ruby-or-python/">This previous
article</a> explains
how to read ChaCha20-Poly encrypted data using Ruby or Python. My first goal is
to ensure other languages can read data encrypted within
<a href="https://www.beamapp.co/">Beam</a>, but the end goal is to decrypt it within your
browser, using client-side HTML and Javascript. Sadly,
<a href="http://www.w3.org/TR/WebCryptoAPI/">WebCrypto</a> omits ChaCha20-Poly, and I had
to move to <a href="https://en.wikipedia.org/wiki/Galois/Counter_Mode">AES-GCM</a>
instead.</p>
<p><a href="https://github.com/diafygi/webcrypto-examples#aes-gcm---decrypt">This
extensive</a>
documentation says to use <code class="language-plaintext highlighter-rouge">additionalData</code> for the tag part, but that never
worked on my code, and I had to do that manually.</p>
<p>Use the following in Xcode Playground to encrypt a string:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">import</span> <span class="kt">UIKit</span>
<span class="kd">import</span> <span class="kt">CryptoKit</span>
<span class="k">let</span> <span class="nv">str</span> <span class="o">=</span> <span class="s">"Hello, playground"</span>
<span class="k">let</span> <span class="nv">strData</span> <span class="o">=</span> <span class="n">str</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span><span class="nv">using</span><span class="p">:</span> <span class="o">.</span><span class="n">utf8</span><span class="p">)</span><span class="o">!</span>
<span class="k">let</span> <span class="nv">key</span> <span class="o">=</span> <span class="kt">SymmetricKey</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="o">.</span><span class="n">bits256</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">keyString</span> <span class="o">=</span> <span class="n">key</span><span class="o">.</span><span class="n">withUnsafeBytes</span> <span class="p">{</span> <span class="kt">Data</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span><span class="o">.</span><span class="nf">base64EncodedString</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">sealbox</span> <span class="o">=</span> <span class="k">try!</span> <span class="kt">AES</span><span class="o">.</span><span class="kt">GCM</span><span class="o">.</span><span class="nf">seal</span><span class="p">(</span><span class="n">strData</span><span class="p">,</span> <span class="nv">using</span><span class="p">:</span> <span class="n">key</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"Key: </span><span class="se">\(</span><span class="n">keyString</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"Combined: </span><span class="se">\(</span><span class="n">sealbox</span><span class="o">.</span><span class="n">combined</span><span class="o">!.</span><span class="nf">base64EncodedString</span><span class="p">()</span><span class="se">)</span><span class="s">"</span><span class="p">)</span></code></pre></figure>
<p>The output when running it on my computer (you obviously will get a different
result):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Key: lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec=
Combined: NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz
</code></pre></div></div>
<!--more-->
<h4 id="ruby">Ruby</h4>
<p>You can decode the encrypted string with the private key using Ruby:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"openssl"</span>
<span class="nb">require</span> <span class="s2">"base64"</span>
<span class="c1">### AES GCM</span>
<span class="n">key</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span> <span class="s2">"lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec="</span>
<span class="n">combined</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span> <span class="s2">"NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz"</span>
<span class="n">text</span> <span class="o">=</span> <span class="s2">"Hello, playground"</span>
<span class="c1"># Combined version</span>
<span class="n">combinedTag</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="o">-</span><span class="mi">16</span><span class="o">..-</span><span class="mi">1</span><span class="p">]</span>
<span class="n">combinedNonce</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="mi">0</span><span class="o">..</span><span class="mi">11</span><span class="p">]</span>
<span class="n">combinedCipherText</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="mi">12</span><span class="o">..</span><span class="p">(</span><span class="n">combined</span><span class="p">.</span><span class="nf">size</span><span class="o">-</span><span class="mi">17</span><span class="p">)]</span>
<span class="n">decipher</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">Cipher</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"AES-256-GCM"</span><span class="p">).</span><span class="nf">decrypt</span>
<span class="n">decipher</span><span class="p">.</span><span class="nf">key</span> <span class="o">=</span> <span class="n">key</span>
<span class="n">decipher</span><span class="p">.</span><span class="nf">iv</span> <span class="o">=</span> <span class="n">combinedNonce</span>
<span class="n">decipher</span><span class="p">.</span><span class="nf">auth_tag</span> <span class="o">=</span> <span class="n">combinedTag</span>
<span class="n">decrypted</span> <span class="o">=</span> <span class="n">decipher</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">combinedCipherText</span><span class="p">)</span> <span class="o">+</span> <span class="n">decipher</span><span class="p">.</span><span class="nf">final</span>
<span class="k">if</span> <span class="n">decrypted</span> <span class="o">==</span> <span class="n">text</span>
<span class="nb">puts</span> <span class="s2">"OK!"</span>
<span class="k">end</span></code></pre></figure>
<h4 id="python">Python</h4>
<p>You can decode the encrypted string with the private key using Python:</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1">#!/usr/bin/env python3
# Installation:
# pip install pycryptodome
</span>
<span class="kn">import</span> <span class="n">json</span>
<span class="kn">import</span> <span class="n">Crypto</span>
<span class="kn">from</span> <span class="n">base64</span> <span class="kn">import</span> <span class="n">b64decode</span>
<span class="kn">from</span> <span class="n">Crypto.Cipher</span> <span class="kn">import</span> <span class="n">AES</span>
<span class="n">clearText</span> <span class="o">=</span> <span class="s">"Hello, playground"</span>
<span class="n">key</span> <span class="o">=</span> <span class="nf">b64decode</span><span class="p">(</span><span class="s">"lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec="</span><span class="p">)</span>
<span class="n">combined</span> <span class="o">=</span> <span class="nf">b64decode</span><span class="p">(</span><span class="s">"NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz"</span><span class="p">)</span>
<span class="n">clearText</span> <span class="o">=</span> <span class="s">"Hello, playground"</span>
<span class="n">combinedNonce</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[:</span><span class="mi">12</span><span class="p">]</span>
<span class="n">combinedTag</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[:</span><span class="o">-</span><span class="mi">16</span><span class="p">]</span>
<span class="n">combinedCipher</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="mi">12</span><span class="p">:</span><span class="o">-</span><span class="mi">16</span><span class="p">]</span>
<span class="n">cipher</span> <span class="o">=</span> <span class="n">AES</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">AES</span><span class="p">.</span><span class="n">MODE_GCM</span><span class="p">,</span> <span class="n">nonce</span><span class="o">=</span><span class="n">combinedNonce</span><span class="p">)</span>
<span class="n">clear</span> <span class="o">=</span> <span class="n">cipher</span><span class="p">.</span><span class="nf">decrypt</span><span class="p">(</span><span class="n">combinedCipher</span><span class="p">).</span><span class="nf">decode</span><span class="p">()</span>
<span class="k">if</span> <span class="n">clearText</span> <span class="o">==</span> <span class="n">clear</span><span class="p">:</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"OK!"</span><span class="p">)</span></code></pre></figure>
<h4 id="html-and-javascript">HTML and Javascript</h4>
<p>You can decode the encrypted string with the private key using browser based HTML and Javascript:</p>
<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="c"><!-- index.html --></span>
<span class="nt"><html></span>
<span class="nt"><head></span>
<span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"UTF-8"</span><span class="nt">></span>
<span class="nt"><style></span>
<span class="nt">body</span> <span class="p">{</span> <span class="nl">font-size</span><span class="p">:</span> <span class="m">0.8em</span><span class="p">;</span> <span class="p">}</span>
<span class="nt">pre</span> <span class="p">{</span>
<span class="nl">white-space</span><span class="p">:</span> <span class="n">pre-wrap</span><span class="p">;</span> <span class="c">/* css-3 */</span>
<span class="nl">white-space</span><span class="p">:</span> <span class="n">-moz-pre-wrap</span><span class="p">;</span> <span class="c">/* Mozilla, since 1999 */</span>
<span class="nl">white-space</span><span class="p">:</span> <span class="n">-pre-wrap</span><span class="p">;</span> <span class="c">/* Opera 4-6 */</span>
<span class="nl">white-space</span><span class="p">:</span> <span class="n">-o-pre-wrap</span><span class="p">;</span> <span class="c">/* Opera 7 */</span>
<span class="nl">word-wrap</span><span class="p">:</span> <span class="n">break-word</span><span class="p">;</span> <span class="c">/* Internet Explorer 5.5+ */</span>
<span class="p">}</span>
<span class="nt"></style></span>
<span class="nt"></head></span>
<span class="nt"><body></span>
<span class="nt"><pre</span> <span class="na">id=</span><span class="s">"results"</span><span class="nt">></pre></span>
<span class="nt"></body></span>
<span class="nt"><script </span><span class="na">src=</span><span class="s">"aes_gcm.js"</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">></script></span>
<span class="nt"></html></span></code></pre></figure>
<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="c1">// aes_gcm.js</span>
<span class="kd">const</span> <span class="nx">fromBase64</span> <span class="o">=</span> <span class="nx">base64String</span> <span class="o">=></span> <span class="nb">Uint8Array</span><span class="p">.</span><span class="nf">from</span><span class="p">(</span><span class="nf">atob</span><span class="p">(</span><span class="nx">base64String</span><span class="p">),</span> <span class="nx">c</span> <span class="o">=></span> <span class="nx">c</span><span class="p">.</span><span class="nf">charCodeAt</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
<span class="kd">let</span> <span class="nx">clearText</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Hello, playground</span><span class="dl">"</span>
<span class="kd">let</span> <span class="nx">combined</span> <span class="o">=</span> <span class="nf">fromBase64</span><span class="p">(</span><span class="dl">"</span><span class="s2">NYsQV/IXJDyZgSY3hb/AQapynEBSIDXlO4TdMC+6F6DHmUBOnXEPcE/+sVrz</span><span class="dl">"</span><span class="p">)</span>
<span class="kd">let</span> <span class="nx">privateKey</span> <span class="o">=</span> <span class="nf">fromBase64</span><span class="p">(</span><span class="dl">"</span><span class="s2">lQ4F/9K45Ym9K8Qv9CkVrozkTsGij7/OErhzMmhb8Ec=</span><span class="dl">"</span><span class="p">)</span>
<span class="kd">let</span> <span class="nx">nonce</span> <span class="o">=</span> <span class="nx">combined</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">12</span><span class="p">)</span>
<span class="kd">let</span> <span class="nx">tag</span> <span class="o">=</span> <span class="nx">combined</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="o">-</span><span class="mi">16</span><span class="p">)</span>
<span class="kd">let</span> <span class="nx">cipher</span> <span class="o">=</span> <span class="nx">combined</span><span class="p">.</span><span class="nf">slice</span><span class="p">(</span><span class="mi">12</span><span class="p">,</span> <span class="o">-</span><span class="mi">16</span><span class="p">)</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nf">test</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">key</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nf">importKey</span><span class="p">(</span><span class="dl">"</span><span class="s2">raw</span><span class="dl">"</span><span class="p">,</span>
<span class="nx">privateKey</span><span class="p">,</span>
<span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AES-GCM</span><span class="dl">"</span> <span class="p">},</span>
<span class="kc">true</span><span class="p">,</span>
<span class="p">[</span><span class="dl">"</span><span class="s2">decrypt</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">encrypt</span><span class="dl">"</span><span class="p">])</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">encrypted</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nf">encrypt</span><span class="p">(</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AES-GCM</span><span class="dl">"</span><span class="p">,</span>
<span class="na">iv</span><span class="p">:</span> <span class="nx">nonce</span><span class="p">,</span>
<span class="p">},</span>
<span class="nx">key</span><span class="p">,</span>
<span class="k">new</span> <span class="nc">TextEncoder</span><span class="p">().</span><span class="nf">encode</span><span class="p">(</span><span class="nx">clearText</span><span class="p">)</span>
<span class="p">)</span>
<span class="nx">combinedEncrypted</span> <span class="o">=</span> <span class="nf">_append2Buffer</span><span class="p">(</span><span class="nx">nonce</span><span class="p">,</span> <span class="nx">encrypted</span><span class="p">)</span>
<span class="c1">// Encrypted version</span>
<span class="c1">// add_log(_arrayBufferToBase64(combinedEncrypted))</span>
<span class="kd">var</span> <span class="nx">decrypted</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nf">decrypt</span><span class="p">(</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AES-GCM</span><span class="dl">"</span><span class="p">,</span>
<span class="na">iv</span><span class="p">:</span> <span class="nx">nonce</span><span class="p">,</span>
<span class="c1">// Don't use `additionalData, it *does not* work</span>
<span class="p">},</span>
<span class="nx">key</span><span class="p">,</span>
<span class="nx">encrypted</span><span class="p">)</span>
<span class="c1">// Decrypt the encrypted version</span>
<span class="c1">//add_log(new TextDecoder().decode(decrypted))</span>
<span class="p">}</span> <span class="nf">catch</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">add_log</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">try</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">decrypted</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">window</span><span class="p">.</span><span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nf">decrypt</span><span class="p">(</span>
<span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">AES-GCM</span><span class="dl">"</span><span class="p">,</span>
<span class="na">iv</span><span class="p">:</span> <span class="nx">nonce</span><span class="p">,</span>
<span class="p">},</span>
<span class="nx">key</span><span class="p">,</span>
<span class="nf">_append2Buffer</span><span class="p">(</span><span class="nx">cipher</span><span class="p">,</span> <span class="nx">tag</span><span class="p">))</span>
<span class="nf">if </span><span class="p">(</span><span class="nx">clearText</span> <span class="o">==</span> <span class="k">new</span> <span class="nc">TextDecoder</span><span class="p">().</span><span class="nf">decode</span><span class="p">(</span><span class="nx">decrypted</span><span class="p">))</span> <span class="p">{</span>
<span class="nf">add_log</span><span class="p">(</span><span class="dl">"</span><span class="s2">OK</span><span class="dl">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span> <span class="nf">catch</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">add_log</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nf">test</span><span class="p">()</span>
<span class="cm">/*
* -----------------------------------------------------------------------
*/</span>
<span class="c1">// Add logs</span>
<span class="kd">function</span> <span class="nf">add_log</span><span class="p">(</span><span class="nx">text</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">results</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">getElementById</span><span class="p">(</span><span class="dl">"</span><span class="s2">results</span><span class="dl">"</span><span class="p">)</span>
<span class="nx">results</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">+=</span> <span class="nx">text</span> <span class="o">+</span> <span class="dl">"</span><span class="se">\n</span><span class="dl">"</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">text</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nf">_arrayBufferToBase64</span><span class="p">(</span><span class="nx">buffer</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">binary</span> <span class="o">=</span> <span class="dl">''</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">bytes</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint8Array</span><span class="p">(</span> <span class="nx">buffer</span> <span class="p">);</span>
<span class="kd">var</span> <span class="nx">len</span> <span class="o">=</span> <span class="nx">bytes</span><span class="p">.</span><span class="nx">byteLength</span><span class="p">;</span>
<span class="nf">for </span><span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o"><</span> <span class="nx">len</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">binary</span> <span class="o">+=</span> <span class="nb">String</span><span class="p">.</span><span class="nf">fromCharCode</span><span class="p">(</span> <span class="nx">bytes</span><span class="p">[</span> <span class="nx">i</span> <span class="p">]</span> <span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nb">window</span><span class="p">.</span><span class="nf">btoa</span><span class="p">(</span><span class="nx">binary</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">function</span> <span class="nf">_append2Buffer</span><span class="p">(</span><span class="nx">buffer1</span><span class="p">,</span> <span class="nx">buffer2</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">tmp</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint8Array</span><span class="p">(</span><span class="nx">buffer1</span><span class="p">.</span><span class="nx">byteLength</span> <span class="o">+</span> <span class="nx">buffer2</span><span class="p">.</span><span class="nx">byteLength</span><span class="p">)</span>
<span class="nx">tmp</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="k">new</span> <span class="nc">Uint8Array</span><span class="p">(</span><span class="nx">buffer1</span><span class="p">),</span> <span class="mi">0</span><span class="p">)</span>
<span class="nx">tmp</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="k">new</span> <span class="nc">Uint8Array</span><span class="p">(</span><span class="nx">buffer2</span><span class="p">),</span> <span class="nx">buffer1</span><span class="p">.</span><span class="nx">byteLength</span><span class="p">)</span>
<span class="k">return</span> <span class="nx">tmp</span><span class="p">.</span><span class="nx">buffer</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>
Swift CryptoKit and Ruby/Python2021-03-18T19:30:00+00:00https://pen.so/2021/03/18/cryptokit-using-ruby-or-pythonFabien Pensohttps://pen.so
<p>I spent days figuring out how to decrypt ChaChaPoly encrypted data with
Swift CryptoKit using other languages. What should have taken me minutes took
me hours. As a time savior, here is how you can decrypt it using Ruby or
Python. I ended up reading the source code of
<a href="https://github.com/apple/swift-crypto/blob/3c632a678e06ef15d6c9d93f7176dfb1de9e0696/Sources/Crypto/AEADs/ChachaPoly/ChaChaPoly.swift#L27">swift-crypto</a>
to understand what’s the combined sealbox was doing.</p>
<p>Use the following in Xcode Playground to encrypt a string:</p>
<figure class="highlight"><pre><code class="language-swift" data-lang="swift"><span class="kd">import</span> <span class="kt">UIKit</span>
<span class="kd">import</span> <span class="kt">CryptoKit</span>
<span class="k">let</span> <span class="nv">str</span> <span class="o">=</span> <span class="s">"Hello, playground"</span>
<span class="k">let</span> <span class="nv">strData</span> <span class="o">=</span> <span class="n">str</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span><span class="nv">using</span><span class="p">:</span> <span class="o">.</span><span class="n">utf8</span><span class="p">)</span><span class="o">!</span>
<span class="k">let</span> <span class="nv">key</span> <span class="o">=</span> <span class="kt">SymmetricKey</span><span class="p">(</span><span class="nv">size</span><span class="p">:</span> <span class="o">.</span><span class="n">bits256</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">keyString</span> <span class="o">=</span> <span class="n">key</span><span class="o">.</span><span class="n">withUnsafeBytes</span> <span class="p">{</span> <span class="kt">Data</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span><span class="o">.</span><span class="nf">base64EncodedString</span><span class="p">()</span>
<span class="k">do</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">sealbox</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">ChaChaPoly</span><span class="o">.</span><span class="nf">seal</span><span class="p">(</span><span class="n">strData</span><span class="p">,</span> <span class="nv">using</span><span class="p">:</span> <span class="n">key</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"Key: </span><span class="se">\(</span><span class="n">keyString</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"Combined: </span><span class="se">\(</span><span class="n">sealbox</span><span class="o">.</span><span class="n">combined</span><span class="o">.</span><span class="nf">base64EncodedString</span><span class="p">()</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> <span class="p">}</span></code></pre></figure>
<p>The output when running it on my computer (you obviously will get a different
result):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Key: j6tifPZTjUtGoz+1RJkO8dOMlu48MUUSlwACw/fCBw0=
Combined: OWFsadrLrBc6ak+6TiYhAI6JKvoQzVMpnRdJ6iE5vEiAhadrCu6EcEQiAs7G
</code></pre></div></div>
<p>You can decrypt it using Ruby with:</p>
<!--more-->
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"openssl"</span>
<span class="nb">require</span> <span class="s2">"base64"</span>
<span class="n">key</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span> <span class="s2">"j6tifPZTjUtGoz+1RJkO8dOMlu48MUUSlwACw/fCBw0="</span>
<span class="n">combined</span> <span class="o">=</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span> <span class="s2">"OWFsadrLrBc6ak+6TiYhAI6JKvoQzVMpnRdJ6iE5vEiAhadrCu6EcEQiAs7G"</span>
<span class="n">text</span> <span class="o">=</span> <span class="s2">"Hello, playground"</span>
<span class="n">combinedTag</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="o">-</span><span class="mi">16</span><span class="o">..-</span><span class="mi">1</span><span class="p">]</span>
<span class="n">combinedNonce</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="mi">0</span><span class="o">..</span><span class="mi">11</span><span class="p">]</span>
<span class="n">combinedCipherText</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="mi">12</span><span class="o">..</span><span class="p">(</span><span class="n">combined</span><span class="p">.</span><span class="nf">size</span><span class="o">-</span><span class="mi">17</span><span class="p">)]</span>
<span class="n">decipher</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">Cipher</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"chacha20-poly1305"</span><span class="p">).</span><span class="nf">decrypt</span>
<span class="n">decipher</span><span class="p">.</span><span class="nf">key</span> <span class="o">=</span> <span class="n">key</span>
<span class="n">decipher</span><span class="p">.</span><span class="nf">iv</span> <span class="o">=</span> <span class="n">combinedNonce</span>
<span class="n">decipher</span><span class="p">.</span><span class="nf">auth_tag</span> <span class="o">=</span> <span class="n">combinedTag</span>
<span class="n">decrypted</span> <span class="o">=</span> <span class="n">decipher</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">combinedCipherText</span><span class="p">)</span> <span class="o">+</span> <span class="n">decipher</span><span class="p">.</span><span class="nf">final</span>
<span class="k">if</span> <span class="n">decrypted</span> <span class="o">==</span> <span class="n">text</span>
<span class="nb">puts</span> <span class="s2">"OK!"</span>
<span class="k">end</span></code></pre></figure>
<p>And you can decrypt it using Python:</p>
<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1">#!/usr/bin/env python3
# Installation:
# pip install pycryptodome
</span>
<span class="kn">import</span> <span class="n">json</span>
<span class="kn">import</span> <span class="n">Crypto</span>
<span class="kn">from</span> <span class="n">base64</span> <span class="kn">import</span> <span class="n">b64decode</span>
<span class="kn">from</span> <span class="n">Crypto.Cipher</span> <span class="kn">import</span> <span class="n">ChaCha20_Poly1305</span>
<span class="n">key</span> <span class="o">=</span> <span class="nf">b64decode</span><span class="p">(</span><span class="s">"j6tifPZTjUtGoz+1RJkO8dOMlu48MUUSlwACw/fCBw0="</span><span class="p">)</span>
<span class="n">combined</span> <span class="o">=</span> <span class="nf">b64decode</span><span class="p">(</span><span class="s">"OWFsadrLrBc6ak+6TiYhAI6JKvoQzVMpnRdJ6iE5vEiAhadrCu6EcEQiAs7G"</span><span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="s">"Hello, playground"</span>
<span class="n">combinedNonce</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[:</span><span class="mi">12</span><span class="p">]</span>
<span class="n">combinedTag</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[:</span><span class="o">-</span><span class="mi">16</span><span class="p">]</span>
<span class="n">combinedCipher</span> <span class="o">=</span> <span class="n">combined</span><span class="p">[</span><span class="mi">12</span><span class="p">:</span><span class="o">-</span><span class="mi">16</span><span class="p">]</span>
<span class="n">decrypted</span> <span class="o">=</span> <span class="n">ChaCha20_Poly1305</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="n">key</span><span class="p">,</span> <span class="n">nonce</span><span class="o">=</span><span class="n">combinedNonce</span><span class="p">).</span><span class="nf">decrypt</span><span class="p">(</span><span class="n">combinedCipher</span><span class="p">).</span><span class="nf">decode</span><span class="p">()</span>
<span class="k">if</span> <span class="n">decrypted</span> <span class="o">==</span> <span class="n">text</span><span class="p">:</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"OK!"</span><span class="p">)</span></code></pre></figure>
<p>I hope this post saved you some time. Thanks to
<a href="https://twitter.com/FredericJacobs">@FredericJacobs</a> for the help.</p>
Mac Mini M12020-12-11T09:30:00+00:00https://pen.so/2020/12/11/mac-mini-m1Fabien Pensohttps://pen.so
<p class="has-drop-cap">After Apple’s announcement, I ordered an M1 Mac Mini and canceled it when I noticed the non-upgradable RAM. I then reordered it (16G/1T), and it has just arrived today :grin:</p>
<p>You’ve probably seen many online reviews (I watched tons of them on youtube) and what everyone says is true. It’s fast! Pretty stable, and I can’t hear the fan even while compiling.</p>
<p>Looking at my <a href="https://browser.geekbench.com/user/347515">geekbench</a> you’ll see it’s the fastest machine I own, CPU wise, even more than my MBP 16” 2020. On the compute GPU level, it’s slower, but that was expected from a Mac Mini. I’m glad I got rid of the Hackintosh…</p>
<!--more-->
<p>Safari also feels much faster, but I’m not sure it’s only the CPU. I’ve had many issues with my MBP16” having to reboot since coding with Xcode, or Safari slowness issue like DNS resolving latency. Tried to fix it using SquidMan, but that brought other problems. So far, none of those appeared on the M1.</p>
<p>Many suggested 8G is enough, but after half a day of work, I can see I’m already using more. But I have a few apps opened at once.</p>
<p><img src="/generated/img/m1_memory-800-884f2b8fe.png" srcset="/generated/img/m1_memory-400-884f2b8fe.png 400w, /generated/img/m1_memory-600-884f2b8fe.png 600w, /generated/img/m1_memory-800-884f2b8fe.png 800w, /generated/img/m1_memory-1000-884f2b8fe.png 1000w" /></p>
<h1 class="post_photo" id="dev-tools">Dev Tools</h1>
<h4 id="1password">1Password</h4>
<p>1Password released a 7.7.1 supporting M1. Install 1Password from their
website, enable beta builds in the update settings tab, and then check for a new update. Using their version instead of the AppStore also allows you to use the QRCode reader for 2FA.</p>
<h4 id="iterm">iTerm</h4>
<p>I’ve installed iTerm 3.4.1, which includes M1 support. I use this as my default term when I don’t need to run a Rosetta version of a command-line tool. You’ll want to run most of your commands for software installed through the terminal with <code class="language-plaintext highlighter-rouge">arch -x86_64</code>. For example, to run Jekyll, I use:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ arch -x86_64 jekyll serve --livereload --drafts
</code></pre></div></div>
<p>Maybe <code class="language-plaintext highlighter-rouge">alias a='arch -x86_64'</code> would be useful.</p>
<p>You can also <a href="https://www.notion.so/Run-x86-Apps-including-homebrew-in-the-Terminal-on-Apple-Silicon-8350b43d97de4ce690f283277e958602">set a Terminal with Rosetta</a>, to avoid having to use <code class="language-plaintext highlighter-rouge">arch -x86_64</code> all the time. Anything you’ll type inside will be running as intel version.</p>
<h4 id="homebrew">Homebrew</h4>
<p>This is definitely not ready, and unless you want to spend a huge amount of time compiling software manually one by one (I tried a few, but not everything is ready for M1), I suggest you use the Intel version either with the <code class="language-plaintext highlighter-rouge">arch</code> command or within a Rosetta terminal.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
</code></pre></div></div>
<p><em>Update</em>: Homebrew now supports M1 natively, and you can install it the classic way following instructions on their homepage.</p>
<p>You can then reinstall your existing list of brews doing the following:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># On your older Mac
$ brew tap Homebrew/bundle
$ brew bundle dump
# Move the Brewfile and then execute on your new Mac:
$ arch -x86_64 brew bundle
</code></pre></div></div>
<h4 id="logitech-mouse">Logitech Mouse</h4>
<p>There seems to be a bug with the MX Master 3 Mac mouse and the Bluetooth connection, which you can quickly fix with a unifier dongle (I had an old one). Don’t think you can do without this trick. It’s really annoying, and the mouse cursor hangs. But reconnecting my mouse with the dongle fixed the issue.</p>
<h4 id="direnv">Direnv</h4>
<p>I use <code class="language-plaintext highlighter-rouge">direnv</code> for settings environment variables in projects, xcode and rails. I had to <a href="https://golang.org/doc/install">install Go</a> manually first, and then build <code class="language-plaintext highlighter-rouge">direnv</code> from its source.</p>
<h4 id="xcode">Xcode</h4>
<p>Pretty expectedly, Xcode does work fine. But I was surprised it still took
forever (1h?) to install + unpack.</p>
<h4 id="karabiner-elements">Karabiner Elements</h4>
<p><strong>Update</strong>: I used to use this for years to remap command and option keys on my
PC based keyboard. But <a href="https://github.com/pqrs-org/Karabiner-Elements/issues/2513">it crashes
M1</a> computers on
reboots, meaning you have a <em>This computer has crashed</em> popup once restarted.
I deleted it, and now use the <a href="https://superuser.com/questions/80976/how-to-re-map-command-and-option-keys-on-mac-os-x-with-a-pc-keyboard/81843#81843">default
MacOS</a>
way to do that.</p>
<h1 id="conclusion">Conclusion</h1>
<p>I’m pretty impressed about this M1. It’s fast, silent, and I have all my tools
working either natively or through Rosetta. I can’t wait for <em>all</em> of them
being built for M1 for the speed gain, but using the Rosetta version doesn’t
feel slow at all.</p>
<p>It kinda feels like when arm64 was first released and you had to rebuild all
your Linux software, until a distribution fully supported it.</p>
<p>What do you like most about your M1?</p>
Own your email2020-12-10T00:00:00+00:00https://pen.so/2020/12/10/own-your-emailFabien Pensohttps://pen.so
<p class="has-drop-cap">Using <em>@gmail.com</em> for your email address is like living at someone’s house
without rent and potentially being kicked out any day without warning. All your
belongings inside, without any access.</p>
<p>One of my first jobs around 1997 was being a sysadmin and managing email
servers, writing <code class="language-plaintext highlighter-rouge">sendmail.cf</code> configuration files without M4, and I should
have known better.</p>
<p>Someone who used Gmail for over 10 years <a href="https://www.businessinsider.fr/us/google-users-locked-out-after-years-2020-10">recently got locked
out</a>
without explanation. When all services you use, tools, and all your life are
connected to your <em>@gmail.com</em> address, you can imagine how much of a nightmare
scenario this is.</p>
<!--more-->
<blockquote class="blockquote">
<p>“It feels like getting baited by all the convenience that Google offers, only for Google to use your data as it pleases and possibly takes it all away with no prior notice.”</p>
<p><cite>— <a href="https://www.businessinsider.fr/us/google-users-locked-out-after-years-2020-10">What it’s like to get locked out of Google indefinitely</a></cite></p>
</blockquote>
<p>Gmail has been here for so long it’s hard to imagine they can take it away from
you just as quickly. And good luck to get it back once that happens… Therefore
I highly recommend using their <a href="https://takeout.google.com/">Takeout</a> feature
to back up all your data and move away from using their domain <em>@gmail.com</em> in
your email address.</p>
<p>The unit of Internet space ownership is the domain name, get yours now.</p>
<p>I also use my <em>@gmail.com</em> too much because it’s easy, and its spam filtering
is so good. But I’ve reconsidered it, moved away, and use a non-public address
on my own domain when registering for new services. The following is a detail
of what I did and what I used to implement it.</p>
<h4 id="email-portability">Email portability</h4>
<p>Mobile phone numbers are so critical to everyday life that France has a law
allowing you to keep your phone number using a <a href="https://fr.wikipedia.org/wiki/Relev%C3%A9_d%27identit%C3%A9_op%C3%A9rateur">Relevé d’identité
opérateur</a>
(carrier identity number) when changing carrier. This service must be provided
free of charge.</p>
<p>Email addresses are essential, and portability is as key for them as for phone
numbers. But you can’t keep the same address when switching from one provider
to another. Once you start using <em>something@gmail.com</em>, it is painful to move
to a new one as you have to change it on every service you use, confirming each
one, one by one.</p>
<p><a href="https://twitter.com/dhh/status/1323582505065320448">@dhh says</a> <em>Hey!</em> will
forward your email for life once you paid for the first year. That should be
mandatory for all providers, so you don’t have this Gmail life single point of
failure. It’s best to own your own domain name, but this is a lesser evil than
most other email providers.</p>
<p>I believe email providers should be legally obliged to forward your email
address for life to a new email address. A routing system similar to
<a href="https://fr.wikipedia.org/wiki/Relev%C3%A9_d%27identit%C3%A9_op%C3%A9rateur">RIO</a>
preventing the old provider from having to forward them to the new one would be
best, but this is not technically possible with the SMTP Protocol.</p>
<p>Some people went as far as <a href="https://kevq.uk/de-googling-my-life-2-years-on/">De-Googling their
life</a> completely with success.</p>
<h1 id="my-current-email-setup">My current email setup</h1>
<p>I created my <a href="https://www.hey.com">hey</a> account for both making sure
fabienAThey.com would be mine if I ever wanted to use it, and for trying it
after viewing <a href="https://www.youtube.com/watch?v=UCeYTysLyGI&t=175s">this video</a>
about the service. Some of the features really make sense, like editing email
subjects <em>after</em> you received them or grouping multiple threads, but I still
prefer classic email interfaces.</p>
<p>After looking at a few options, I opted for
<a href="https://www.fastmail.com">Fastmail</a> for their pricing, security disclaimer,
and overall good reputation. I used <a href="https://kolabnow.com/">Kolab</a> for years
but decided to move away. I also quickly tried Zoho but had a bad experience
with their UI trying to set things up.</p>
<p>I also used FM import feature to get all my old emails from Kolab back to FM.</p>
<p>I enabled catch-all emails on FM, meaning any email to my domain is redirected
to me. Anywhere I register, I use a different email address based on the
service’s name (service@my_domain_com). I can easily set specific filters like
anything sent to service@mydomain goes to its folder and skip the inbox, or
find who resell my email.</p>
<p>It used to be a real pain to do that, but the password manager included in
Safari (or 1Password) now remembers which email you used to register to a
service. You don’t have to remember that yourself anymore.</p>
<p>I added a server filter. Anything matching /unsubscribe/ goes to a specific
/unsub/ folder and skip the inbox. All newsletters usually get caught in this.
I enabled server-side spam filtering on FM and installed
<a href="https://c-command.com/spamsieve/">Spamsieve</a> on my laptop for a local bayesian
filter, moving detected spam to a Junk folder.</p>
<p>I use <a href="https://freron.com">Mailmate</a> to read emails, which is by far the best
email client I ever found on macOS (a long way from Linux and in order Elm,
Pine, Mutt, Gnus). I now remember why I started using Gmail… Because I was
coming from those MUA!</p>
Heritage is closing2020-11-24T21:30:00+00:00https://pen.so/2020/11/24/heritage-is-closingFabien Pensohttps://pen.so
<p class="has-drop-cap">I started working on <a href="http://heritage.io">Heritage</a> in 2012 to have a place for
film analog photographers to show and tell about their work. To enforce quality
and consistency over the site, I put an invitation system in place, and
existing members had to invite you if you wanted to upload photos. It also
allowed you to have your gallery on your domain, a feature still used as of
today by some of you.</p>
<p>Years going, other projects got me very busy, and I never invested enough
energy for Heritage to take off. My last code contribution is over five years
old; some libraries are outdated, not maintained, or even have known bugs. If
I had to redo Heritage today, I would do it very differently. But upgrading the
existing code would take too much effort, and let’s also be honest, there are
now better ways to show your work than what I did.</p>
<p>Since then, more options became available for photographers to easily publish
their work, <a href="https://portfolio.adobe.com/">Adobe Portfolio</a>,
<a href="https://www.squarespace.com">Squarespace</a>, <a href="https://www.format.com">Format</a>,
<a href="https://www.exposure.co">Exposure</a> or even <a href="https://medium.com">Medium</a> and
<a href="https://www.wordpress.com">WordPress</a>. Many photographers are simply using
Instagram.</p>
<!--more-->
<p>With all that in mind and the very little time I have, I see no other option
than taking the website offline. Therefore, I will close Heritage on January
1st, 2021, and delete all pictures and its database.</p>
<p>I still think a place for analog photographers to converse and share is needed.
Instagram went from a photography app to a lifestyle full-time bloated
advertisement application for influencers; Facebook became a useless
service; Flickr never really came back following its multiple acquisitions;
500px isn’t the same. We all consume a massive amount of data from one photo to
another, rarely stopping and taking things slow. We (I?) badly need a way to
view and discuss pictures we care about without the noise brought by the
previously mentioned services.</p>
<p>Curated and handcrafted works matter. I wish I had a place for analog
photographers to feel safe and at home. If you had an account on Heritage, I
want to thank you for being part of this experiment. I hope to meet all of you
one day or another, and keep shooting films!</p>
<p>Some approximated numbers about the project:</p>
<ul>
<li>3,082 photos</li>
<li>223 stories</li>
<li>171 photographers</li>
<li><del>4,473</del> ~300 user accounts, rest was spam</li>
<li>14,362 email addresses</li>
<li>398 comments and likes</li>
<li>3,510,993 (3.5 million) total views on photos</li>
<li>~1,500 euros cost. Mostly hosting at DigitalOcean and paid by me only.</li>
</ul>
<p>Here is a non-exhaustive list of photographers who participated, whom work I like.</p>
<ul>
<li><a href="https://www.danielcuthbert.com">Daniel Cuthbert</a></li>
<li><a href="http://www.sylvaindemange.com">Sylvain Demange</a></li>
<li><a href="https://jeromebardenet.com">Jerome Bardenet</a></li>
<li><a href="https://www.rajbhardwaj.com">Raj Bhardwaj</a></li>
<li><a href="https://banastas.photo">Bull Anastas</a></li>
<li><a href="https://www.ryancarver.com">Ryan Carver</a></li>
<li><a href="https://www.instagram.com/yordalhb">Fabien Lefevre aka Yorda</a></li>
<li><a href="https://www.scotturnerphoto.com">Scott Turner</a></li>
<li><a href="http://pascalreydet.com/">Pascal Reydet</a></li>
<li><a href="https://yael-paris.fr">Yael Paris</a></li>
<li><a href="https://remilagoin.com">Remi Lagoin</a></li>
<li><a href="https://fabricedeutscher.com">Fabrice Deutscher</a></li>
<li><a href="http://www.fototheque.com">Matthew Joseph aka fotodudenz</a></li>
<li><a href="https://www.karlfrench.com">Karl French</a></li>
<li><a href="https://www.philippwortmann.com">Philipp Wortmann</a></li>
<li><a href="https://www.tamaraporras.com/">Tamara Porras</a></li>
<li><a href="http://nire.free.fr">Nicolas Rémond</a></li>
<li><a href="https://ckreature.exposure.co">Norrin Radd</a></li>
<li><a href="https://nicolashermann.com">Nicolas Hermann</a></li>
<li><a href="https://www.ashkelleher.com">Ash Kelleher</a></li>
<li><a href="https://www.gillapie.com/gallery">Ryan Gillespie</a></li>
<li><a href="https://www.nassio.com">Nassio</a></li>
<li><a href="https://www.walterrothwell.com">Walter Rothwell</a></li>
<li><a href="https://christophethillier.format.com">Christophe Thillier</a></li>
<li><a href="http://www.cyrilfakiri.com">Cyril Fakiri</a></li>
<li><a href="https://www.andrewjanjigian.com">Andrew Janjigian</a></li>
<li><a href="http://www.richardpjlambert.com/">Richard PJ Lambert</a></li>
<li><a href="https://www.benyaminreich.com">Benjamin Reich</a></li>
<li><a href="https://www.samscott.co">Sam Scott</a></li>
<li><a href="https://www.fabricemullerphotography.ch/">Fabrice Muller</a></li>
<li><a href="https://www.flickr.com/people/fredericnoe/">Frederic Noe</a>, a friend who sadly passed away.</li>
</ul>
<p>Which photographers do you enjoy most? What tools and platform do you use to
view and discuss with peers?</p>
Moving away from Hackintosh2020-11-18T18:00:00+00:00https://pen.so/2020/11/18/moving-away-from-hackintoshFabien Pensohttps://pen.so
<p class="has-drop-cap">After Apple’s announcement, I ordered an M1 Mini only to cancel it when I
noticed the 16G non-upgradable RAM. I just reordered it, and I plan to retire
my <a href="https://pen.so/2014/11/02/switching-to-hackintosh/">seven years old
Hackintosh</a>. It served me
well, but after spending the whole weekend trying to upgrade it, having issues
with Clover, OpenCore, and find later than Big Sur might not run on it, I
decided it was time to move on. The last nail in the coffin was when I remember
I had to fix iCloud after moving to OpenCore. When using a Hackintosh, you have
to find a matching working serial that Apple servers will accept. After giving
a try with ten random ones without luck, I also remembered you might be locked
out of Apple services for security reasons, with the only solution to call them
to unlock your account. I can only imagine the explanation I’d have to give to
the Apple support.</p>
<p>I am way more dependant on my Apple account than before, mobile apps and the
AppStore, iCloud storage through many Mac apps, sync between devices, iCloud
files, Keychain sync, etc. Being locked out of it would suck.</p>
<p>When reading this <a href="https://pen.so/2014/11/02/switching-to-hackintosh/">seven-year-old
post</a>, I remember I used to
have a mini before because the current MacPro had not been upgraded for years.
The then-new MacPro didn’t fit my need (lots of internal storage), so I moved
to Hackintosh as it was way cheaper, and I thought upgradable. Since then, the
only upgrade I did was upgrading the GPU (the new OS didn’t support my old GTX
760 graphic card) and adding more SSD disks.</p>
<p>All those steps I thought were transitional, not meaning to last long. They all
lasted way longer than expected, and I feel the new M1 mini might last longer
than expected as well. Looking at <a href="https://browser.geekbench.com/user/347515">my
geekbenchs</a> you’ll see my Hackintosh
(listed as iMac14,2) was doing <em>3719</em> Multi-core. My new 2020 MBP16” is already
twice faster, but the M1 mini <a href="https://browser.geekbench.com/macs/mac-mini-late-2020">beats them
all</a>.</p>
<p>However the 2008 MacPro I bought was a great machine, still used to its maximum
by a friend I sold it to a decade later. So I’m very much looking forward to
the next iMac and MacPro with the new Apple chip.</p>
<p>So long, my dear Hackintosh.</p>