한동안 바빠 오랜만에 글을 쓰는것 같다.


얼마전 프로젝트를 통해 REST 통신시 데이터 인증을 하기 위해 HMAC SHA-256 알고리즘을 사용한다는 것을 알게 되었다.

일방향 암호화라 복호화가 불가능하다. 클라이언트와 서버가 동일한 키값을 이용하여 데이터에 대한 무결성을 확인한다.


HMAC SHA-256에 대한 자세한 내용 및 이론은 아래 링크를 참조

SHA, HMAC


소스로 확인해보자.


C# Code

* C# HMACSHA256 클래스에 대한 정의는 아래 링크를 참조

https://msdn.microsoft.com/ko-kr/library/system.security.cryptography.hmacsha256(v=vs.110).aspx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// System.Security.Cryptography <- using을 해야 C#의 HMACSHA256 클래스 사용이 가능하다.
using System.Security.Cryptography;
 
// HMAC 생성 함수
private string GenerateHMAC(string key, string payload)
{
    // 키 생성
    var hmac_key = Encoding.UTF8.GetBytes(key);
 
    // timestamp 생성
    var timeStamp = DateTime.UtcNow;
    var timeSpan = (timeStamp - new DateTime(197011000));
    var hmac_timeStamp = (long)timeSpan.TotalMilliseconds;
 
    // HMAC-SHA256 객체 생성
    using (HMACSHA256 sha = new HMACSHA256(hmac_key))
    {
        // 본문 생성
        // 한글이 포함될 경우 글이 깨지는 경우가 생기기 때문에 payload를 base64로 변환 후 암호화를 진행한다.
// 타임스탬프와 본문의 내용을 합하여 사용하는 경우가 일반적이다.
// 타임스탬프 값을 이용해 호출, 응답 시간의 차이를 구해 invalid를 하거나 accepted를 하는 방식으로 사용가능하다.
// 예시에서는 (본문 + 타임스탬프)이지만, 구글링을 통해 찾아보면 (본문 + "^" + 타임스탬프) 등의 방법을 취한다.
        var bytes = Encoding.UTF8.GetBytes(payload + hmac_timeStamp);
        string base64 = Convert.ToBase64String(bytes);
        var message = Encoding.UTF8.GetBytes(base64);
 
        // 암호화
        var hash = sha.ComputeHash(message);
 
        // base64 컨버팅
        return Convert.ToBase64String(hash);
    }
}
cs


Javascript Code

Javascript를 통해 HMAC SHA256 암호화를 하기 위해선 아래 두개의 라이브러리를 로드해야한다.

1
2
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/hmac-sha256.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/enc-base64-min.js"></script>
cs

소스 및 예제 실행은 아래 링크를 통해 확인.

Javascript HMAC SHA-256


Node.js Code

nodejs의 crypto를 이용해 HMAC SHA-256 암호화를 한다.

* nodejs crypto에 대한 정의는 아래 링크를 참조

https://nodejs.org/api/crypto.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var crypto = require('crypto');
 
function GenerateHMAC(key, payload) {
    // 암호화 객체 생성, sha256 알고리즘 선택
    var hmac = crypto.createHmac('sha256', key);
 
    // 암호화할 본문 생성
    var timestamp = new Date().getTime();
    var message = new Buffer(payload + timestamp).toString('base64');
 
    hmac.write(message);
    hmac.end();
 
    return hmac.read();
}
 
var hash = GenerateHMAC('hello world''sha256');
 
var encoded_hash  = new Buffer(hash).toString('base64');
 
console.log(encoded_hash);
cs


C#, Javascript, Node.js 예제 소스에 동일한 payload와 key, timestamp 입력시 동일한 hash값을 얻을 수 있었다.


끝.

'Programming > 기타' 카테고리의 다른 글

