1
+ import crypto from "crypto"
2
+ import url from "url"
3
+
1
4
import {
2
5
generateFileUrl ,
3
6
generateImageUrl ,
@@ -7,6 +10,122 @@ import {
7
10
type ImageArgs = Parameters < typeof generateImageUrl > [ 1 ]
8
11
9
12
describe ( `url-generator` , ( ) => {
13
+ describe ( `URL encryption` , ( ) => {
14
+ function decryptImageCdnUrl (
15
+ key : string ,
16
+ iv : string ,
17
+ encryptedUrl : string
18
+ ) : { decryptedUrl : string ; randomPadding : string } {
19
+ const decipher = crypto . createDecipheriv (
20
+ `aes-256-ctr` ,
21
+ Buffer . from ( key , `hex` ) ,
22
+ Buffer . from ( iv , `hex` )
23
+ )
24
+ const decrypted = decipher . update ( Buffer . from ( encryptedUrl , `hex` ) )
25
+ const clearText = Buffer . concat ( [ decrypted , decipher . final ( ) ] ) . toString ( )
26
+
27
+ const [ randomPadding , ...url ] = clearText . split ( `:` )
28
+
29
+ return { decryptedUrl : url . join ( `:` ) , randomPadding }
30
+ }
31
+
32
+ const fileUrlToEncrypt = `https://example.com/file.pdf`
33
+ const imageUrlToEncrypt = `https://example.com/image.png`
34
+
35
+ const imageNode = {
36
+ url : imageUrlToEncrypt ,
37
+ mimeType : `image/png` ,
38
+ filename : `image.png` ,
39
+ internal : {
40
+ contentDigest : `digest` ,
41
+ } ,
42
+ }
43
+
44
+ const resizeArgs = {
45
+ width : 100 ,
46
+ height : 100 ,
47
+ format : `webp` ,
48
+ quality : 80 ,
49
+ }
50
+
51
+ const generateEncryptedUrlForType = ( type : string ) : string => {
52
+ const url = {
53
+ file : generateFileUrl ( {
54
+ url : fileUrlToEncrypt ,
55
+ filename : `file.pdf` ,
56
+ } ) ,
57
+ image : generateImageUrl ( imageNode , resizeArgs ) ,
58
+ } [ type ]
59
+
60
+ if ( ! url ) {
61
+ throw new Error ( `Unknown type: ${ type } ` )
62
+ }
63
+
64
+ return url
65
+ }
66
+
67
+ const getUnencryptedUrlForType = ( type : string ) : string => {
68
+ if ( type === `file` ) {
69
+ return fileUrlToEncrypt
70
+ } else if ( type === `image` ) {
71
+ return imageUrlToEncrypt
72
+ } else {
73
+ throw new Error ( `Unknown type: ${ type } ` )
74
+ }
75
+ }
76
+
77
+ it . each ( [ `file` , `image` ] ) (
78
+ `should return %s URL's untouched if encryption is not enabled` ,
79
+ type => {
80
+ const unencryptedUrl = generateEncryptedUrlForType ( type )
81
+
82
+ const { eu, u } = url . parse ( unencryptedUrl , true ) . query
83
+
84
+ expect ( eu ) . toBe ( undefined )
85
+ expect ( u ) . toBeTruthy ( )
86
+
87
+ expect ( u ) . toBe ( getUnencryptedUrlForType ( type ) )
88
+ }
89
+ )
90
+
91
+ it . each ( [ `file` , `image` ] ) (
92
+ `should return %s URL's encrypted if encryption is enabled` ,
93
+ type => {
94
+ const key = crypto . randomBytes ( 32 ) . toString ( `hex` )
95
+ const iv = crypto . randomBytes ( 16 ) . toString ( `hex` )
96
+
97
+ process . env . IMAGE_CDN_ENCRYPTION_SECRET_KEY = key
98
+ process . env . IMAGE_CDN_ENCRYPTION_IV = iv
99
+
100
+ const urlWithEncryptedEuParam = generateEncryptedUrlForType ( type )
101
+
102
+ expect ( urlWithEncryptedEuParam ) . not . toContain (
103
+ encodeURIComponent ( getUnencryptedUrlForType ( type ) )
104
+ )
105
+
106
+ const { eu : encryptedUrlParam , u : urlParam } = url . parse (
107
+ urlWithEncryptedEuParam ,
108
+ true
109
+ ) . query
110
+
111
+ expect ( urlParam ) . toBeFalsy ( )
112
+ expect ( encryptedUrlParam ) . toBeTruthy ( )
113
+
114
+ const { decryptedUrl, randomPadding } = decryptImageCdnUrl (
115
+ key ,
116
+ iv ,
117
+ encryptedUrlParam as string
118
+ )
119
+
120
+ expect ( decryptedUrl ) . toEqual ( getUnencryptedUrlForType ( type ) )
121
+ expect ( randomPadding . length ) . toBeGreaterThan ( 0 )
122
+
123
+ delete process . env . IMAGE_CDN_ENCRYPTION_SECRET_KEY
124
+ delete process . env . IMAGE_CDN_ENCRYPTION_IV
125
+ }
126
+ )
127
+ } )
128
+
10
129
describe ( `generateFileUrl` , ( ) => {
11
130
it ( `should return a file based url` , ( ) => {
12
131
const source = {
0 commit comments