Rhythm & Biology

Engineering, Science, et al.

AES128暗号化 - Java編 -

先日iPhone(というかObjective-C)でのAES128暗号化について触れましたが、今回はJava編です。未確認ですが、おそらくAndroidでも動きます。

/*
 *  $ javac CryptAES.java
 *  $ java CryptAES <secret_key> <iv> <message>
 */
import java.security.spec.AlgorithmParameterSpec;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.crypto.spec.IvParameterSpec;

class CryptAES {
  static final String cipher_type = "AES/CBC/PKCS5Padding";

  public static void main(String[] args) {
    String key  = args[0];
    String iv   = args[1];
    String data = args[2];

    byte[] enc = encode(key, iv, data.getBytes());
    byte[] dec = decode(key, iv, enc);

    for (int i = 0; i < enc.length; i++) {
      System.out.printf("%02x", enc[i]);
    }
    System.out.println();
        
    System.out.println(new String(dec));
  }

  public static byte[] encode(String skey, String iv, byte[] data) {
    return process(Cipher.ENCRYPT_MODE, skey, iv, data);
  }

  public static byte[] decode(String skey, String iv, byte[] data) {
    return process(Cipher.DECRYPT_MODE, skey, iv, data);
  }

  private static byte[] process(int mode, String skey, String iv, byte[] data) {
    SecretKeySpec key = new SecretKeySpec(skey.getBytes(), "AES");
    AlgorithmParameterSpec param = new IvParameterSpec(iv.getBytes());
    try {
      Cipher cipher = Cipher.getInstance(cipher_type);
      cipher.init(mode, key, param);
      return cipher.doFinal(data);
    } catch (Exception e) {
      System.err.println(e.getMessage());
      throw new RuntimeException(e);
    }
  }

}

今回はivは指定のものを使うようにしてありますが、特に指定がなければ自動で生成されますし、そのivをあとから取り出すことももちろん可能です。
パディング方式に関しては外部ライブラリをいれない限りはPKCS7が使えません。ただし、デフォルトで使えるPKCS5は7と互換性が(ほぼ)あるため、特に問題が起きることはないでしょう(逆にPKCS7のライブラリがバグってるという話もあるので、無理にPKCS7にしようとすると本末転倒な結果になりかねません)。
それから、128bitの指定などは特にしていないのですが、secret keyやivの長さ(どちらも16バイト)から自動で判別しているのだと思います。secret keyの長さを変えれば自動で256bitになったりするのでしょうか(要検証)。

Javaは普段使ってないので、もしマズいところがあればご指摘頂けるとありがたいです。

iPhoneでAES128暗号化

iPhoneアプリ開発で暗号化を行う必要が出てきたので、試しに書いてみました。
暗号方式にはAES128を、Padding方式にはPKCS7を使っています。

ここで使っているCommonCryptoはiOS SDKについてくるものですが、mac上でも普通に使うことができます(iPhone Simulator用のライブラリを無理矢理使います)。

使い方としては、下のコードをコンパイルして、

$ ./a.out aaaabbbbccccdddd eeeeffffgggghhhh message

というようにすると"message"が暗号化され、さらに続けて復号化されます。
第一引数は鍵、第二引数はInitial Vectorです。どちらも16バイトである必要があります。

/*                                                                                                  
 * gcc -std=c99 crypto.m -framework Foundation
 */
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonCryptor.h>

@interface NSData (AES)
- (NSData *)AES128Operation:(CCOperation)operation key:(NSString *)key iv:(NSString *)iv;
- (NSData *)AES128EncryptWithKey:(NSString *)key iv:(NSString *)iv;
- (NSData *)AES128DecryptWithKey:(NSString *)key iv:(NSString *)iv;
@end

