Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
silver-xu committed May 13, 2020
1 parent 6d70778 commit e1de2ba
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,5 @@ dist

# TernJS port file
.tern-port

.vscode
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: ruby

services:
- docker

before_install:
- docker build -t silver-xu/deno-aws-sign-v4 .

script:
- docker run silver-xu/deno-aws-sign-v4
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

FROM 'aminnairi/deno'
COPY . app
CMD deno bundle ./app/src/mod.ts
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,43 @@
# deno-aws-signv4
# deno-aws-signer-v4 [![Build Status](https://travis-ci.org/silver-xu/deno-aws-sign-v4.svg?branch=master)](https://travis-ci.org/silver-xu/deno-aws-sign-v4)

> Generates AWS Signature V4 for AWS low-level REST APIs.
## Region & Credentials

The below example will generate signed headers based on the region and credentials in following ENV variables:

- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
- AWS_REGION

```javascript
const signer = new AWSSignerV4();
const headers = signer.sign("es", endpoint, "POST", payload);

const response = await fetch(endpoint, {
headers
method,
body: JSON.stringify(payload),
});

```

## Complete setup

```javascript
const signer = new AWSSignerV4(
'ap-southeast-2', // Your real region
{
awsAccessKeyId: 1234 // Your real access key id
awsSecretKey: 1234 // Your real secret key
});

const headers = signer.sign("es", endpoint, "POST", payload);

const response = await fetch(endpoint, {
headers
method,
body: JSON.stringify(payload),
});

```
10 changes: 10 additions & 0 deletions src/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const ANY_BUT_DIGITS: RegExp = /[^\d]/g;
const ANY_BUT_DIGITS_T: RegExp = /[^\dT]/g;

export const toAmz = (date: Date): string => {
return `${date.toISOString().slice(0, 19).replace(ANY_BUT_DIGITS_T, "")}Z`;
};

export const toDateStamp = (date: Date): string => {
return date.toISOString().slice(0, 10).replace(ANY_BUT_DIGITS, "");
};
3 changes: 3 additions & 0 deletions src/deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { encode } from "https://denopkg.com/chiefbiiko/[email protected]/mod.ts";
export { hmac } from "https://denopkg.com/chiefbiiko/[email protected]/mod.ts";
export { sha256 } from "https://denopkg.com/chiefbiiko/[email protected]/mod.ts";
103 changes: 103 additions & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const method = "POST";
import { sha256 } from "./deps.ts";
import { toAmz, toDateStamp } from "./date.ts";
import { getSignatureKey, signAwsV4 } from "./signing.ts";
import { Credentials } from "./types.ts";

const endpoint =
"https://search-jurassi-elasti-637ekxnqkku7-ck3h4rezux6u2m6gfwfrb2ipfm.ap-southeast-2.es.amazonaws.com/repo_index/repo/_search";

const payload = {
query: {
multi_match: {
query: "comprihensiv guide",
fields: ["title", "summary"],
fuzziness: "AUTO",
},
},
_source: ["title", "summary", "publish_date"],
size: 1,
};

export class AWSSignerV4 {
private region: string;
private credentials: Credentials;

constructor(region?: string, credentials?: Credentials) {
this.region = region || this.getDefaultRegion();
this.credentials = credentials || this.getDefaultCredentials();
}

public sign = (
service: string,
url: string,
method: string = "GET",
body: any = undefined
) => {
const date = new Date();
const amzdate = toAmz(date);
const datestamp = toDateStamp(date);

const urlObj = new URL(url);
const { host, pathname, searchParams } = urlObj;
const canonicalQuerystring = searchParams.toString();

const canonicalHeaders = `host:${host}\nx-amz-date:${amzdate}\n`;
const signedHeaders = "host;x-amz-date";

const payload = body ? JSON.stringify(body) : "";
const payloadHash = sha256(payload, "utf8", "hex") as string;

const { awsAccessKeyId, awsSecretKey } = this.credentials;

const canonicalRequest = `${method}\n${pathname}\n${canonicalQuerystring}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
const canonicalRequestDigest = sha256(
canonicalRequest,
"utf8",
"hex"
) as string;

const algorithm = "AWS4-HMAC-SHA256";
const credentialScope = `${datestamp}/${this.region}/${service}/aws4_request`;
const stringToSign = `${algorithm}\n${amzdate}\n${credentialScope}\n${canonicalRequestDigest}`;

const signingKey = getSignatureKey(
awsSecretKey,
datestamp,
this.region,
service
);

const signature = signAwsV4(signingKey, stringToSign);

const authHeader = `${algorithm} Credential=${awsAccessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;

const headers = {
"x-amz-date": amzdate,
Authorization: authHeader,
};

return headers;
};

private getDefaultCredentials = () => {
const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = Deno.env.toObject();
if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) {
throw new Error("Invalid Credentials");
}

return {
awsAccessKeyId: AWS_ACCESS_KEY_ID,
awsSecretKey: AWS_SECRET_ACCESS_KEY,
};
};

private getDefaultRegion = () => {
const { AWS_REGION } = Deno.env.toObject();
if (!AWS_REGION) {
throw new Error("Invalid Region");
}

return AWS_REGION;
};
}
38 changes: 38 additions & 0 deletions src/signing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { hmac, encode } from "./deps.ts";
const AWS4: Uint8Array = encode("AWS4", "utf8");

export const signAwsV4 = (
key: string | Uint8Array,
msg: string
): string | Uint8Array => {
return hmac("sha256", key, msg, undefined, "hex");
};

export const getSignatureKey = (
key: string | Uint8Array,
dateStamp: string,
region: string,
service: string
) => {
if (typeof key === "string") {
key = encode(key, "utf8") as Uint8Array;
}

const paddedKey: Uint8Array = new Uint8Array(4 + key.byteLength);

paddedKey.set(AWS4, 0);
paddedKey.set(key, 4);

let mac: Uint8Array = hmac(
"sha256",
paddedKey,
dateStamp as string,
"utf8"
) as Uint8Array;

mac = hmac("sha256", mac, region, "utf8") as Uint8Array;
mac = hmac("sha256", mac, service, "utf8") as Uint8Array;
mac = hmac("sha256", mac, "aws4_request", "utf8") as Uint8Array;

return mac;
};
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Credentials {
awsAccessKeyId: string;
awsSecretKey: string;
}

export type Method = "GET" | "PUT" | "POST" | "DELETE";

0 comments on commit e1de2ba

Please sign in to comment.