diff --git a/.env.sample b/.env.sample index 65065fa2..86e32dd9 100644 --- a/.env.sample +++ b/.env.sample @@ -37,6 +37,7 @@ GITHUB_ACCESS_TOKEN= # AWS credentials (optional in dev) AWS_ACCESS_KEY_ID=notcheckedindev AWS_SECRET_ACCESS_KEY=notcheckedindev +AWS_REGION=ap-xxxxx-1 AWS_S3_UPLOAD_BUCKET_URL=http://s3:4569 AWS_S3_UPLOAD_BUCKET_NAME=outline-dev AWS_S3_UPLOAD_MAX_SIZE=26214400 diff --git a/package.json b/package.json index 4c460a0f..7c4908e1 100644 --- a/package.json +++ b/package.json @@ -188,4 +188,4 @@ "rimraf": "^2.5.4" }, "version": "0.17.0" -} +} \ No newline at end of file diff --git a/server/api/users.js b/server/api/users.js index d3dd3212..8dc8f235 100644 --- a/server/api/users.js +++ b/server/api/users.js @@ -1,7 +1,13 @@ // @flow import uuid from 'uuid'; import Router from 'koa-router'; -import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3'; +import format from 'date-fns/format'; +import { + makePolicy, + getSignature, + publicS3Endpoint, + makeCredential, +} from '../utils/s3'; import { ValidationError } from '../errors'; import { Event, User, Team } from '../models'; import auth from '../middlewares/authentication'; @@ -63,7 +69,9 @@ router.post('users.s3Upload', auth(), async ctx => { const s3Key = uuid.v4(); const key = `uploads/${ctx.state.user.id}/${s3Key}/${filename}`; - const policy = makePolicy(); + const credential = makeCredential(); + const longDate = format(new Date(), 'YYYYMMDDTHHmmss\\Z'); + const policy = makePolicy(credential, longDate); const endpoint = publicS3Endpoint(); const url = `${endpoint}/${key}`; @@ -84,13 +92,15 @@ router.post('users.s3Upload', auth(), async ctx => { maxUploadSize: process.env.AWS_S3_UPLOAD_MAX_SIZE, uploadUrl: endpoint, form: { - AWSAccessKeyId: process.env.AWS_ACCESS_KEY_ID, 'Cache-Control': 'max-age=31557600', 'Content-Type': kind, - key, acl: 'public-read', - signature: signPolicy(policy), + key, policy, + 'x-amz-algorithm': 'AWS4-HMAC-SHA256', + 'x-amz-credential': credential, + 'x-amz-date': longDate, + 'x-amz-signature': getSignature(policy), }, asset: { contentType: kind, diff --git a/server/utils/s3.js b/server/utils/s3.js index 1e423390..77894272 100644 --- a/server/utils/s3.js +++ b/server/utils/s3.js @@ -8,9 +8,29 @@ import fetch from 'isomorphic-fetch'; import bugsnag from 'bugsnag'; const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; +const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; +const AWS_REGION = process.env.AWS_REGION; const AWS_S3_UPLOAD_BUCKET_NAME = process.env.AWS_S3_UPLOAD_BUCKET_NAME; -export const makePolicy = () => { +const hmac = (key: string, message: string, encoding: any) => { + return crypto + .createHmac('sha256', key) + .update(message, 'utf8') + .digest(encoding); +}; + +export const makeCredential = () => { + const credential = + AWS_ACCESS_KEY_ID + + '/' + + format(new Date(), 'YYYYMMDD') + + '/' + + AWS_REGION + + '/s3/aws4_request'; + return credential; +}; + +export const makePolicy = (credential: string, longDate: string) => { const tomorrow = addHours(new Date(), 24); const policy = { conditions: [ @@ -20,6 +40,9 @@ export const makePolicy = () => { ['content-length-range', 0, +process.env.AWS_S3_UPLOAD_MAX_SIZE], ['starts-with', '$Content-Type', 'image'], ['starts-with', '$Cache-Control', ''], + { 'x-amz-algorithm': 'AWS4-HMAC-SHA256' }, + { 'x-amz-credential': credential }, + { 'x-amz-date': longDate }, ], expiration: format(tomorrow, 'YYYY-MM-DDTHH:mm:ss\\Z'), }; @@ -27,13 +50,16 @@ export const makePolicy = () => { return new Buffer(JSON.stringify(policy)).toString('base64'); }; -export const signPolicy = (policy: any) => { - invariant(AWS_SECRET_ACCESS_KEY, 'AWS_SECRET_ACCESS_KEY not set'); - const signature = crypto - .createHmac('sha1', AWS_SECRET_ACCESS_KEY) - .update(policy) - .digest('base64'); +export const getSignature = (policy: any) => { + const kDate = hmac( + 'AWS4' + AWS_SECRET_ACCESS_KEY, + format(new Date(), 'YYYYMMDD') + ); + const kRegion = hmac(kDate, AWS_REGION); + const kService = hmac(kRegion, 's3'); + const kCredentials = hmac(kService, 'aws4_request'); + const signature = hmac(kCredentials, policy, 'hex'); return signature; };