Xác thực kiểu Digest

  • Xác thực kiểu Basic dùng base64 để mã hóa thông tin. Nó không an toàn vì có thể giải mã dễ dàng.
  • Xác thực kiểu Digest dùng băm với mã hóa MD5 và có thêm giá trị nonce để ngăn chặn các cuộc tấn công dò tìm lặp đi lặp lại.

Ngày nay, xác thực digest được dùng phổ biến. Thí dụ khi lấy ảnh snapshot của camera với username/password.

Xác thực Digest phải qua 2 bước:

1. Client gởi thử yêu cầu đến webserver. Webserver sẽ trả lời với header tương tự như sau

WWW-Authenticate: Digest realm="testrealm@host.com",
 qop="auth,auth-int",
 nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
 opaque="5ccc069c403ebaf9f0171e9517f40e41"

2. Client gởi tiếp yêu cầu có username/password trong header tương tự như sau

Authorization: Digest username="Mufasa",
  realm="testrealm@host.com",
  nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
  uri="/dir/index.html",
  qop=auth,
  nc=00000001,
  cnonce="0a4f113b",
  response="6629fae49393a05397450978507c4ef1",
  opaque="5ccc069c403ebaf9f0171e9517f40e41"

trong đó giá trị nonce opaque được cho bởi server trong lần yêu cầu trước, riêng giá trị responce thì client phải tính toán dựa trên:

  1. MD5 của username, authentication realmpassword. Kết quả gọi là HA1.
  2. MD5 của method URI, thí dụ “GET” và “/dir/index.html“. Kết quả gọi là HA2.
  3. MD5 của server nonce (nonce), request counter (nc), client nonce (cnonce), quality of protection code (qop) và HA2.
HA1 = MD5( "Mufasa:testrealm@host.com:Circle Of Life" )
// --> 939e7578ed9e3c518a452acee763bce9

HA2 = MD5( "GET:/dir/index.html" )
// --> 39aff3a2bab6126f332b942af96d3366

Response = MD5( "939e7578ed9e3c518a452acee763bce9:\
                 dcd98b7102dd2f0e8b11d0f600bfb0c093:\
                 00000001:0a4f113b:auth:\
                 39aff3a2bab6126f332b942af96d3366" )
// --> 6629fae49393a05397450978507c4ef1

Thư viện Nodejs cần cho việc mã hóa MD5 là crypto, có sẵn không cần cài đặt.

//Digest access authentication
//digest.js - version 20220220"
//LNT <lnt@ly-le.info>

var http = require("http")
  , https = require("https")
  , crypto = require('crypto')
  , {URL} = require('url')

class httpDigest {
  constructor (fullUrl) {
    this.url = new URL(fullUrl)
  }
  request (callback) {
    let request_method = this.url.protocol == 'https:' ? https.request : http.request
    request_method (this.url, (res) => {
      if (res.statusCode === 401 && res.headers['www-authenticate']) {
        let wwwAuthenticate = res.headers["www-authenticate"]
        this._handle_response (wwwAuthenticate, callback)
      }
      callback(res)
    }).on("error", (e) => {
      console.error(e);
    }).end();
  }

  _handle_response (wwwAuthenticate, callback) {
    let headers = {}
    headers.Authorization = this.digestAuthHeader(wwwAuthenticate)
    let request_method = this.url.protocol == 'https:' ? https.request : http.request
    request_method (this.url, { headers: headers }, (res) => {
      callback(res)
    }).on("error", (e) => {
      console.error(e);
    }).end()
  }

  digestAuthHeader(wwwAuth) {
    let nc = '00000001'
    wwwAuth = wwwAuth.replace(/^Digest /,'')
    let parts = wwwAuth.split(",");
    let length = parts.length;
    let params = {};
    for (let i = 0; i < length; i++) {
      let part = parts[i].match(/^\s*(\w+)=['"]([^'"]+)['"]\s*$/);
      if (part) {
        params[part[1]] = part[2];
      }
    }
    if (!params.realm || !params.nonce) return ''
    let qop = params.qop || ''
    if (qop) qop = qop.split(',')[0]
    let cnonce = crypto.randomBytes(8).toString('hex')
    let ha1 = crypto.createHash("md5").update([this.url.username, params.realm, this.url.password].join(":")).digest("hex")
    let ha2 = crypto.createHash("md5").update(['GET', this.url.pathname + this.url.search].join(":")).digest("hex")
    let response = crypto.createHash("md5").update([ha1, params.nonce, nc, cnonce, qop, ha2].join(":")).digest("hex")
    let authStr = 'Digest username="' + this.url.username + '", realm="' + params.realm
      + '", nonce="' + params.nonce + '", uri="' + this.url.pathname + this.url.search
      + '", response="' + response + '"'
    if (params.opaque) authStr += ', opaque="' + params.opaque + '"'
    if (qop) authStr +=', qop=' + qop + ', nc=' + nc + ', cnonce="' + cnonce + '"'
    return authStr
  }
}

module.exports = httpDigest

Sử dụng module httpDigest để lấy snapshot từ camera IMOU như sau:

const digest = require('./lib/digest.js');
    , fs = require('fs')

let fullUrl = 'http://admin:<password>@<cam_ip>:80/onvifsnapshot/media_service/snapshot?channel=1&subtype=0'
new digest(fullUrl).request((res) => { res.pipe(fs.createWriteStream("snapshot.jpg"))})

Tuy nhiên, có thể dùng các cách khác để lấy snapshot

// dùng wget
childProcess.spawn('wget', ['-q', '-b', '-O', filename, fullUrl], {detached: false, stdio: 'ignore'})

// hay dùng curl
childProcess.spawn('curl', ['-s', '--digest', '-o', filename, fullUrl], {detached: false, stdio: 'ignore'})

trong đó, việc dùng wget cho tốc độ nhanh nhất, kế đến là curl và sau cùng là digest.js

Cũng đúng thôi, chạy ứng dụng nhị phân phải nhanh hơn javascript, tuy chênh lệch không nhiều. httpDigest còn dùng trong các trường hợp khác thuần javascript.

Comments Off on Xác thực kiểu Digest

Filed under Software

Comments are closed.