Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/source/filters/hmac_sha256.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: hmac_sha256
---

{% since %}vNEXT{% endsince %}

Converts a string into an SHA-256 hash using a hash message authentication code (HMAC). The secret key is passed as the filter argument. The output is a lowercase hexadecimal string.

Input

```liquid
{%- assign secret_potion = 'Polyjuice' | hmac_sha256: 'Polina' -%}
My secret potion: {{ secret_potion }}
```

Output

```text
My secret potion: 8e0d5d65cff1242a4af66c8f4a32854fd5fb80edcc8aabe9b302b29c7c71dc20
```
1 change: 1 addition & 0 deletions docs/source/filters/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ Array | slice, map, sort, sort_natural, uniq, where, where_exp, group_by, group_
Date | date, date_to_xmlschema, date_to_rfc822, date_to_string, date_to_long_string
Misc | default, json, jsonify, inspect, raw, to_integer
Base64 | base64_encode, base64_decode
Crypto | sha256, hmac_sha256

[shopify/liquid]: https://github.com/Shopify/liquid
20 changes: 20 additions & 0 deletions docs/source/filters/sha256.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: sha256
---

{% since %}vNEXT{% endsince %}

Converts a string into an SHA-256 hash. The output is a lowercase hexadecimal string.

Input

```liquid
{%- assign secret_potion = 'Polyjuice' | sha256 -%}
My secret potion: {{ secret_potion }}
```

Output

```text
My secret potion: 44ac1d7a2936e30a5de07082fd65d6fe9b1fb658a1a98bfe65bc5959beac5dd0
```
12 changes: 10 additions & 2 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ const browserBase64 = {
delimiters: ['', ''],
'./base64-impl': '../build/base64-impl-browser'
}
const browserCrypto = {
include: './src/filters/crypto.ts',
delimiters: ['', ''],
'./crypto-impl': '../build/crypto-impl-browser'
}
const browserStream = {
include: './src/emitters/index.ts',
delimiters: ['', ''],
Expand All @@ -67,7 +72,7 @@ const nodeCjs = {
format: 'cjs',
banner
}],
external: ['path', 'fs', 'stream'],
external: ['path', 'fs', 'stream', 'crypto'],
plugins: [versionInjection, typescript(tsconfig('ES2020'))],
treeshake,
input
Expand All @@ -79,7 +84,7 @@ const nodeEsm = {
format: 'esm',
banner
}],
external: ['path', 'fs', 'stream'],
external: ['path', 'fs', 'stream', 'crypto'],
plugins: [
versionInjection,
replace(esmRequire),
Expand All @@ -100,6 +105,7 @@ const browserEsm = {
versionInjection,
replace(browserFS),
replace(browserBase64),
replace(browserCrypto),
replace(browserStream),
typescript(tsconfig('es6'))
],
Expand All @@ -119,6 +125,7 @@ const browserUmd = {
versionInjection,
replace(browserFS),
replace(browserBase64),
replace(browserCrypto),
replace(browserStream),
typescript(tsconfig('es5'))
],
Expand All @@ -138,6 +145,7 @@ const browserMin = {
versionInjection,
replace(browserFS),
replace(browserBase64),
replace(browserCrypto),
replace(browserStream),
typescript(tsconfig('es5')),
uglify()
Expand Down
45 changes: 45 additions & 0 deletions src/build/crypto-impl-browser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { webcrypto } from 'crypto'
import * as cryptoImpl from './crypto-impl-browser'

describe('crypto-impl/browser', function () {
beforeEach(function () {
Object.defineProperty(global, 'crypto', {
value: webcrypto,
writable: true,
configurable: true
})
})

afterEach(function () {
delete (global as any).crypto
})

describe('#sha256()', function () {
it('should hash the Shopify reference example', async function () {
expect(await cryptoImpl.sha256('Polyjuice'))
.toBe('44ac1d7a2936e30a5de07082fd65d6fe9b1fb658a1a98bfe65bc5959beac5dd0')
})

it('should hash an empty string', async function () {
expect(await cryptoImpl.sha256(''))
.toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
})

it('should hash unicode characters', async function () {
expect(await cryptoImpl.sha256('你好世界'))
.toBe('beca6335b20ff57ccc47403ef4d9e0b8fccb4442b3151c2e7d50050673d43172')
})
})

describe('#hmacSha256()', function () {
it('should hash the Shopify reference example', async function () {
expect(await cryptoImpl.hmacSha256('Polyjuice', 'Polina'))
.toBe('8e0d5d65cff1242a4af66c8f4a32854fd5fb80edcc8aabe9b302b29c7c71dc20')
})

it('should hash an empty message with a key', async function () {
expect(await cryptoImpl.hmacSha256('', 'key'))
.toBe('5d5d139563c95b5967b9bd9a8c9b233a9dedb45072794cd232dc1b74832607d0')
})
})
})
27 changes: 27 additions & 0 deletions src/build/crypto-impl-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
function bufferToHex (buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let hex = ''
for (let i = 0; i < bytes.length; i++) {
hex += bytes[i].toString(16).padStart(2, '0')
}
return hex
}

export async function sha256 (str: string): Promise<string> {
const data = new TextEncoder().encode(str)
const digest = await crypto.subtle.digest('SHA-256', data)
return bufferToHex(digest)
}

export async function hmacSha256 (str: string, key: string): Promise<string> {
const encoder = new TextEncoder()
const cryptoKey = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(str))
return bufferToHex(signature)
}
9 changes: 9 additions & 0 deletions src/filters/crypto-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createHash, createHmac } from 'crypto'

