Compare SHA256 on clipboard to Shell Script result

I have been having to compare quite a few SHA256 Checksums lately.

Anyone have an idea on how I might approach this?

Usually I copy the checksum from a web page. Download the file. Then run:
openssl dgst -sha256 /path/to/the/file/I/want/to/check
in the terminal.

Then I get an output something like this:
SHA256(/Users/myHome/Downloads/somefile-1.6.0.5.pkg)= 9d310aa6049c4f672c826e423dc5134f82773a3fb0def98ee579576547acedd2

Then I add spaces until my cursor is right under the checksum and do a paste of the copied checksum to visually check it character by character. There’s got to be an easier way to do this.

1 Like

Hey John,

I use HashTab.

http://implbits.com/products/hashtab/

Drop your file on it, and it will produce the relevant hashes.

Paste your reference hash into the provided field, and it will compare for you.

Then again this sort of thing is not too hard to make into a macro:

Compare sha256 hash of selected file and against value in the clipboard.kmmacros (4.0 KB)

-Chris

2 Likes

Hey Chris… Thanks for doing that. I wasn’t expecting you to write one but it works great so solution in place. The program you sent the link to looks great. Unfortunately I don’t use Windows at all so I can’t use it. Yet again you have gone above and beyond.

Is HashTab available for OS X / macOS ?

(looks like a Windows application)

Eh?

Grrff. I posted the URL from the app itself...

You can find it on the App-Store (freeware):

-Chris

A hoop for a slightly more general KM macro to jump through is the range of SHA lengths still in use.

( The PureScript downloads, for example, use SHA1 at the moment )

A rule of thumb for choosing which switch to pass to openssl might be based on the length of the string in the clipboard. (number of bits / 4 ?)

64 for SHA-256
40 for SHA1
etc.

The full range of SHA switches is:

2 Likes

Hey Folks,

Interestingly another freebie from the App-Store crossed my desk today:

It has some rough-edges such as a non-resizable window, but I rather like the UI.

I’ve just asked the developer to polish a few things, and we’ll see whether he’s responsive or not.

-Chris

Well then. I stand corrected. :slight_smile:

I won't distribute this as a packaged macro, and anyone should test it carefully before using it, but you could experiment with dropping something along these lines into an Execute JavaScript for Automation action.

(on my system I am splitting out the the boolean result from the report string, and playing a sound that depends on the result, as well as displaying the report)

// Copyright (c) 2016 Rob Trew
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

