Cảm ơn bạn đã đọc và ủng hộ blog KTMT ʘ‿ʘ Từ bây giờ chúng tôi sẽ là kipalog.com !

Comments

Mở đầu

2h sáng. “beep, you’ve got mail”. Mail từ hệ thống giám sát zabbix.

1 URL quan trọng trong hệ thống web không hiển thị được. Truy cập vào URL đó nhận status code http trả về 503. Zabbix định kỳ kiểm tra mã lỗi và khi mã trả về khác 200, zabbix gửi mail cho hắn.

“Lại có vấn đề gì rồi đây…” — hắn vùng dậy, mở laptop lên, mở browser ra và truy cập thử vào URL được thông báo. “Quả nhiên là không vào được”, hắn nghĩ. Ssh thử vào một máy chủ và kiểm tra error log. Thông báo lỗi “Không truy cập được đến máy chủ cơ sở dữ liệu X” liên tiếp liên tiếp được ghi ra log. “Máy chủ X lại có vấn đề gì rồi đây …”. Hắn vừa nghĩ, mắt vừa lướt qua các đồ thị giám sát tài nguyên của toàn bộ hệ thống. “Lưu lượng truy cập vào máy chủ web vẫn bình thường. Tỉ lệ cachehit vẫn không đổi. Mọi thứ không có gì có vẻ bất thường. Vậy vấn đề này ở máy chủ X rồi”. Hắn nghĩ, rồi gõ

ssh X

Truy tìm

X là một máy chủ cơ sở dữ liệu chạy mysql, 4 cores 24GB Ram 2 đĩa cứng 300GB RAID 1. Không quá yếu nhưng cũng không quá khoẻ. Vì là máy chủ cơ sở dữ liệu nên phần lớn tài nguyên của X được dùng cho mysql.

“Để xem chú mày bị làm sao nhé!” - hắn bắt đầu công đoạn chẩn đoán bệnh của máy chủ.

Sau khi vào máy chủ X, hắn gõ top. Lệnh top hiện ra máy chủ có 4 cores, tất cả đều có %cpu xấp xỉ 95%. Hắn gõ iostat 1, và quan sát I/O của đĩa cứng. TPS (Trasfer per second) biến động từ 131.89 xuống đến 19.00. tps trung bình không cao. Blk_wrtn/s và Blk_read/s cũng biến động nhưng trung bình cũng không cao.

“CPU hoạt động cật lực trong khi đấy I/O thì không quá lớn”, hắn ghi lại điểm quan trọng này trong đầu. Ghi nhớ xong, hắn tiếp tục mở slow query log ra xem. Log này ghi lại những query mà mysql chạy quá lâu hơn 1s. 1 loạt query kiểu

select * from table_name where video_id in (12345, ‘23434’) and language = ‘en-us’;

được ghi ra log.

Query trên có 2 điểm rất kỳ lạ.

  • Thứ nhất name được query theo cả kiểu số và xâu dữ liệu.
  • Thứ hai query trên khá đơn giản, lệnh show table status like ‘table_name’ cho hắn kết quả số dòng chỉ khoảng 70000 dòng - 1 con số không lớn. Vậy mà X phải hoạt đông 95% cpu mà vẫn không thể nào trả về kết quả câu lệnh trên trong 1s.
$ mysql -u root -p
Enter password: ***********
mysql> use database database_name;
mysql> show table status like ‘table_name’\G;
*************************** 1. row ***************************
           Name: table_name
         Engine: InnoDB
        Version: 10
     Row_format: Compact
           Rows: 72148
 Avg_row_length: 924
    Data_length: 66732032
Max_data_length: 0
   Index_length: 14630912
      Data_free: 7340032
 Auto_increment: NULL
    Create_time: 2013-10-11 18:33:07
    Update_time: NULL
     Check_time: NULL
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options:
        Comment: Latest translation for vid

Để xem chú mày đang bận rộn xử lý cái gì nhé.

mysql> show process list;

1 loạt query kiểu 

“select * from table_name where video_id in (12345, ‘23434’) and language = ‘en-us’;”

“beep, you’ve got mail”. Một mail mới lại về. Máy chủ web đã không thể nào truy cập được X. Zabbix thông báo bản thân zabbix cũng không thể nào truy cập máy chủ X để lấy thông tin giám sát.

“Tình huống có vẻ nghiêm trọng lên.” hắn lẩm bẩm.

Máy chủ bận rộn CPU, I/O không lớn chứng tỏ là query trên tốn rất nhiều CPU. Có lẽ CPU đang tốn thời gian để sắp xếp và tìm kiếm, một mình chứng của việc mysql đang phải tìm với 1 lượng dữ liệu lớn. 70000 không phải con số to, do vậy chỉ có thể là máy chủ X đang phải tìm kiếm mà không có chỉ mục (index)!

“Không lẽ nào!”, vừa nói hắn vừa gõ lệnh

mysql> show index from table_name;

+-------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table       | Non_unique | Key_name          | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+-------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| table_name  |          0 | PRIMARY           |       1      | id          | A         |     73908   |     NULL | NULL   |      | BTREE      |         |
| table_name  |          0 | PRIMARY           |       2      | language    | A         |     73908   |     NULL | NULL   |      | BTREE      |         |
| table_name  |          1 | idx_table_name_1  |       1      | user_id     | A         |     24636   |     NULL | NULL   |      | BTREE      |         |
+-------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

Rất buồn, vậy là video_id có gắn index đàng hoàng. Vậy thì không có lý do gì mà query trên lại không query theo index cả. Thật kỳ lạ. Vậy để thử xem query trên có dùng index không nhé. Đoạn hắn lấy 1 query bất kỳ và thử EXPLAIN.

mysql> explain SELECT * FROM table_name WHERE `video_id` IN (1412240325) AND `language` = “en-us”\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: table_name
         type: ALL
possible_keys: PRIMARY,idx_table_name_2
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 66870
        Extra: Using where
1 row in set (0.00 sec)

ERROR:
No query specified

key: NULL nghĩa là query trên không sử dụng index! Tại sao bảng có chỉ mục mà query lại không dùng index. Chắc chắn là video_id có vấn đề rồi. Vừa nghĩ hắn vừa gõ câu lệnh show create table để xem kiểu dữ liệu lúc tạo bảng.

mysql> show create table table_name;
table_name | CREATE TABLE `table_name` (
  `video_id` varchar(34) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `language` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `user_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`video_id`,`language`),
  KEY `idx_videotranslationinfo_1` (`user_id`),
  KEY `idx_videotranslationinfo_2` (`video_id`),
  KEY `idx_videotranslationinfo_3` (`language`),
  ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='Latest translation for videos.' |

Có gì đó không ổn. Query thì coi video_id như là kiểu số nguyên, trong khi bảng lại định nghĩa video_id kiểu xâu dữ liệu. Có lẽ việc khác nhau trong kiểu dữ liệu này làm mysql không so sánh được truy cập với index, làm cho mysql sẽ tìm bản ghi bằng cách lặp toàn bộ bảng. Suy nghĩ vậy, hắn liền thử explain 1 query sau khi đã thay số bằng chữ.

mysql> explain SELECT * FROM table_name WHERE `video_id` IN (“1412240325”) AND `language` = “en-us”\G;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: table_name
         type: ALL
possible_keys: PRIMARY,idx_table_name_2
          key: PRIMARY
      key_len: NULL
          ref: NULL
         rows: 66870
        Extra: Using where
1 row in set (0.00 sec)

ERROR:
No query specified

“Ồ la la” hắn khẽ reo lên.

Sau khi đổi video_id thành kiểu chuỗi thì index đã được sử dụng key: PRIMARY. Hắn ngay lập tức liên lạc với bên phát triển và để sửa đoạn code sinh ra query trên. Bên phát triển lập tức tìm ra có 1 dòng code chưa gọi strval để biến video_id thành xâu dữ liệu trước ném query cho DB. Bên phát triển lập tức sửa source code và cập nhật phiên bản mới nhất lên máy chủ. Ngay lập tức %cpu của X trở về 1%. Trang web lại vào bình thường như chưa từng có gì cản trở. Slow log query cũng dừng log query hẳn.

Bài học

Index thật quan trọng và Kiểu dữ liệu cũng rất quan trọng.

Hắn khoái trí khi phát hiện ra hiểu ra được thêm 1 nguyên lý hoạt động của mysql cũng như ảnh hưởng của máy chủ X lên toàn bộ hệ thống. Đôi khi chỉ 1 mặt xích sai sót trong cả 1 dây chuyền có thể phá huỷ toàn bộ dây chuyền - hắn lờ mờ suy nghĩ và ngủ gục. Giờ là 4h sáng.

Comments

1. Lời nói đầu

Là một lập trình viên, tôi có thể khẳng định một điều là tôi ghét quảng cáo! Và tôi chắc chắn 90% các bạn cũng ghét quảng cáo như tôi. Bằng chứng là bạn đang cài adblock extension cho chrome hoặc firefox, hay là chúng ta hay đem hình tượng ‘kangaroo’ (một quảng cáo đã trở thành hiện tượng khi phát vào chung kết C1 năm 2011) là một hình tượng cho sự ‘xấu xa’, ‘phiền phức’ của quảng cáo.

Chúng ta cũng hay dùng quảng cáo như là thước đo cho chất lượng của một kênh truyền hình hay là trang web, ví dụ như: ‘vtv3 dạo này toàn quảng cáo’, hay là ‘trang web abcxyz toàn quảng cáo, lừa đảo đó!’.

Vậy lý do làm sao chúng ta lại ghét quảng cáo đến vậy? Có lẽ nguyên nhân lớn nhất là chúng cản trở chúng ta đến với dịch vụ (che tầm nhìn của trang web hay ti vi), và chúng hiển thị những thông tin mà chúng ta coi là dư thừa, không cần thiết. Tuy nhiên có phải vì thế mà quảng cáo chỉ toàn điều xấu và không đáng tồn tại?

Tuy nhiên, hãy nhìn vào mặt tốt của quảng cáo một chút

  • Quảng cáo giúp những người làm sản phẩm có cơ hội được khách hàng biết đến. Nếu không có quảng cáo thì những sản phẩm chưa được biết đến hầu như không có cơ hội ‘ngoi lên’ trên thị trường.
  • Quảng cáo giúp người tiêu dùng gặp được những sản phẩm có ích (mặc dù theo cách rất tình cờ)
  • Quảng cáo giúp cho các trang web miễn phí, các nhà phát triển app trên điện thoại miễn phí có nguồn doanh thu để tạo ra các sản phẩm có ích cho chúng ta dùng.

Vậy nếu nhìn theo những hướng tích cực này thì quảng cáo không hề xấu, chỉ có cách thức tiến hành quảng cáo tồi đã gây nên những hình ảnh thiếu tích cực với quảng cáo. Vậy với tư cách là người tiêu dùng, chúng ta cần một hệ thống quảng cáo thông minh hơn, mà không cản trở, cũng như cung cấp những thông tin quảng cáo có ích, phù hợp hơn đúng không?

Thực tế thì hệ thống quảng cáo đã và đang thay đổi hàng ngay để thông minh hơn, đến đúng người dùng hơn. Trong bài viết này tôi sẽ giới thiệu qua về hệ thống quảng cáo trên internet nói chung, về khái niệm cũng như cách thức vận hành của chúng. Qua đó bạn sẽ hiểu tại sao google lại chỉ có thể sống được mà chỉ nhờ có quảng cáo, bạn cũng hiểu được tại sao facebook, twitter sẵn sàng cung cấp dịch vụ miễn phí cho bạn. Để bắt đầu, trước tiên chúng ta sẽ đến với các thuật ngữ chuyên môn được sử dụng trong hệ thống quảng cáo.

2. Các thuật ngữ

Do hệ thống quảng cáo ở Việt Nam còn khá non nớt và thô sơ, nên các thuật ngữ trong ngành ít được phổ biến rộng rãi bằng tiếng Việt, do đó ở dưới đây tôi sẽ nói về các thuật ngữ bằng tiếng Anh.

Thuật ngữ chung

  • Media (hay còn gọi là publisher): media bạn có thể hiểu là các trang web (vd như vnexpress), hay các mobile application (vd như flappy bird). Media là nơi có nhiều user tập trung, và cũng là nơi để đặt quảng cáo.
  • Advertiser: Là những người nắm(own) quảng cáo. Advertiser bạn có thể hình dung là các doanh nghiệp muốn đưa hình ảnh của mình đến người dùng, vd như cocacola, piagio, adidas…
  • Click: Là một trong những đơn vị để tính đơn giá của quảng cáo. Khi người dùng ‘click’ vào một banner quảng cáo của advertiser, advertiser sẽ phải trả tiền cho click đó (trả tiền cho ‘ai’ thì chúng ta sẽ hiểu được ở các phần tiếp theo)
  • Impression: Cũng là một đơn vị để tính đơn giá của quảng cáo. 1 impression có thể hiểu đơn giản là 1 ‘view’, tức là khi 1 user ‘nhìn’ thấy một quảng cáo, advertiser sở hữu quảng cáo đó sẽ phải trả tiền.
  • Conversion: Khi người dùng nhìn quảng cáo, bấm vào trang web của advertiser, mua hàng hay trở thành khách hàng của advertiser, toàn bộ quá trình đó được gọi là ‘converse’, do đó chỉ số conversion(CV) được dùng để ám chỉ độ hiệu quả của quảng cáo.
  • Conversion Rate(CVR): Là tỉ lệ converse chia cho số lượng truy cập vào website của advertiser.
  • Click Per Cost(CPC): Nhà quảng cáo phải mất bao nhiêu tiền để có được 1 click của user.
  • Click Through Rate(CTR): Là số click / số impression. Chỉ số này cho thấy ‘độ hiệu quả’ của 1 quảng cáo.
  • Cost Per Acquisition(CPA): Là số tiền tốn để ‘kiếm’ được một khách hàng thực thụ (khách hàng trả tiền hay mua hàng của advertiser).
  • ROI(Return on Investment): Đây là số tiền để đánh giá độ hiệu quả của một ‘chiến dịch’ quảng cáo. Một chiến dịch quảng cáo advertiser có thể tung ra ở rất nhiều nơi, sử dụng rất nhiều banner khác nhau. ROI là số tiền thu được sau chiến dịch quảng cáo đó / số tiền advertiser đã đầu tư và chiến dịch đó.

Thuật ngữ về hệ thống kĩ thuật:

  • Ad-Network: là một ‘mạng lưới’ các media (các website hay các application). Sẽ có các công ty nắm các ad-network này. Đặc điểm của các công ty đó là họ sẽ có chiến lược để ‘phân phối’ (deliver) các quảng cáo đến các media thích hợp để tăng lợi nhuận cho họ cũng như bên media.
  • Ad-Exchange: Đây là một trong những bước tiến lớn để giúp quảng cáo thông minh hơn. Adexchange là một hệ thống nằm trung gian giữa Media và Advertiser, giúp ‘trao đổi’ quảng cáo giữa các advertiser. Bạn có thể hình dung Ad-Exchange giống như một sàn chứng khoán, mà những người chơi chứng khoán là các advertiser.
  • Realtime-Bidding: Là hệ thống đi kèm với Ad-Exchange, giúp việc ‘trao đổi’, ‘mua bán’ quảng cáo được diễn ra tại thời gian thực. Bạn có thể hình dung có một thời điểm có một cô gái xinh đẹp vừa mua hàng của LV cách đây 2 ngày (hệ thống quảng cáo biết được việc này thông qua cookie) vào trang web X. Tại ngày thời điểm cô gái request đến X, X sẽ tung ra một món hàng là impression của cô gái này. Các advertiser của NineWest hay H&M sẽ đấu giá với nhau để mua impression của cô gái (và tất nhiên là ai chịu chơi hơn sẽ thắng). Người chiến thắng sẽ được hệ thống ‘vận chuyển’ quảng cáo của mình trở lại X, và có được impression của cô gái. Hệ thống giúp đấu giá nói trên chính là RTB.
  • DSP (Demand Side Platform): Là một hệ thống được sử dụng bởi các advertisers. Bạn có thể hình dung advertiser sử dụng DSP như một hệ thống cung cấp cánh của để bước vào thế giới của Ad-network và Ad-Exchage. DSP giúp quản lý một lúc nhiều Ad-network, Ad-exchange, giúp advertiser thống nhất cùng một user ở các media khác nhau (thông qua cookie), giúp advertiser sử dụng RTB và còn rất nhiều chức năng thông minh khác.
  • SSP (Supply Side Platform): Khác với DSP ở phía ‘gần’ với advertiser hơn, SSP ở phía gần với media hơn. SSP giúp media quản lý nhiều Ad-Network, Ad-Exchange khác nhau, giúp cho media có thể tăng lợi nhuận cho mình. Mỗi khi có một user truy cập vào media, media sẽ sử dụng SSP để chọn ra quảng cáo có giá trị tiền cao nhất. Giống như DSP, SSP là cách cửa giúp media bước vào thế giới của Ad-Network, Ad-Exchange.
  • Retargeting: Đây là một kĩ thuật được sử dụng rất phổ biến trong quảng cáo. Giả sử có một cô gái xinh đẹp vào trang web của LV, ngẵm nghĩa một chiếc túi xách, xong do không có tiền, cô ấy rời khỏi trang web mất :(. Vài ngày sau, cô gái vào trang tin tức Y, đột nhiên thấy quảng cáo về chiếc túi xách cô ấy mong ước, rất may cô ấy đã nhận lương. Và nhờ thế cô ấy đã click vào quảng cáo của LV, và đôi bên cùng có lợi. Kĩ thuật giúp thực hiện quá trình tren được gọi là Re-targeting.

Như vậy chúng ta đã có các khái niệm hết sức cơ bản về các thuật ngữ sử dụng trong quảng cáo, để có cái nhìn đầu tiên cho các bài viết tiếp theo. Trong phần tiếp chúng ta sẽ tiếp tục nói về:

  • Phân loại hệ thống quảng cáo
  • Kiến trúc hệ thống quảng cáo
Comments

bài viết lần trước, tôi đã nói về “hoàn cảnh” tại sao tôi lại cần sử dụng openssl trên android native, đồng thời cũng đã giới thiệu qua về cách sử dụng ndk. Ở bài viết lần này tôi sẽ nói nốt phần còn lại về cách sử dụng openssl trên android ndk. Thông qua bài viết các bạn đồng thời có thể nắm được thêm về cách sử dụng openssl nói chung, cũng như các tiện ích mà openssl mang lại.

Giới thiệu về openssl

OpenSSL là một bộ thư viện/tiện ích dùng trong mã hoá (cryptography) viết bằng C, open source, và được sử dụng rất rộng rãi trên rất nhiều các phần mềm. OpenSSL cung cấp hầu hết các thuật toán mã hoá nổi tiếng như AES, RSA cũng như các thuật toán hash quan trọng như MD5, SHA1.

Như cái tên của nó, OpenSSL được sinh ra chủ yếu để hỗ trợ cho việc truyên tin qua internet một cách bảo mật thông qua SSL (Secure Socket Layer) và TLS (Transport Layer Security), mà ví dụ rõ ràng nhất là việc sử dụng trên các browser hay là các web server để dành cho các kết nối https.

Tuy nhiên OpenSSL vẫn được sử dụng rộng rãi trong nhiều hoàn cảnh khác nhau, ví dụ như khi bạn chỉ cần tính giá trị SHA1 hash, hay là muốn sử dụng một số thuật toán mã hoá đối xứng như là AES hay DES cho các ứng dụng yêu cầu về tốc độ và thực hiện đơn giản.

Trong thực tế OpenSSL được sử dụng rất nhiều, ví dụ như trong git, để tính giá trị HMAC khi nhận message thông qua imap, git sẽ sử dụng openssl trong trường hợp máy client có cài đặt sẵn bộ thư viện openssl:

https://github.com/git/git/blob/97b8860c071898d9e162678ea1035a8ced2f8b1f/imap-send.c#L861

Như vậy chúng ta có thể hình dung openssl là bộ thư viện (có thể gọi là qui chuẩn) dành để làm các công việc liên quan đến mã hoá.

Cài đặt và sử dụng openssl trên android native

OpenSSL là một bộ thư viện viết bằng C, còn android bản chất là hệ điều hành linux. Do đó việc cài đặt OpenSSL trên Android các bạn có thể hình dung tương tự như cài đặt một thư viện trên linux, cũng có make, có build, có copy file thư viện vào các đường dẫn cần thiết.

OpenSSL là một thư viện đồ sộ và khá phức tạp để build. Tuy nhiên rất may mắn là những người phát triển OpenSSL đã bỏ thời gian ra làm cho chúng ta một bản hướng dẫn cực kì đầy đủ để build từ source code và sử dụng trên android. Các bạn có thể tham khảo ở đường dẫn dưới đây:

http://wiki.openssl.org/index.php/Android

Làm theo hướng dẫn trên sẽ giúp các bạn tạo ra được 2 file (libcrypto.so libssl.so) hoặc (libcrypto.a libssl.a) tuỳ theo setting lúc build. File .so và file .a là các file thư viện động và tĩnh, mà các hàm trong các thư viện đó có thể được gọi trực tiếp từ C code. Cả .so và .a file đều có thể được gọi dễ dàng chỉ bằng việc thay đổi ndk make file. Do bản chất của ndk như đã trình bày ở phần 1, từ android OS muốn gọi được logic từ C code phải thông qua JNI interface, chúng ta có thể hình dung được qui trình để sử dụng openssl trên ndk theo từng bước như sau:

    1. Code Logic sử dụng openssl trên C, sử dụng JNI để “public” các hàm cần thiết sử dụng openssl ra ngoài.
    1. Sử dụng file code ở trên, build ra các file thư viện native để có thể gọi được từ java code.
    1. Gọi logic sử dụng openssl từ java code.

Ở dưới đây chúng ta sẽ lần lượt đi từng bước ở trên. Đầu tiên sẽ là việc quan trọng nhất là sử dụng openssl trên C ra sao.

Sử dụng openssl

OpenSSL thường được sử dụng dưới dạng “utility” trên unix system, tức là bạn sẽ gọi thông qua command line, ví dụ như sau:

1
openssl sha1 -out digest.txt file.txt

Dòng lệnh trên ở trên console sẽ được sử dụng để tính hash của nội dung file digest.txt theo thuật toán SHA1, và ghi nội dung của hash vào file file.txt.

Tuy nhiên bài toán của chúng ta ở đây là cần sử dụng openssl trong “code” chứ không phải thông qua command line. Việc sử dụng openssl trong code phức tạp hơn khá nhiều so với command line. Lý do là các thuật toán mã hoá đều khá phức tạp, và để sử dụng trong code thì đòi hỏi hiểu biết về thuật toán mã hoá đang sử dụng sâu hơn. Trong bài toán như tôi đã trình bày trong phần 1, chúng ta sẽ implement một thuật toán mã hoá đối xứng thông qua openssl. Do đó trước khi bắt tay vào coding, chúng ta hãy tìm hiểu sơ qua về thuật toán mã hoá đối xứng.

Sơ qua về thuật toán mã hoá đối xứng

Ở phần 1 đã nói sơ qua về thế nào là mã hoá đối xứng. Một cách đơn giản, thuật toán mã hoá đối xứng là khi bên gửi và bên nhân sẽ dùng cùng một key, cùng một initialize vector

Thuật toán mã hoá đối xứng chia làm 2 loại chính: block cipherstream cipher.

  • Block cipher là chia dữ liệu ra thành nhiều block nhỏ, mỗi block có độ dài cố định (128bit, 256bit..) N, sau đó từng block sẽ được mã hoá riêng biệt. Nếu dữ liệu có độ dài không chia hết cho N, thì đoạn dữ liệu thừa ra sẽ được thêm vào một chuỗi ngẫu nhiên để cho bằng độ dài của N rồi cũng được tiến hành mã hoá.
  • Stream cipher thì đơn giản hơn, đầu tiên một khoá (keystream)sẽ được tạo ra ngẫu nhiên. Sau đó dữ liệu sẽ đơn giản là được XOR với khoá đó để cho ra chuỗi mã hoá.

Stream cipher thì sẽ có tốc độ nhanh hơn rất nhiều so với Block cipher, tuy nhiên vì chỉ đơn giản thực hiện phép XOR sẽ làm Stream cipher có một số thuộc tính làm nó trở nên kém an toàn hơn so với Block cipher. Do đó trong bài toán lần này chúng ta sẽ sử dụng Block cipher.

Block cipher có khá nhiều “mode”. Mỗi “mode” có thể hiểu là các cách thức tiến hành mã hoá khác nhau. Cơ bản thì sẽ có 4 loại mode dưới đây:

  • ECB (Electronic Code Book): Ở mode này, 1 block của dữ liệu ban đầu (plaintext) sẽ được mã hoá thành 1 block của dữ liệu sau mã hoá (ciphertext). Mode này không tốt ở điểm dễ bị tấn công bởi dictionary attack, và là mode kém an toàn nhất
  • CBC (Cipher Block Chaining) Mode này giải quyết điểm yếu dictionary attack của mode ECB thông qua việc tiến hành XOR ciphertext của block phía trước với plaintext của block tiếp theo. Việc này được tiến hành liên tiếp cho đến khi ra kết quả cuối cùng. Từ đặc điểm là việc mã hoá được tiến hành liên tiếp, chúng ta có thể thấy cần một chuỗi ngẫu nhiên để tiến hành XOR với block đầu tiên. Chuỗi đó được gọi là initialization vector (IV).
  • CFB (Cipher Feedback) và OFB (Output Feedback) : 2 mode này dùng để biến từ block cipher thành stream cipher, do đó thường ít được sử dụng trong thực tế.

Ở bài toán của chúng ta, có thể thấy rằng CBC mode là lựa chọn tốt nhất. Việc tiếp theo là lựa chọn thuật toán mã hoá.

Có thể kể ra một vài thuật toán mã hoá đối xứng, sử dụng BlockCipher tiêu biểu gồm có : AES, BlowFish, DES, TripleDES. Trong đó AES (Advanced Encryption Standard) là thuật toán được tạo ra gần đây, có thể sử dụng key và độ dài block lên tới 256 bit. AES được chính phủ Mĩ sử dụng làm tiêu chuẩn mã hoá, và là một thuật toán mã hoá đã được nghiên cứu rất kỹ lưỡng trong vòng 5 năm. Do vậy mà so với các thuật toán còn lại như Blowfish hay DES, AES đảm bảo được độ an toàn cao hơn. Trong lần này chúng ta sẽ sử dụng AES 256 bit, trên CBC mode.

Openssl thông qua EVP interface

Như chúng ta đã thấy ở trên, mỗi loại thuật toán mã hoá, mỗi mode đều có những con đường (routines) khác nhau để thực hiện. Do đó nếu mỗi con đường đó được thực hiện với những interface khác nhau sẽ rất khó nhớ và khó để thực hiện. Rât may mắn, OpenSSL cung cấp sẵn cho chúng ta một interface thống nhất cho một loạt các thuật toán mã hoá khác nhau, gọi là EVP. Thông qua EVP thì qui trình mã hoá trở nên rất đơn giản thông qua việc gọi lần lượt các hàm của EVP. Để tiến hành mã hoá

1
2
3
4
EVP_CIPHER_CTX_new  //tạo EVP context
EVP_EncryptInit_ex  //Khởi tạo việc mã hoá
EVP_EncryptUpdate   //Tiến hành mã hoá
EVP_EncryptFinal_ex //Trong trường hợp có sử dụng padding, tức là thêm dữ liệu vào cuối plaintext cho đủ chiều dài chia hết cho độ dài block, thì bước này dùng để mã hoá "nốt" đoạn dữ liệu được padding đó. Bước này được dùng để kết thúc quá trình mã hoá

Để tiến hành giải mã chúng ta cũng dùng các hàm gần tương tự, chỉ thay Encrypt bằng Decrypt

1
2
3
4
EVP_CIPHER_CTX_new
EVP_DecryptInit_ex
EVP_DecryptUpdate
EVP_DecryptFinal_ex

Coding

Sử dụng những kiến thức đã được nói ở phần trên, chúng ta đã có thể tiến hành coding. Một đoạn sample code sử dụng openssl để mã hoá đối xứng theo AES 256bit được mô tả như dưới đây. Chúng ta sẽ đặt tên file dưới đây là security.c:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/des.h>
#include <openssl/rand.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <openssl/err.h>
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/sha.h>
#include <jni.h>
#include <android/log.h>

#define BUFSIZE 64
void handleErrors() {
  return;
}

int encrypt(unsigned char *plaintext, int plaintext_len, unsigned char *key,
  unsigned char *iv, unsigned char *ciphertext)
{
  EVP_CIPHER_CTX *ctx;

  int len;

  int ciphertext_len;

  if(!(ctx = EVP_CIPHER_CTX_new())) handleErrors();
  if(1 != EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
    handleErrors();

  if(1 != EVP_EncryptUpdate(ctx, ciphertext, &len, plaintext, plaintext_len))
    handleErrors();
  ciphertext_len = len;

  if(1 != EVP_EncryptFinal_ex(ctx, ciphertext + len, &len)) handleErrors();
  ciphertext_len += len;

  /* Clean up */
  EVP_CIPHER_CTX_free(ctx);

  return ciphertext_len;
}

int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *key,
  unsigned char *iv, unsigned char *plaintext)
{
  EVP_CIPHER_CTX *ctx;

  int len;

  int plaintext_len;

  if(!(ctx = EVP_CIPHER_CTX_new())) handleErrors();

  if(1 != EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), NULL, key, iv))
    handleErrors();

  if(1 != EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
    handleErrors();
  plaintext_len = len;

  if(1 != EVP_DecryptFinal_ex(ctx, plaintext + len, &len)) handleErrors();
  plaintext_len += len;

  /* Clean up */
  EVP_CIPHER_CTX_free(ctx);

  return plaintext_len;
}

char *base64(const unsigned char *input, int length)
{
  BIO *bmem, *b64;
  BUF_MEM *bptr;

  b64 = BIO_new(BIO_f_base64());
  bmem = BIO_new(BIO_s_mem());
  b64 = BIO_push(b64, bmem);
  BIO_write(b64, input, length);
  BIO_flush(b64);
  BIO_get_mem_ptr(b64, &bptr);

  char *buff = (char *)malloc(bptr->length);
  memcpy(buff, bptr->data, bptr->length-1);
  buff[bptr->length-1] = 0;

  BIO_free_all(b64);

  return buff;
}

jstring Java_jp_co_common_android_libs_CryptUtils_stringFromJNI(JNIEnv* env, jobject thiz, jstring uuid) {
    char *plaintext = (*env)->GetStringUTFChars(env, uuid, 0);

    unsigned char ciphertext[1024];
    unsigned char *key = "11111111111111111111111111111111";
    unsigned char *iv = "2222222222222222";

    int ciphertext_len = encrypt(plaintext, strlen(plaintext), key, iv, ciphertext);
    __android_log_print(ANDROID_LOG_INFO, "kimisaki", "ndk: %s", base64(ciphertext, ciphertext_len));
    (*env)->ReleaseStringUTFChars(env, uuid, plaintext);
    return (*env)->NewStringUTF(env, base64(ciphertext, ciphertext_len));
}

Ngoài việc sử dụng các kiến thức đã nói ở trên, chúng ta có thể chú ý thấy một số điểm đặc biệt ở đoạn code trên:

  • Chúng ta phải include đầy đủ các file header cần thiết của openssl như ..
  • Có thể để ý thấy việc sử dụng Base64 để encode dữ liệu trả về phía android. Lý do là sau khi mã hoá thì plaintext ban đầu sẽ trở thành 1 chuỗi bit vô nghĩa, và việc encode thành Base64 sẽ giúp dữ liệu dễ để truyền qua lại hơn, và cũng dễ debug hơn. Cách sử dụng base64 qua BIO interface các bạn có thể tìm hiểu thông qua trang chủ của openssl.
  • Việc chọn độ dài cho key và IV là vô cùng quan trọng. Chọn sai độ dài cho key và IV sẽ dẫn đến các kết quả mã hoá không lường trước được và sẽ gây ra việc giải mã ra kết quả sai. Với AES 256 thì key sẽ có độ dài là 32 bytes, còn iv phải có độ dài là 16 bytes.

Kết hợp với android

Như vậy là chúng ta đã tiến hành xong công đoạn coding. Công đoạn tiếp theo không kém phần quan trọng là việc phải build được đoạn code đó thành thư viện native để sử dụng trên android OS. Để làm được việc đó chúng ta cần làm:

  • Tổ chức cấu trúc folder sao cho hợp lý.
  • Viết make file
  • Build

Cấu trúc folder theo như bài viết lần đầu, chúng ta sẽ tạo 1 folder jni ở project$ROOT. Trong đó sẽ được sắp xếp như sau

Chúng ta có thể thấy điểm đặc biêt ở đây là thư mục libprebuilt sẽ chứa các file .so của openssl được build cho từng platform khác nhau. Hiện tại android có thể chạy trên ARM(armeabi), Intel(x86) và MIPS. Do việc build ra thư viện .so từng platform khác nhau có thể gặp khá nhiều khó khăn nên chúng ta có thể làm theo 1 cách đơn giản hơn, đó là kiếm các file .so “có sẵn” của từng platform và copy vào đây, thay vì phải build tử source. Các file này có thể kiếm được dễ dàng từ bản phân phối của các image của android OS.

Một điểm nữa cần lưu ý là chúng ta cần copy các file header cần sử dụng của openssl vào trong thư mục dự án thì mới include được.

2 file make để build native source sẽ có nội dung như sau

  • Android.mk
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    AL_PATH := $(call my-dir)
    
    # Prebuilt libssl
    include $(CLEAR_VARS)
    LOCAL_MODULE := ssl
    LOCAL_SRC_FILES := libprebuilt/$(TARGET_ARCH_ABI)/libssl.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    # Prebuilt libcrypto
    include $(CLEAR_VARS)
    LOCAL_MODULE := crypto
    LOCAL_SRC_FILES := libprebuilt/$(TARGET_ARCH_ABI)/libcrypto.so
    include $(PREBUILT_SHARED_LIBRARY)
    
    include $(CLEAR_VARS)
    
  • Application.mk
    1
    
    APP_ABI := all
    

Chúng ta có thể chú ý thấy điểm đặc biệt ở Android.mk. Trong make file này chúng ta sẽ thấy việc chỉ định các biến build PREBUILT_SHARED_LIBRARY, LOCAL_SRC_FILE, LOCAL_MODULE để hệ thống build của ndk có thể nhận đưọc sự tồn tại của các file .so và copy vào các folder cần thiết để gọi được sau trên java code.

Để tiến hành build thì chúng ta chỉ cần vào thư mục dự án và gõ

1
ndk-build

Sau khi tiến hành build thì trong thư mục /libs sẽ có các thư mục tương ứng với các platform được tạo ra, và các file .so cần thiết sẽ được copy vào trong đó. File security.c ở trên sẽ được build thành các file security.so tương ứng.

Tiếp theo chỉ còn là vấn đè sử dụng các file .so trên java code:

1
2
3
4
5
6
7
8
9
public class Foo {
   public native static String stringFromJNI(String input);

   static {
       System.loadLibrary("ssl");
       System.loadLibrary("crypto");
       System.loadLibrary("security");
   }
}

Chỉ với các chỉ định như trên thì chúng ta đã có thể sử dụng được hàm stringFromJNI được code trong security.c. Khi truyền vào 1 chuỗi bất kỳ, thì chúng ta sẽ nhận được kết quả mã hoá của chuỗi đó theo AES 256bit, với key và iv được qui định trong security.c. Vậy là bài toán của chúng ta đã được giải quyết :D.

Kết luận

Qua hai bài viết tương đối đầy đủ, hy vong các bạn đã nắm được:

  • Cách cài đặt, sử dụng và bản chất của android ndk
  • Sơ qua về mã hoá đối xứng
  • Sơ qua về OpenSSL, cách sử dụng trực tiếp trên C code và cách để intergrate với android ndk

Tham khảo:

Copyright © 2015 kỹ thuật máy tính