<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: sellerkit</title>
    <description>The latest articles on DEV Community by sellerkit (@sellerkit).</description>
    <link>https://dev.to/sellerkit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4083399%2F25754528-ad7f-4335-a336-8f81de4f4de7.jpeg</url>
      <title>DEV Community: sellerkit</title>
      <link>https://dev.to/sellerkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sellerkit"/>
    <language>en</language>
    <item>
      <title>I skipped the Azure SDK to read blobs from an IDE plugin. Here's the signing code.</title>
      <dc:creator>sellerkit</dc:creator>
      <pubDate>Tue, 18 Aug 2026 14:07:57 +0000</pubDate>
      <link>https://dev.to/sellerkit/i-skipped-the-azure-sdk-to-read-blobs-from-an-ide-plugin-heres-the-signing-code-1kg2</link>
      <guid>https://dev.to/sellerkit/i-skipped-the-azure-sdk-to-read-blobs-from-an-ide-plugin-heres-the-signing-code-1kg2</guid>
      <description>&lt;p&gt;I wanted to browse Azure Blob Storage from inside my IDE without alt-tabbing to a desktop app,&lt;br&gt;
so I started writing a JetBrains plugin. First instinct was to grab &lt;code&gt;azure-storage-blob&lt;/code&gt; and be&lt;br&gt;
done in an afternoon.&lt;/p&gt;

&lt;p&gt;Then I looked at what comes with it. Netty. Reactor. Jackson. In a service I wouldn't care —&lt;br&gt;
it's my process. A plugin isn't my process. Everything I ship gets loaded next to whatever the&lt;br&gt;
IDE and every other plugin already loaded, and I get to re-check all of it every time the IDE&lt;br&gt;
updates.&lt;/p&gt;

&lt;p&gt;And for what I actually needed — list containers, walk a prefix, read a file — it's like four&lt;br&gt;
REST calls. So I wrote the auth myself. That part took a day longer than it should have, which&lt;br&gt;
is really what this post is about.&lt;/p&gt;
&lt;h2&gt;
  
  
  The signature
&lt;/h2&gt;

&lt;p&gt;Blob storage wants an HMAC-SHA256 over a canonical string. If that string is off by a single&lt;br&gt;
newline you get a 403 with no hint about which part you got wrong. Ask me how I know.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extraHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;canonHeaders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extraHeaders&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;mapOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-ms-date"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"x-ms-version"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nc"&gt;API_VERSION&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"x-ms-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sortedBy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lowercase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;US&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"${it.key.lowercase(Locale.US)}:${it.value}\n"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;canonResource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildString&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emulator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// yes, twice. more on this below&lt;/span&gt;
        &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sortedBy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stringToSign&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildString&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonHeaders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonResource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;mac&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Mac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HmacSHA256"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SecretKeySpec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"HmacSHA256"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doFinal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stringToSign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toByteArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UTF_8&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;repeat(11)&lt;/code&gt; looks like nonsense until you know what it is. Those are Content-Encoding,&lt;br&gt;
Content-Language, Content-Length, Content-MD5, Content-Type, Date, If-Modified-Since, If-Match,&lt;br&gt;
If-None-Match, If-Unmodified-Since, Range. A read request fills in none of them, but the empty&lt;br&gt;
lines still have to be there. I originally wrote 10 and spent a while convinced my key was wrong.&lt;/p&gt;

&lt;p&gt;The other one that got me: the query values in the canonical resource are the &lt;strong&gt;decoded&lt;/strong&gt; ones,&lt;br&gt;
sorted by key, even though the URL you send has them encoded.&lt;/p&gt;
&lt;h2&gt;
  
  
  The emulator doubles your account name
&lt;/h2&gt;

&lt;p&gt;This is the one I'd have paid money to know up front.&lt;/p&gt;