(function () {
    'use strict';

    // show :: a -> String
    var show = function (x) {
        return JSON.stringify(x, null, 2);
    };

    // clipSHA :: () -> Maybe String
    var clipSHA = function () {
        var a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a),
            mbClip = sa.theClipboard();

        if (mbClip && typeof mbClip === 'string') {
            var strClip = mbClip.trim();
            return strClip.match(/^[0-9a-fA-F]{40,}$/) ? strClip : undefined;
        } else return undefined;
    };

    // 6 SHA strings (SHA, SHA1 ... SHA512) for file at path
    // fileSHAs :: pathString -> [{shaType:String, sha:String, charCount:Int}]
    var fileSHAs = function (strPath) {
        var a = Application.currentApplication(),
            sa = (a.includeStandardAdditions = true, a),
            dctPath = pathExistsAndisFolder(strPath),
            pathIsFile = dctPath.exists && !dctPath.isFolder;

        return (pathIsFile ? sa.doShellScript([0, 1, 224, 256, 384, 512]
                .map(function (x) {
                    return 'openssl dgst -sha' +
                        (x ? x.toString() : '') + ' "' + strPath + '"';
                })
                .join('\n')) : [])
            .split(/[\n\r]+/)
            .reduce(function (a, s) {
                if (s.indexOf(')= ') !== -1) {
                    var pair = s.split(')= '),
                        strSHA = pair[1];
                    return a.concat({
                        shaType: pair[0].split('(')[0],
                        sha: strSHA,
                        charCount: strSHA.length
                    });
                } else return a;
            }, []);
    };

    // fileSHAsOfClipLength :: pathString -> shaString -> [{type:a, sha:b}]
    var fileSHAsOfClipLength = function (strClipSHA, strPath) {
        var lngClipSHA = strClipSHA.length;

        return fileSHAs(strPath)
            .filter(function (dct) {
                return dct.charCount === lngClipSHA;
            });
    };

    // diffIndices :: String -> String -> [Int]
    var diffIndices = function (sa, sb) {
        var as = sa.split(''),
            bs = sb.split(''),
            lngA = sa.length,
            lngB = sb.length;

        var xs = zipWith(function (a, b) {
                return a === b;
            }, as, bs),
            deltas = all(function (x) {
                return x;
            }, xs) ? [] : xs.reduce(function (a, x, i) {
                return x ? a : a.concat(i);
            }, []);

        return deltas.concat(
            lngA !== lngB ? range(min(lngA, lngB), max(lngA, lngB)) : []
        );
    };

    // showDiff :: String -> String -> String
    var showDiff = function (s1, s2) {
        var ds = diffIndices(s1, s2);
        var blnOK = ds.length === 0;

        return {
            shaOK: blnOK,
            display: !blnOK ? ['', s1, s2, replicateS(
                    max(s1.length, s2.length), ' '
                )
                .split('')
                .map(function (x, i) {
                    return elem(i, ds) ? '^' : x;
                })
                .join('')
            ].join('\n') : s1
        };
    };

    // shaLengths :: Dictionary
    var shaLengths = {
        SHA: 40,
        SHA1: 40,
        SHA224: 56,
        SHA256: 64,
        SHA384: 96,
        SHA512: 128
    };

    // shaTypeLength :: String -> Int
    var shaTypeLength = function (shaName) {
        return shaLengths[shaName];
    };

    // shaLengthTypes :: Int -> [String]
    var shaLengthTypes = function (intChars) {
        return Object.keys(shaLengths)
            .filter(function (k) {
                return shaLengths[k] === intChars;
            });
    };

    // shaReport :: shaString -> pathString -> {matched:Bool, report:String}
    var shaReport = function (strClipSHA, strPath) {
        var shaSet = fileSHAsOfClipLength(strClipSHA, strPath),
            shaDiffs = shaSet.map(function (x) {
                return showDiff(strClipSHA, x.sha);
            }),
            mbIntMatch = findIndex(function (dct) {
                return dct.shaOK === true;
            }, shaDiffs),
            lngClipChars = strClipSHA.length,
            blnMatched = mbIntMatch !== undefined;

        return {
            matched: blnMatched,
            report: blnMatched ? [strClipSHA, shaDiffs[mbIntMatch].display]
                .join('\n') : 'FAILURE: OpenSSL SHA of file:\n\n\t' +
                strPath + '\n\ndoes NOT match SHA in clipboard.\n\n' +
                shaDiffs.map(function (dct, i) {
                    return 'Clipboard expectation, then ' + shaSet[i].shaType +
                        ' found:' + dct.display + '\n';
                })
                .join('\n'),
            shaType: blnMatched ?
                shaSet[mbIntMatch]
                .shaType : 'Possible SHA type(s) for clipboard of ' +
                'this length:' + (' (' + lngClipChars + ' chars) ') +
                show(shaLengthTypes(lngClipChars))
        };
    };

    // GENERIC FUNCTIONS -----------------------------------------------------

    // pathExistsAndisFolder :: String -> (Bool, Int)
    var pathExistsAndisFolder = function (strPath) {
        var ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory($(strPath)
                .stringByStandardizingPath, ref) ? {
                'exists': true,
                'isFolder': ref[0] === 1
            } : {
                'exists': false
            };
    };

    // selectedPaths :: () -> [pathString]
    var selectedPaths = function () {
        return Application('Finder')
            .selection()
            .map(function (x) {
                return decodeURI(x.url())
                    .slice(7);
            });
    };

    // findIndex :: (a -> Bool) -> [a] -> Maybe Int
    var findIndex = function (f, xs) {
        for (var i = 0, lng = xs.length; i < lng; i++) {
            if (f(xs[i])) return i;
        }
        return undefined;
    };

    // all :: (a -> Bool) -> [a] -> Bool
    var all = function (f, xs) {
        return xs.every(f);
    };

    // elem :: Eq a => a -> [a] -> Bool
    var elem = function (x, xs) {
        return xs.indexOf(x) !== -1;
    };

    // max :: Ord a => a -> a -> a
    var max = function (a, b) {
        return b > a ? b : a;
    };

    // min :: Ord a => a -> a -> a
    var min = function (a, b) {
        return b < a ? b : a;
    };

    // range :: Int -> Int -> [Int]
    var range = function (m, n) {
        return Array.from({
            length: Math.floor(n - m) + 1
        }, function (_, i) {
            return m + i;
        });
    };

    // replicateS :: Int -> String -> String
    var replicateS = function (n, s) {
        var v = s,
            o = '';
        if (n < 1) return o;
        while (n > 1) {
            if (n & 1) o = o.concat(v);
            n >>= 1;
            v = v.concat(v);
        }
        return o.concat(v);
    };

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    var zipWith = function (f, xs, ys) {
        var ny = ys.length;
        return (xs.length <= ny ? xs : xs.slice(0, ny))
            .map(function (x, i) {
                return f(x, ys[i]);
            });
    };

    // -----------------------------------------------------------------------

    // MAIN (SHA in clipboard vs SHA of selected file)

    // Clipboard -> Finder selection -> Report

    // 1. SHA in clipboard ?
    var strClipSHA = clipSHA();

    if (strClipSHA === undefined) {
        return show({
            result: false,
            report: 'No SHA in clipboard ?\n\n' +
                '(Expected hexadecimal string of 40-128 characters)'
        });
    }

    // 2. File selected ?
    var selns = selectedPaths(),
        strPath = selns.length > 0 ? selns[0] : undefined;

    if (strPath === undefined) return 'No file selected in Finder ?';
    if (pathExistsAndisFolder(strPath)
        .isFolder) {
        return show({
            result: false,
            report: 'Selection: \n\n\t' + strPath +
                '\n\nis a folder – please select a file to check SHA ...\n'
        });
    }

    // 3. openSSL cmd yields any SHA for this file which matches clipboard ?
    var dctReport = shaReport(strClipSHA, strPath);

    return show({
        result: dctReport.matched,
        report: dctReport.matched ? 'OK for ' + dctReport.shaType +
            '\n\nClipboard matches ' + dctReport.shaType +
            ' returned by the openssl command for :\n\n\t' +
            strPath + ' \n\n' +
            dctReport.report : [dctReport.report, dctReport.shaType].join('\n')
    });
})();

// Testing 40 char SHA | SHA1 (should also work with other SHA sizes)
// aa508b4ee87c8dbf7dc48fa386591c73efd747c6
// 3d63e562d8f0be2e8727efa5ade5c507c395bbda
// 094742fa10af057d610d88f9ea1602e846c56d19

1 Like

Rob what action did you use for “Split Result into two variables”? I can’t seem to locate an action that matches. Also “Report on problem or match” and “Play a sound that depends on result”, - can’t find those either.

Looks really great though.

"Split Result into two variables" is a (renamed) Execute JavaScript for Automation action:

containing just this code (which assumes that the output of the previous script action goes into a SHAResult variable)

(function () {
    'use strict';

    var kme = Application('Keyboard Maestro Engine'),
           dctSHAResult = JSON.parse((kme.getvariable('SHAResult')));
        
       kme.setvariable('SHAMatched', {to:dctSHAResult.result});
       kme.setvariable('SHAReport', {to:dctSHAResult.report});
})();

"Play a sound that depends on result" is a (renamed) If Then Else action

Thanks. So is “report a problem or match” the “Display Text” action?

is "report a problem or match" the "Display Text" action?

That's right: