Skip to content

Commit

Permalink
feat: use kms to encrypt env
Browse files Browse the repository at this point in the history
  • Loading branch information
steve-lemon committed Jul 18, 2019
1 parent cf6d47f commit 7820c2a
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 52 deletions.
68 changes: 44 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,65 @@
# lemon-hello-api

Basic Serverless Lambda API Example
Basic Serverless Hello API with Lambda + API Gateway + KMS

# Basic Usage (기본 사용법)
## 설명 (Description)

## Quick Start
- AWS CloudWatch 의 내용을 `lemon-hello-sns`으로 수신 함 -> 이후 슬랙으로 전달
- 슬랙 webhook를 이용하여, 해당 슬랙 채널에 메세지를 보냄

- run API sever in local

```bash
$ npm install
## 사용법 (Usage)

# by use serverless-offline
$ npm run server
로컬에서 바로 실행

# call API
$ http ':8888/hello/'
```
```bash
$ npm install
$ npm run express
```

## Deploy to AWS
### 준비. KMS로 설정내용 암호화 하기

- by profile + stage, use different configuration
1. KMS 마스터 키ID 생성하기 (최초 생성)

```bash
# deploy profile lemon
$ npm run deploy
```
```bash
# 최초의 사용자 키 생성히기..
$ aws kms create-key --profile <profile> --description 'hello master key'
{
"KeyMetadata": {
"AWSAccountId": "000000000000",
"KeyId": "0039d20d-.....-387b887b4783",
"Arn": "arn:aws:kms:ap-northeast-2:000000000000:key/0039d20d-.....-387b887b4783",
"CreationDate": 0,
"Enabled": true,
"Description": "hello master key",
"KeyUsage": "ENCRYPT_DECRYPT",
"KeyState": "Enabled",
"Origin": "AWS_KMS",
"KeyManager": "CUSTOMER"
}
}
# Alias 생성하기 ('0039d20d-.....-387b887b4783'은 앞에서 생성된 KeyId 항목으로 변경)
$ aws kms create-alias --profile <profile> --alias-name alias/lemon-hello-api --target-key-id 0039d20d-.....-387b887b4783
```

1. 암호화 테스트 하기.

# Functions (기능들)
```sh
# 'hello lemon' 를 <kms-key-id>로 암호화하기...
$ aws kms encrypt --profile <profile> --key-id alias/lemon-hello-api --plaintext "hello lemon" --query CiphertextBlob --output text
## Post Message to Slack Channel (슬랙 채널에 메세지 보내기)
# 또는 서버실행후, 아래 요청으로 확인.
$ http ':8888/hello/0/test-encrypt'
```

Send "hello" text message to slack's public channel.

`$ echo '{"text":"hello"}' | http ':8888/hello/public/slack'`

- config `SLACK_PUBLIC` environment to override webhook address.

## 설치하기 (Installation)

AWS Lambda 에 배포됨.

```bash
$ npm run deploy
```