HMAC SHA-256 암호화 (C#, Javascript, Nodejs)  (2) 2018.08.28
[C#]텔레그램 봇 만들기  (2) 2017.01.05
noVNC  (0) 2017.01.02
  1. 빗물 2019.08.21 14:35

    제가 하는 project가 node.js를 가져가 쓸수 없는 구조라
    무척 많은 도움이 되었습니다.
    감사드립니다. 꾸벅~~

작년에 클라이언트 프로그램에서 이슈가 발생했을 때 사용자에게 문자를 보내줘야하는 기능을 개발 해야했는데 

SMS는 돈주고 사야하므로 유료이므로 난 그럴 능력이 없으므로 텔레그램 봇을 개발했었다. 


그때는 "NetTelegramBotApi"라는 라이브러리를 사용해 개발을 했었는데

지금 글을 쓰려고 다시 찾아보니 Nuget에 더 좋은 라이브러리가 있었다.


"NetTelegramBotApi"는 패스 


Nuget에서 telegram을 검색하면 제일 위에 나오는 "Telegram.Bot"라이브러리를 써보자.

 <- 요놈 받으면 된다.



일단 개발해보기에 앞서 Telegram Bot을 만들려면 API access 키를 발급 받아야 한다.

어려울거 하나 없다.


핸드폰 보랴 모니터 보랴 왔다갔다 하지말고 https://web.telegram.org/ 에 들어가서 봇을 추가하자.


1. BotFather를 찾아라.

BotFather를 클릭하면 아래와 같이 화면이 바뀐다.



Start 버튼을 클릭하자.


그럼, 봇아빠가 알아듣는 커맨드가 쭈루룩 나온다. 그걸 잘 읽어보면 봇을 만들 수 있다.


2. Bot을 추가해라.


봇아빠에게 /newbot 이라고 입력하자.

그럼 이름을 말하라고 한다.

원하는 봇의 이름을 입력하자.

주의할점은 끝에 _bot 또는 Bot이 꼭 들어가야 한다. 안그러면 안만들어쥼


이름을 잘 입력해서 만들면 access token을 바로 똭하고 준다.


/// 그 외 다양한 커맨드가 있으니 한번씩 해보세요. 재밌어요.


3. Access Token을 받아 개발을 해보자.

그럼 access token을 복사해서 봇을 만들어 보자.




* 참고 

Telegram 홈페이지 Telegram Bot API : Telegram API Doc

Telegram.Bot 라이브러리 도큐먼트 : Telegram.Bot dll Doc

Telegram.Bot 라이브러리 예제 : Telegram.Bot Example  << 예제를 보면 다양한 케이스가 많다. 사진, 음성, 위치접근, 연락처, 등등


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////만들어 보자


나는 C# 콘솔 응용 프로그램 으로 만들었다.

    class Program
    {
        static void Main(string[] args)
        {
            /// 봇 접근 함수 호출
            testAPIAsync();
            Console.ReadLine();
        }

        /// 비동기 봇 접근
        static async void testAPIAsync()
        {
            var Bot = new Telegram.Bot.TelegramBotClient("봇 아빠가 준 access token을 넣어요.");
            var me = await Bot.GetMeAsync();
            // 올바르게 접속이 되면 봇 이름이 출력된다.
            System.Console.WriteLine("Hello my name is " + me.FirstName);
        }
    }

위 소스를 실행 하면 다음과 같은 결과 화면을 볼 수 있다.


간단하게 봇을 실행 시키는 것까지 성공했다.


하지만 내가 해야할 것은 이벤트가 생겼을 때 telegram 봇이 사용자에게 메세지를 전송 하거나, 


사용자로 부터 명령을 받아 DB를 바꾼다던지, DB를 조회해 정보를 보여준다던지 등의 작업을 해야한다.


아래 소스를 하나씩 따라가보자.


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

///텔레그램 dll using
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.ReplyMarkups;

namespace telegram
{
    class Program
    {
        //봇 생성
        private static readonly TelegramBotClient Bot = new TelegramBotClient("봇 아빠가 준 access token을 넣어요.");

        //임시방편 유저 리스트
        private static List<User> Users = new List<User>();
        
        //telegram 사용자의 상태 저장 변수
        private static Dictionary<long, UserState> dicUserState = new Dictionary<long, UserState>();

        static void Main(string[] args)
        {
            ///임시방편 유저 리스트에 유저 추가
            Users.Add(new User("시나공공", 28)); //28살이다...ㅁㄴ아ㅓㄹ ㅓㅁㄴ아ㅣㄹ허 님
            Users.Add(new User("텔레그램", 30));
            Users.Add(new User("봇아빠", 55));

            ///봇 이벤트 추가
            Bot.OnMessage += Bot_OnMessage;
            Bot.OnMessageEdited += Bot_OnMessage;
            Bot.OnReceiveError += Bot_OnReceiveError;
        
            ///Me 획득 (Me가 뭔지 정확하게는.... 일단 눈치껏 보면 Username에 sinagonggongBot이 들어가는걸 보면 Bot 본인을 뜻하는 듯하다)
            var me = Bot.GetMeAsync().Result;

            Console.Title = me.Username;

            /// Recv Start
            Bot.StartReceiving();
            Console.ReadLine();
            /// Recv Stop
            Bot.StopReceiving();
        }

        /// Recv Error
        private static void Bot_OnReceiveError(object sender, Telegram.Bot.Args.ReceiveErrorEventArgs e)
        {
            Debugger.Break();
        }

        ///사용자로 부터 Message Recv
        private static async void Bot_OnMessage(object sender, Telegram.Bot.Args.MessageEventArgs messageEventArgs)
        {
            /// Message 객체
            var message = messageEventArgs.Message;

            /// 예외처리
            if (message == null || message.Type != MessageType.TextMessage) return;

            /// "/사용자추가" 라는 명령을 받음
            if (message.Text.StartsWith("/사용자추가"))
            {
                dicUserState[message.Chat.Id] = UserState.addUser;
                await Bot.SendTextMessageAsync(message.Chat.Id, 
                    @"사용자 이름과 나이를 입력해 주세요.
ex)시나공공,28");
            }

            /// "/사용자삭제" 라는 명령을 받음
            else if (message.Text.StartsWith("/사용자삭제"))
            {
                dicUserState[message.Chat.Id] = UserState.deleteUser;
                await Bot.SendTextMessageAsync(message.Chat.Id, "사용자 이름을 입력해 주세요.");
            }

            /// "/사용자목록" 라는 명령을 받음
            else if (message.Text.StartsWith("/사용자목록"))
            {
                dicUserState[message.Chat.Id] = UserState.none;

                string _message = string.Empty;
                Users.ForEach(x => _message += string.Format("이름 : {0}, 나이 : {1}\r\n", x.Name, x.Age) );

                await Bot.SendTextMessageAsync(message.Chat.Id, _message);
            }

            /// "/도움말" 라는 명령을 받음
            else if(message.Text.StartsWith("/도움말"))
            {
                var usage = @"
/사용자추가    - 사용자 추가
/사용자삭제 - 사용자 삭제
/사용자목록  - 사용자 목록
/도움말       - 도움말
                            ";

                await Bot.SendTextMessageAsync(message.Chat.Id, usage,
                    replyMarkup: new ReplyKeyboardHide());
            }
            /// 그 외 다른 말을 받을 경우 사용자 상태를 보고 적절하게 대응한다.
            else
            {
                /// 예외처리
                if(!dicUserState.ContainsKey(message.Chat.Id))
                {
                    await Bot.SendTextMessageAsync(message.Chat.Id, "먼 말인지 모르겠어요.");
                    return;
                }

                /// 사용자 상태가 사용자 추가일 경우
                if (dicUserState[message.Chat.Id] == UserState.addUser)
                {
                    /// 이름,나이 로 입력을 받을 것이기 때문에 , 로 tokenizing하자
                    /// 0번은 이름, 1번은 나이
                    string[] NameAndAge = message.Text.Split(',');

                    /// 쪼갰는데 개수가 2보다 작으면 잘못된 값을 받았다 판단
                    if (NameAndAge.Length < 2)
                    {
                        await Bot.SendTextMessageAsync(message.Chat.Id, 
                            @"다시 입력해 주세요.
ex)시나공공, 28");
                        return;
                    }

                    /// 이름 나이 셋팅
                    string _name = NameAndAge[0];
                    int _age = 0;
                    bool result = Int32.TryParse(NameAndAge[1], out _age);

                    /// 나이값이 정상이면 추가
                    if(result)
                    {
                        // DB작업을 해야한다면 여기서 하면 될것같다.
                        Users.Add(new User(_name, _age));
                        await Bot.SendTextMessageAsync(message.Chat.Id, message.Text + " 사용자를 추가 했어요.");
                        dicUserState[message.Chat.Id] = UserState.none;
                    }
                    /// 나이값이 이상하면 예외
                    else
                    {
                        await Bot.SendTextMessageAsync(message.Chat.Id,
                            @"나이가 이상해요.
다시 입력해 주세요.");
                    }
                }

                /// 사용자 상태가 사용자 삭제일 경우
                else if(dicUserState[message.Chat.Id] == UserState.deleteUser)
                {
                    /// SingleOfDefault로 사용자를 찾았는데 없으면 예외처리 있으면 삭제
                    var _user = Users.SingleOrDefault(x => x.Name == message.Text);
                    if (_user != null)
                    {
                        // DB작업을 해야한다면 여기서 하면 될것같다.
                        Users.Remove(_user);
                        await Bot.SendTextMessageAsync(message.Chat.Id, message.Text + " 사용자를 삭제 했어요.");
                        dicUserState[message.Chat.Id] = UserState.none;
                    }
                    else
                    {
                        await Bot.SendTextMessageAsync(message.Chat.Id, 
                            message.Text + @" 사용자가 없어요.
다시 입력해 주세요.");
                    }
                }

                /// 사용자 상태가 추가, 삭제가 아닌 경우 시치미 뚝
                else
                {
                    await Bot.SendTextMessageAsync(message.Chat.Id, "먼 말인지 모르겠어요.");
                }

                
            }
        }
    }

    /// <summary>
    /// 사용자를 상태를 나타내는 enum
    /// </summary>
    public enum UserState
    {
        addUser,
        deleteUser,
        none
    }

    /// <summary>
    /// 사용자 클래스, 이름과 나이
    /// </summary>
    public class User
    {
        string name = string.Empty;
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        int age = 0;
        public int Age
        {
            get { return age; }
            set { age = value; }
        }

        public User() : this("none", 0) { }
        public User(string name, int age)
        {
            this.Name = name;
            this.Age = age;
        }
    }
}


이렇게 하면 아래와 같은 결과를 볼 수 있다.

봇에게 말을 걸려면 봇아빠에게 말을 걸었던것과 같이 하면 된다.

나의 경우 @sinagonggongBot을 검색해 대화를 시작했다.




Telegram Bot을 이용해 간단하게 요청을 하고 응답을 받는 서비스를 만들어봤다.

네이버나 다음의 API를 섞어서 사용하거나, 나름의 알고리즘으로 유용한 봇을 만들 수 있을 것 같다.


아 5초마다 이벤트 발생시켜서 사용자한테 메세지 보내는걸 까먹었네. 그건 패스할게요... 귀찮...

키포인트는 Message.Chat.Id를 잘 보관해야 한다.


작년 개발 당시 Message.Chat.Id를 로그인 DB에 컬럼을 추가해 같이 저장해 이용했다.

이슈가 생겼을 때 DB의 TelegramID를 조회해 sendMessage를 했었다.


Group Chat방에 메세지를 보낼땐 https://stackoverflow.com/a/45577773 참조 바랍니다. (Group Chat ID 얻어오기 Tip)



끝.

'Programming > 기타' 카테고리의 다른 글

HMAC SHA-256 암호화 (C#, Javascript, Nodejs)  (2) 2018.08.28
[C#]텔레그램 봇 만들기  (2) 2017.01.05
noVNC  (0) 2017.01.02
  1. 짧은 프로그램 2018.02.13 07:15

    좋은 정보 감사드립니다.

    꼼꼼하게 적어 주신 덕분에 잘 배우고 갑니다.^^

    오늘도 좋은 하루 되십시요.^^

noVNC란 브라우져에서 PC를 원격으로 제어하는 브라우저 VNC라고 생각하면 된다.

작동원리는.. 정확하진 않지만 아마 이런 그림일 것이다.



noVNC 서버는 Python, Nodejs 등 여러 언어를 지원하며 MPL-2.0 라이센스이다.

자세한 내용은 아래 noVNC 사이트에서 확인 가능하다.

noVNC 클라이언트 출처 : https://kanaka.github.io/noVNC/

noVNC 서버 "Websockify" 출처 : https://github.com/novnc/websockify


*사용 방법

nodejs를 이용한 noVNC 사용법을 소개하려한다.


1. websockify.js를 node.js로 실행 (node.js 실행 방법은 인터넷을 찾아보자)


github Websockify(noVNC Server) 프로젝트에 들어가서 소스를 다운받아 압축 해제

"other -> js -> websockify.js"라는 파일이 있다.


node.js를 처음 설치 후 websockify.js를 실행하면 여러가지 모듈이 설치가 안되 실행이 되지 않을 수 있다.

다음과 같은 모듈들을 설치 후 다시 시도해 보자

"optimist", "policyfile", "ws" 모듈을 설치 후 다시 시도하면 다음과 같은 모습을 볼 수 있다.

모듈 설치 방법은 npm을 이용해 설치하면된다. ex) "npm install optimist"

"node websockify.js 6080 서버PC의 IP:5900"를 입력하면 다음과 같이 서버가 실행되는 모습을 볼 수 있다.

여기서 6080은 Browser와 websockify서버가 통신을 할때 사용할 port를 의미 하고,

서버PC의 IP는 websockify서버가 실행될 pc의 IP를 의미한다.

5900은 websockify서버와 vnc서버(real, ultra 등등)가 통신을 할때 사용하는 port를 의미한다.


2. vnc.html 실행

github noVNC(noVNC Client) 프로젝트에 들어가서 소스를 다운받아 압축 해제

"noVNC-master -> vnc.html"라는 파일이 있다.

vnc.html을 더블클릭하여 브라우저에 띄우게 되면 다음과 같은 화면이 나타난다.

톱니바퀴 모양의 Setting과 자물쇠? 쇠사슬? 모양의 Connection을 설정 후 noVNC를 접속해보자.


톱니바퀴 모양

 Path를 localhost:5900으로 설정한다.

 Host를 localhost로 Port를 6080으로 설정 후 Connect버튼을 클릭하면!

여기서 localhost는 websockify 서버pc의 ip를 의미합니다.

.

.

.

.

.

.

.

.

짜란! 끗!

'Programming > 기타' 카테고리의 다른 글

HMAC SHA-256 암호화 (C#, Javascript, Nodejs)  (2) 2018.08.28
[C#]텔레그램 봇 만들기  (2) 2017.01.05
noVNC  (0) 2017.01.02

+ Recent posts