From 9d6e248cae8f4815c2a17c76d099c33ac66387a8 Mon Sep 17 00:00:00 2001 From: Dag Heyman Date: Wed, 15 Feb 2017 10:13:57 +0100 Subject: [PATCH] Add QR parse logic --- py/qr/qrparse.py | 159 +++++++++++++++++++++++++++++++++++++++++++++++ py/yubikey.py | 29 ++++++--- screenshot.h | 2 +- 3 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 py/qr/qrparse.py diff --git a/py/qr/qrparse.py b/py/qr/qrparse.py new file mode 100644 index 00000000..3dd935bd --- /dev/null +++ b/py/qr/qrparse.py @@ -0,0 +1,159 @@ +""" +Given an image, locates and parses the pixel data in QR codes. +""" + +from __future__ import division +from collections import namedtuple + + +__all__ = ['parse_qr_codes'] + + +Box = namedtuple('Box', ['x', 'y', 'w', 'h']) + + +def is_dark(pixel): + return pixel == 1 + + +def buffer_matches(matched): + return len(matched) == 5 \ + and max(matched[:2] + matched[3:]) <= matched[2] // 2 \ + and min(matched[:2] + matched[3:]) >= matched[2] // 6 + + +def check_line(pixels): + matching_dark = False + matched = [0, 0, 0, 0, 0] + for (i, pixel) in enumerate(pixels): + if is_dark(pixel): # Dark pixel + if matching_dark: + matched[-1] += 1 + else: + matched = matched[1:] + [1] + matching_dark = True + else: # Light pixel + if not matching_dark: + matched[-1] += 1 + else: + if buffer_matches(matched): + width = sum(matched) + yield i - width, width + matched = matched[1:] + [1] + matching_dark = False + + # Check final state of buffer + if matching_dark and buffer_matches(matched): + width = sum(matched) + yield i - width, width + + +def check_row(line, bpp, x_offs, x_width): + return check_line(line[x_offs:x_offs+x_width]) + + +def check_col(image, bpp, x, y_offs, y_height): + return check_line(bytes([image.get_line(i)[x] + for i in range(y_offs, y_offs + y_height)])) + + +def read_line(line, bpp, x_offs, x_width): + matching_dark = not is_dark(line[x_offs*bpp:(x_offs+1)*bpp]) + matched = [] + for x in range(x_offs, x_offs + x_width): + pixel = line[x*bpp:(x+1)*bpp] + if is_dark(pixel): # Dark pixel + if matching_dark: + matched[-1] += 1 + else: + matched.append(1) + matching_dark = True + else: # Light pixel + if not matching_dark: + matched[-1] += 1 + else: + matched.append(1) + matching_dark = False + return matching_dark, matched + + +def read_bits(image, bpp, img_x, img_y, img_w, img_h, size): + qr_x_w = img_w / size + qr_y_h = img_h / size + qr_data = [] + for qr_y in range(size): + y = img_y + int(qr_y_h / 2 + qr_y * qr_y_h) + img_line = image.get_line(y) + qr_line = [] + for qr_x in range(size): + x = img_x + int(qr_x_w / 2 + qr_x * qr_x_w) + qr_line.append(is_dark(img_line[x])) + qr_data.append(qr_line) + return qr_data + + +FINDER = [ + [True, True, True, True, True, True, True], + [True, False, False, False, False, False, True], + [True, False, True, True, True, False, True], + [True, False, True, True, True, False, True], + [True, False, True, True, True, False, True], + [True, False, False, False, False, False, True], + [True, True, True, True, True, True, True] +] + + +def parse_qr_codes(image, min_res=2): + size = image.size() + bpp = image.bytesPerLine() // size.width() + + finders = locate_finders(image, min_res) + + # Arrange finders into QR codes and extract data + for (tl, tr, bl) in identify_groups(finders): + min_x = min(tl.x, bl.x) + min_y = min(tl.y, tr.y) + width = tr.x + tr.w - min_x + height = bl.y + bl.h - min_y + + # Determine resolution by reading timing pattern + line = image.scanLine(min_y + int(6.5 / 7 * max(tl.h, tr.h))) + _, line_data = read_line(line, bpp, min_x, width) + size = len(line_data) + 12 + + # Read QR code data + yield read_bits(image, bpp, min_x, min_y, width, height, size) + + +def locate_finders(image, min_res): + bpp = 1 + finders = set() + for y in range(0, image.height, min_res * 3): + for (x, w) in check_row(image.get_line(y), bpp, 0, image.width): + x_offs = x + w // 2 + y_offs = max(0, y - w) + y_height = min(image.height - y_offs, 2 * w) + match = next(check_col(image, bpp, x_offs, y_offs, y_height), None) + if match: + (pos, h) = match + y2 = y_offs + pos + if read_bits(image, bpp, x, y2, w, h, 7) == FINDER: + finders.add(Box(x, y2, w, h)) + + return list(finders) + + +def identify_groups(locators): + # Find top left + for tl in locators: + x_tol = tl.w / 14 + y_tol = tl.h / 14 + + # Find top right + for tr in locators: + if tr.x > tl.x and abs(tl.y - tr.y) <= y_tol: + + # Find bottom left + for bl in locators: + if bl.y > tl.y and abs(tl.x - bl.x) <= x_tol: + yield tl, tr, bl diff --git a/py/yubikey.py b/py/yubikey.py index 83fd5ea3..8fa7b3cb 100644 --- a/py/yubikey.py +++ b/py/yubikey.py @@ -1,18 +1,17 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import os import json import types import re -from base64 import b32decode +from base64 import b32decode, b64decode from binascii import a2b_hex, b2a_hex from ykman.descriptor import get_descriptors -from ykman.driver import ModeSwitchError -from ykman.util import CAPABILITY, TRANSPORT, Mode, derive_key +from ykman.util import CAPABILITY, TRANSPORT, derive_key from ykman.oath import OathController, Credential - +from py.qr import qrparse +from py.qr import qrdecode NON_FEATURE_CAPABILITIES = [CAPABILITY.CCID, CAPABILITY.NFC] @@ -64,7 +63,7 @@ class Controller(object): return self._dev_info def refresh_credentials(self, timestamp, password_key=None): - if password_key != None: + if password_key is not None: password_key = a2b_hex(password_key) return [c.to_dict() for c in self._calculate_all(timestamp, password_key)] @@ -97,7 +96,6 @@ class Controller(object): else: controller.clear_password() - def add_credential(self, name, key, oath_type, digits, algo, touch, password_key): dev = self._descriptor.open_device(TRANSPORT.CCID) controller = OathController(dev.driver) @@ -144,4 +142,21 @@ class Controller(object): return b32decode(key) + def parse_qr(self, image): + data = b64decode(image['data']) + image = PixelImage(data, image['width'], image['height']) + x = qrparse.locate_finders(image, 2) + return x + + +class PixelImage(object): + + def __init__(self, data, width, height): + self.data = data + self.width = width + self.height = height + + def get_line(self, line_number): + return self.data[self.width * line_number:self.width * (line_number + 1)] + controller = Controller() diff --git a/screenshot.h b/screenshot.h index 6a07acb2..a5dfc59f 100644 --- a/screenshot.h +++ b/screenshot.h @@ -19,7 +19,7 @@ public: image = image.convertToFormat(QImage::Format_Mono, Qt::ThresholdDither); // Iterate over all pixels - QByteArray imageArray(4 + 4 + (image.width() * image.height()), 0); + QByteArray imageArray; for (int row = 0; row < image.height(); ++row) { for (int col = 0; col < image.width(); ++col) { QRgb px = image.pixel(col, row);