----------------
Expand Down
2 changes: 1 addition & 1 deletion SNS.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ exports = module.exports = (function (_$) {
// "image_url" : image || '',
"fields" : fields,
}
//! build body for slack.
//! build body for slack
const body = {"attachments": [attachment]};

//! call post-slack
Expand Down
2 changes: 2 additions & 0 deletions env/lemon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ default_env: &default_env
LC: 1 # line-coloring
TS: 1 # time-stamp in line
NAME: 'lemon'
SRC: './src/'
SLACK_PUBLIC: 'AQICAHixEHf7isdNQBbkxpofjAClbUEdI/Y6QQovlgFxV0cfRAF9vnM13bYmsoKZC/XL2tjLAAAArzCBrAYJKoZIhvcNAQcGoIGeMIGbAgEAMIGVBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDB34X0dM0w58MzLx6QIBEIBoI6Aj+JbP9IzZDVn5FFecz3ltSuWjeIJLq1UYZ6ZZ2HWBIE7W4Bo82PQHQuiCId06ogpPsgGrAeI4rcv4ipYFtv952Bnr0YQE36cnfmFKy/aRtasShUkLkJN8Tb0c85r2OsiJ96nwYHw='

#-----------------------------------
# local development configuration.
Expand Down
2 changes: 1 addition & 1 deletion env/ssocio.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ default_env: &default_env
TS: 1 # time-stamp in line
NAME: 'ssocio'
SRC: './src/'
SLACK_PUBLIC: https://stage-admin2.ssocioliving.com/slack/post
SLACK_PUBLIC: 'AQICAHixEHf7isdNQBbkxpofjAClbUEdI/Y6QQovlgFxV0cfRAF+FHEoDRV35GnfKVRptzG1AAAAkTCBjgYJKoZIhvcNAQcGoIGAMH4CAQAweQYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAx5BZAik3j59qOhBZcCARCATA6T1tI/R1nXo44Qfy7itfLJxsnFfaIGpLyBCGGvGO4E9SlOC7aG9mHWYvkl7v5HyGc019HbfKdpiigFo4R/26gyaiMWICuxp+SRbf4='

#-----------------------------------
# local development configuration.
Expand Down
83 changes: 65 additions & 18 deletions src/api/hello-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ exports = module.exports = (function (_$, name) {
next = do_get_hello;
else if (ID !== '!' && CMD === 'test-sns')
next = do_get_test_sns;
else if (ID !== '!' && CMD === 'test-encrypt')
next = do_get_test_encrypt;
break;
case 'PUT':
if (false);
Expand Down Expand Up @@ -131,6 +133,11 @@ exports = module.exports = (function (_$, name) {
/** ********************************************************************************************************************
* Local Functions.
** ********************************************************************************************************************/
const $kms = function() {
if(!_$.kms) throw new Error('$kms(kms-service) is required!');
return _$.kms;
}

//! shared memory.
//WARN! - `serverless offline`는 상태를 유지하지 않으므로, NODES값들이 실행때마다 리셋이될 수 있음.
const NODES = [
Expand Down Expand Up @@ -185,6 +192,34 @@ exports = module.exports = (function (_$, name) {
})
};


//! store channel map in cache
const $channels = {}
const do_load_slack_channel = (name) => {
const ENV_NAME = `SLACK_${name}`.toUpperCase();
const $env = process.env||{};
const webhook = $channels[ENV_NAME]||$env[ENV_NAME]||'';
_log(NS, '> webhook :=', webhook);
if (!webhook) return Promise.reject(new Error(`env[${ENV_NAME}] is required!`));
return Promise.resolve(webhook)
.then(_ => {
if (!_.startsWith('http')){
return $kms().do_decrypt(_)
.then(_ => {
const url = `${_}`.trim();
$channels[ENV_NAME] = url;
return url;
})
}
return _;
})
.then(_ => {
if (!(_ && _.startsWith('http')))
throw new Error('404 NOT FOUND - Channel:'+name);
return _;
})
}

/** ********************************************************************************************************************
* Public API Functions.
** ********************************************************************************************************************/
Expand Down Expand Up @@ -268,11 +303,9 @@ exports = module.exports = (function (_$, name) {
})
}


/**
* Post message via Slack Web Hook
*
*
* ```sh
* # post message to slack/general
* $ echo '{"text":"hello"}' | http ':8888/hello/public/slack'
Expand All @@ -285,24 +318,16 @@ exports = module.exports = (function (_$, name) {
*/
async function do_post_hello_slack(ID, $param, $body, $ctx){
_log(NS, `do_post_hello_slack(${ID})....`);
if (ID !== 'public') return Promise.reject(new Error('404 NOT FOUND - Channel:'+ID));
$param = $param||{};

//! basic configuration.
const WEBHOOK = 'https://hooks.slack.com/services/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'; // channel: `lemoncloud-io/public`
const message = typeof $body == 'string' ? {text: $body} : $body;

//1. load target webhook via environ.
const $env = process.env||{};
const ENV_NAME = `SLACK_${ID}`.toUpperCase();
const webhook = $env[ENV_NAME]||WEBHOOK;
_log(NS, '> webhook :=', webhook);

//2. post message.
const res = await postMessage(webhook, message);

//3. returns
return res;
//! load target webhook via environ.
return do_load_slack_channel(ID)
.then(webhook => {
_log(NS, '> webhook :=', webhook);
// return { webhook }
return postMessage(webhook, message);
})
}

/**
Expand Down Expand Up @@ -388,7 +413,29 @@ exports = module.exports = (function (_$, name) {
})()
.then(local_chain_handle_sns)
}


/**
* Encrypt Test.
*
* ```sh
* $ http ':8888/hello/0/test-encrypt'
*/
function do_get_test_encrypt(ID, $param, $body, $ctx){
_log(NS, `do_get_test_encrypt(${ID})....`);
const message = 'hello lemon';
return $kms().do_encrypt(message)
.then(encrypted => {
return $kms().do_decrypt(encrypted)
.then(decrypted => {
return { encrypted, decrypted, message }
})
})
.then(_ => {
const result = _.encrypted && _.message === _.decrypted;
return { result, ..._ }
})
}

//! return fially.
return main;
//////////////////////////
Expand Down
16 changes: 8 additions & 8 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,26 +144,26 @@ function initialize($export, options) {
if (typeof _$ !== 'function') throw new Error('_$ should be function.');

//! load common functions
const _log = _$.log;
const _inf = _$.inf;
const _err = _$.err;

//! load configuration.
const STAGE = _$.environ('STAGE', '');
STAGE && _inf('#STAGE =', STAGE);

//! load utilities.
//! load utilities & aws
const $U = require('./lib/utilities')(_$);
const $aws = require('aws-sdk'); // AWS module.

//! register to global instance manager.
_$('U', $U); // register: Utilities.
_$('U', $U);
_$('aws', $aws);

//! load basic core services......
const $kms = require('./service/kms-service')(_$);
_$('kms', $kms);

//! load api functions......
//! load api functions............
const hello = require('./api/hello-api')(_$);
_$('aws', $aws); // register: aws instance.

//! register api
_$('hello', hello)

//! export.
Expand Down
92 changes: 92 additions & 0 deletions src/service/kms-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* kms-service.js
* - encrypt/decrypt service api with KMS
*
*
* Author: Steve ([email protected])
* Date : 2018-11-20
*
* Copyright (C) 2019 Lemoncloud - All Rights Reserved.
*/
module.exports = (function (_$, name, options) {
"use strict";
name = name || 'KMS'; // engine service name.

// core module
const $U = _$.U;
if (!$U) throw new Error('$U is required!');

//! load common(log) functions
const _log = _$.log;
const _inf = _$.inf;
const _err = _$.err;

// NAMESPACE TO BE PRINTED.
const NS = $U.NS(name);

//! external service
const $aws = function() {
if(!_$.aws) throw new Error('$aws is required!');
return _$.aws;
}

/** ****************************************************************************************************************
* Public Common Interface Exported.
** ****************************************************************************************************************/
//TODO - load via environ.
const REGION = 'ap-northeast-2';

//! check if base64 string.
const isBase64 = (text) => /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/.test(text);

/**
* Encrypt message
*
* @param {*} message
*/
function do_encrypt(message) {
_inf(NS, 'do_encrypt()..');
const AWS = $aws();
const kms = new AWS.KMS({ region: REGION });
const params = {
KeyId: 'alias/lemon-hello-api',
Plaintext: message,
};
return kms.encrypt(params).promise()
.then(result => {
_log(NS, '> result =', result);
const ciphertext = result.CiphertextBlob ? (result.CiphertextBlob).toString('base64') : message;
_log(NS, '> ciphertext[' + message + '] =', ciphertext.substring(0, 32), '...');
return ciphertext;
})
}

/**
* Decrypt message
*
* @param {*} message
*/
function do_decrypt(encryptedSecret) {
_inf(NS, 'do_decrypt()..');
const AWS = $aws();
const kms = new AWS.KMS({ region: REGION });
encryptedSecret =
typeof encryptedSecret == 'string'
? isBase64(encryptedSecret)
? Buffer.from(encryptedSecret, 'base64')
: encryptedSecret
: encryptedSecret;
//! api param.
const params = {
CiphertextBlob: encryptedSecret,
};
return kms.decrypt(params).promise()
.then(result => {
_log(NS, '> result =', result);
return result.Plaintext ? result.Plaintext.toString() : '';
})
}

//! returns.
return { do_encrypt, do_decrypt };
});

0 comments on commit 7820c2a

Please sign in to comment.