@implementation NSData (AES)
- (NSData *)AES128Operation:(CCOperation)operation key:(NSString *)key iv:(NSString *)iv
{
  char keyPtr[kCCKeySizeAES128 + 1]; 
  memset(keyPtr, 0, sizeof(keyPtr));
  [key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];

  char ivPtr[kCCBlockSizeAES128 + 1]; 
  memset(ivPtr, 0, sizeof(ivPtr));
  [iv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];

  NSUInteger dataLength = [self length];
  size_t bufferSize = dataLength + kCCBlockSizeAES128;
  void *buffer = malloc(bufferSize);

  size_t numBytesCrypted = 0;
  CCCryptorStatus cryptStatus = CCCrypt(operation,
                                        kCCAlgorithmAES128,
                                        kCCOptionPKCS7Padding,
                                        keyPtr,
                                        kCCBlockSizeAES128,
                                        ivPtr,
                                        [self bytes],
                                        dataLength,
                                        buffer,
                                        bufferSize,
                                        &numBytesCrypted);
  if (cryptStatus == kCCSuccess) {
    return [NSData dataWithBytesNoCopy:buffer length:numBytesCrypted];
  }
  free(buffer);
  return nil;
}

- (NSData *)AES128EncryptWithKey:(NSString *)key iv:(NSString *)iv
{
  return [self AES128Operation:kCCEncrypt key:key iv:iv];
}

- (NSData *)AES128DecryptWithKey:(NSString *)key iv:(NSString *)iv
{
  return [self AES128Operation:kCCDecrypt key:key iv:iv];
}
@end

int main(int argc, char const* argv[])
{
  NSAutoreleasePool* pool;
  pool = [[NSAutoreleasePool alloc] init];

  NSString *key = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
  NSString *iv = [NSString stringWithCString:argv[2] encoding:NSUTF8StringEncoding];
  NSString *data_str = [NSString stringWithCString:argv[3] encoding:NSUTF8StringEncoding];
  NSData *data = [data_str dataUsingEncoding:NSUTF8StringEncoding];

  NSData *en_data = [data AES128EncryptWithKey:key iv:iv];
  NSData *de_data = [en_data AES128DecryptWithKey:key iv:iv];

  NSString *de_str = [[[NSString alloc] initWithData:de_data
                                            encoding:NSUTF8StringEncoding] autorelease];

  NSLog(@"%@", en_data);
  NSLog(@"%@", de_str);

  [pool drain];
  return 0;
}

暗号の話は難しいですね。勉強が必要です。


補足(2011/10/18):
CCCryptはデフォルトでCBC(cipher block chaining)を使うようになっています。ECB(electronic codebook)を指定することもできますが、使う理由は特にないでしょう。

libeventでHTTPサーバを試す

libeventに含まれるevhttpを使って、簡単なhttpサーバを作ってみました。

404を返す

まずは404 not foundを返すコードを書いてみます。ただし、GET以外のメソッドの場合はBad Requestを返すようにします。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <event.h>                                                                                  
#include <evhttp.h>

#define HTTPD_ADDR "0.0.0.0"
#define HTTPD_PORT 8080

void req_handler(struct evhttp_request *r, void *arg)
{
  if (r->type != EVHTTP_REQ_GET)
    evhttp_send_error(r, HTTP_BADREQUEST, "Available GET only");
  else
    evhttp_send_error(r, HTTP_NOTFOUND, "Not Found");
}

int main(int argc, const char* argv[])
{
  struct event_base *ev_base;
  struct evhttp *httpd;

  ev_base = event_base_new();
  httpd = evhttp_new(ev_base);
  if (evhttp_bind_socket(httpd, HTTPD_ADDR, HTTPD_PORT) < 0) {
    perror("evhttp_bind_socket()");
    exit(EXIT_FAILURE);
  }
  evhttp_set_gencb(httpd, req_handler, NULL);
  event_base_dispatch(ev_base);
  evhttp_free(httpd);
  event_base_free(ev_base);

  return 0;
}

コンパイル・実行して、ブラウザでlocalhost:8080にアクセスすると"Not Found"と表示されます。

plain textを返す

今度はtext/plainで"Hello World"と返すようにしてみます。main関数は上のものと同じなので省略します。

void req_handler(struct evhttp_request *r, void *arg)
{
  struct evbuffer *evbuf;
  char message[] = "Hello World";
  int message_length = strlen(message);
  char content_length[8];

  evbuf = evbuffer_new();
  if (evbuf == NULL) {
    evhttp_send_error(r, HTTP_SERVUNAVAIL, "Failed to create buffer");
    return;
  }

  snprintf(content_length, 7, "%d", message_length);
  evhttp_add_header(r->output_headers, "Content-Type", "text/plain");
  evhttp_add_header(r->output_headers, "Content-Length", content_length);
  evbuffer_add(evbuf, message, message_length);
  evhttp_send_reply(r, HTTP_OK, "", evbuf);
  evbuffer_free(evbuf);
}