&lt;p&gt;Real Azure puts the account in the hostname, so the resource path you sign is&lt;br&gt;
&lt;code&gt;/myaccount/container/blob&lt;/code&gt;. Azurite puts the account in the path instead, and the string you&lt;br&gt;
sign becomes &lt;code&gt;/myaccount/myaccount/container/blob&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;if&lt;/code&gt;. But before I found it, every single request against the emulator came back 403, and a&lt;br&gt;
403 from Blob storage doesn't tell you whether it's the key, the clock, or the string.&lt;/p&gt;
&lt;h2&gt;
  
  
  Listing is XML and you want it one level deep
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /{container}?restype=container&amp;amp;comp=list&amp;amp;delimiter=/
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;BlobPrefix&lt;/code&gt; elements are your folders, &lt;code&gt;Blob&lt;/code&gt; elements are the files at that level. There are no&lt;br&gt;
real directories in blob storage — the delimiter is the only reason &lt;code&gt;logs/2026-08-18/app.json&lt;/code&gt;&lt;br&gt;
looks like a tree at all.&lt;/p&gt;

&lt;p&gt;Keep the delimiter. If you drop it to "just get everything", a container with a few million blobs&lt;br&gt;
under one prefix will page at you until you give up.&lt;/p&gt;

&lt;p&gt;I parse it with the JDK's DocumentBuilder, with entities turned off, because this is a response&lt;br&gt;
from the network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DocumentBuilderFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newInstance&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"http://apache.org/xml/features/disallow-doctype-decl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;isXIncludeAware&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;isExpandEntityReferences&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  You can build the whole thing without an Azure account
&lt;/h2&gt;

&lt;p&gt;Azurite is Microsoft's own local emulator for Blob, Queue and Table. No subscription, no card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;azurite
npx azurite-blob &lt;span class="nt"&gt;--location&lt;/span&gt; ./azurite-data &lt;span class="nt"&gt;--blobPort&lt;/span&gt; 10000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;devstoreaccount1&lt;/code&gt; and the development key that's printed in the Azurite docs — it's public,&lt;br&gt;
it's not a secret. Same protocol as the real service, so whatever you get working here works&lt;br&gt;
there. My entire test suite runs against it.&lt;/p&gt;

&lt;p&gt;Two things that wasted my time: the npm package is &lt;code&gt;azurite&lt;/code&gt;, not &lt;code&gt;azurite-blob&lt;/code&gt; (that binary&lt;br&gt;
shows up after you install), and container names have to be at least 3 characters. I named a&lt;br&gt;
test container &lt;code&gt;qa&lt;/code&gt; and got back &lt;code&gt;OutOfRangeInput&lt;/code&gt; with a 400, which is a very unhelpful way of&lt;br&gt;
saying "your name is too short".&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it
&lt;/h2&gt;

&lt;p&gt;The plugin ships with zero third-party dependencies. One jar, 66 KB. Nothing to collide with,&lt;br&gt;
nothing extra loading when the IDE starts, no dependency bumps to chase.&lt;/p&gt;

&lt;p&gt;I wouldn't do this for everything though. This is a read path. If you're uploading at scale, or&lt;br&gt;
you need Entra ID instead of an account key, or you want lifecycle management, just use the SDK —&lt;br&gt;
you'd be reimplementing a lot for no reason.&lt;/p&gt;

&lt;p&gt;One more thing since it's a credential: an account key is full access to that storage account.&lt;br&gt;
In a plugin that means the IDE's password safe, not a settings file in the project that someone&lt;br&gt;
commits by accident. I've seen that go badly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build JetBrains plugins as sellerkit — this came out of writing&lt;br&gt;
&lt;a href="https://plugins.jetbrains.com/plugin/33624-azure-blob-browser" rel="noopener noreferrer"&gt;Azure Blob Browser&lt;/a&gt;. None of the&lt;br&gt;
above needs it, it's just where the bruises came from.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>kotlin</category>
      <category>java</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