export function sha256 (str: string): string {
return createHash('sha256').update(str, 'utf8').digest('hex')
}

export function hmacSha256 (str: string, key: string): string {
return createHmac('sha256', key).update(str, 'utf8').digest('hex')
}
22 changes: 22 additions & 0 deletions src/filters/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Crypto related filters
*
* Implements sha256 and hmac_sha256 filters for Shopify compatibility
*/

import { FilterImpl } from '../template'
import { stringify } from '../util'
import { sha256 as sha256Impl, hmacSha256 as hmacSha256Impl } from './crypto-impl'

export function sha256 (this: FilterImpl, value: unknown): string | Promise<string> {
const str = stringify(value)
this.context.memoryLimit.use(str.length)
return sha256Impl(str)
}

export function hmac_sha256 (this: FilterImpl, value: unknown, key: unknown): string | Promise<string> {
const str = stringify(value)
const keyStr = stringify(key)
this.context.memoryLimit.use(str.length + keyStr.length)
return hmacSha256Impl(str, keyStr)
}
2 changes: 2 additions & 0 deletions src/filters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as arrayFilters from './array'
import * as dateFilters from './date'
import * as stringFilters from './string'
import * as base64Filters from './base64'
import * as cryptoFilters from './crypto'
import misc from './misc'
import { FilterImplOptions } from '../template'

Expand All @@ -16,5 +17,6 @@ export const filters: Record<string, FilterImplOptions> = {
...dateFilters,
...stringFilters,
...base64Filters,
...cryptoFilters,
...misc
}
70 changes: 70 additions & 0 deletions test/integration/filters/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { test } from '../../stub/render'

const SHA256_EMPTY = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

describe('filters/crypto', function () {
describe('sha256', function () {
it('should hash the Shopify reference example', () => {
return test(
'{{ "Polyjuice" | sha256 }}',
'44ac1d7a2936e30a5de07082fd65d6fe9b1fb658a1a98bfe65bc5959beac5dd0'
)
})

it('should hash an empty string', () => {
return test('{{ "" | sha256 }}', SHA256_EMPTY)
})

it('should treat undefined as empty string', () => {
return test('{{ foo | sha256 }}', SHA256_EMPTY)
})

it('should treat null as empty string', () => {
return test('{{ null | sha256 }}', SHA256_EMPTY)
})

it('should stringify numeric input', () => {
return test(
'{{ 123 | sha256 }}',
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
)
})

it('should stringify boolean input', () => {
return test(
'{{ true | sha256 }}',
'b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b'
)
})
})

describe('hmac_sha256', function () {
it('should hash the Shopify reference example', () => {
return test(
"{{ 'Polyjuice' | hmac_sha256: 'Polina' }}",
'8e0d5d65cff1242a4af66c8f4a32854fd5fb80edcc8aabe9b302b29c7c71dc20'
)
})

it('should accept a numeric key (stringified)', () => {
return test(
"{{ 'hello' | hmac_sha256: 42 }}",
'3bdadea6ed0e95ededc15dc4421ce7654c970156843dfd997be3fef5358168ca'
)
})

it('should hash an empty message with an empty key', () => {
return test(
"{{ '' | hmac_sha256: '' }}",
'b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad'
)
})

it('should treat undefined input as empty string', () => {
return test(
"{{ foo | hmac_sha256: '' }}",
'b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad'
)
})
})
})
Loading