少し複雑になってきましたが、基本的にはheaderを用意して、struct evbufferにbodyを書き込んで送る、というだけです。

もう少しマシなhttpサーバ

エラー対策などはかなり無視しますが、それなりに動くhttpサーバを書いてみます。

#define DOCUMENT_ROOT "."

static const char *get_mime_type(const char *filepath)
{
  const char *filetype = strrchr(filepath, '.');

  if (strcasecmp(filetype, ".html") == 0 ||
      strcasecmp(filetype, ".htm") == 0)
    return "text/html";
  else if (strcasecmp(filetype, ".js") == 0)
    return "text/javascript";
  else if (strcasecmp(filetype, ".css") == 0)
    return "text/css";
  else if (strcasecmp(filetype, ".txt") == 0)
    return "text/plain";
  else if (strcasecmp(filetype, ".ico") == 0)
    return "image/x-icon";
  else if (strcasecmp(filetype, ".png") == 0)
    return "image/png";
  else if (strcasecmp(filetype, ".gif") == 0)
    return "image/gif";
  else if (strcasecmp(filetype, ".jpeg") == 0 ||
           strcasecmp(filetype, ".jpg") == 0)
    return "image/jpeg";
  else if (strcasecmp(filetype, ".pdf") == 0)
    return "application/pdf";
  return "application/octet-stream";
}

static const char *get_content_from_file(const char *req_path, char **content, int *content_length)
{
  struct stat sb;
  static char filepath[1024];
  FILE *fp;
  int rsize;

  *content = NULL;
  sprintf(filepath, "%s%s", DOCUMENT_ROOT, req_path);

  if (stat(filepath, &sb) < 0)
    return filepath;

  if (S_ISDIR(sb.st_mode)) {
    if (strcmp(req_path, "/") == 0)
      sprintf(filepath, "%s%sindex.html", DOCUMENT_ROOT, req_path);
    else
      sprintf(filepath, "%s%s/index.html", DOCUMENT_ROOT, req_path);

    if (stat(filepath, &sb) < 0)
      return filepath;
  }

  *content_length = (int)sb.st_size;

  fp = fopen(filepath, "rb");
  if (fp) {
    *content = (char *)malloc(*content_length);
    rsize = fread(*content, 1, *content_length, fp);
    fclose(fp);                                                                                     
    if (rsize != *content_length) {
      free(*content);
      *content = NULL;
    }
  }

  return filepath;
}

void req_handler(struct evhttp_request *r, void *arg)
{
  struct evbuffer *evbuf;
  const char *req_path, *res_path;
  char *content;
  int content_length;
  char content_length_str[12];

  if (r->type != EVHTTP_REQ_GET) {
    evhttp_send_error(r, HTTP_BADREQUEST, "Available GET only");
    return;
  }

  req_path = evhttp_request_uri(r);
  res_path = get_content_from_file(req_path, &content, &content_length);
  if (content == NULL) {
    evhttp_send_error(r, HTTP_NOTFOUND, "Not Found");
    return;
  }
  sprintf(content_length_str, "%d", content_length);

  evhttp_add_header(r->output_headers, "Content-Type", get_mime_type(res_path));
  evhttp_add_header(r->output_headers, "Content-Length", content_length_str);

  evbuf = evbuffer_new();
  evbuffer_add(evbuf, content, content_length);
  evhttp_send_reply(r, HTTP_OK, "", evbuf);
  evbuffer_free(evbuf);
}

mime typeが全然足りないとか、mime typeの判別はハッシュ使ったほうがいいとかは気にしないでください。とりあえずそれなりにまともな動作をします。

まとめ

evhttpの基本的な使い方は分かったので、あとは細かいことをやっていくだけですね。わざわざhttpサーバを書く機会も滅多にないと思いますが、楽しかったので満足です。