diff --git a/src/kryptonbutterfly/totp/misc/Utils.java b/src/kryptonbutterfly/totp/misc/Utils.java index 008aebe..b6844c3 100644 --- a/src/kryptonbutterfly/totp/misc/Utils.java +++ b/src/kryptonbutterfly/totp/misc/Utils.java @@ -6,8 +6,17 @@ import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; +import java.util.Map; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.MultiFormatWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import kryptonbutterfly.functions.bool_.BoolToIntFunction; import kryptonbutterfly.math.vector._int.Vec2i; +import kryptonbutterfly.monads.failable.Failable; +import kryptonbutterfly.monads.opt.Opt; public class Utils { @@ -70,4 +79,32 @@ public static final BufferedImage mirror(BufferedImage src) return result; } + + public static final Opt generateQr( + String data, + Color dark, + Color light, + int size, + ErrorCorrectionLevel level) + { + final BoolToIntFunction colorConverter = bit -> bit ? dark.getRGB() : light.getRGB(); + final var sizeRange = range(size); + + return Failable.attempt( + () -> new MultiFormatWriter().encode( + data, + BarcodeFormat.QR_CODE, + size, + size, + Map.of(EncodeHintType.ERROR_CORRECTION, level.name()))) + .toOpt(e -> e.printStackTrace()) + .map(matrix -> + { + final var image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + for (int y : sizeRange) + for (int x : sizeRange) + image.setRGB(x, y, colorConverter.apply(matrix.get(x, y))); + return image; + }); + } } \ No newline at end of file diff --git a/src/kryptonbutterfly/totp/prefs/TotpWindowStates.java b/src/kryptonbutterfly/totp/prefs/TotpWindowStates.java index 103961c..ec58336 100644 --- a/src/kryptonbutterfly/totp/prefs/TotpWindowStates.java +++ b/src/kryptonbutterfly/totp/prefs/TotpWindowStates.java @@ -20,4 +20,7 @@ public final class TotpWindowStates @Expose public GuiPrefs qrScan = new GuiPrefs(100, 100, 640, 400, JFrame.NORMAL); + + @Expose + public GuiPrefs qrExport = new GuiPrefs(100, 100, 230, 280, JFrame.NORMAL); } \ No newline at end of file diff --git a/src/kryptonbutterfly/totp/ui/add/manual/AddKey.java b/src/kryptonbutterfly/totp/ui/add/manual/AddKey.java index c372316..c6cbf38 100644 --- a/src/kryptonbutterfly/totp/ui/add/manual/AddKey.java +++ b/src/kryptonbutterfly/totp/ui/add/manual/AddKey.java @@ -1,6 +1,7 @@ package kryptonbutterfly.totp.ui.add.manual; import java.awt.BorderLayout; +import java.awt.FlowLayout; import java.awt.GridLayout; import java.awt.Window; import java.util.function.Consumer; @@ -31,6 +32,7 @@ @SuppressWarnings("serial") public final class AddKey extends ObservableDialog implements TotpConstants { + final JButton btnExport = new JButton("export"); final JTextField txtAccountname = new JTextField(); final JTextField txtSecretkey = new JTextField(); final JTextField txtIssuer = new JTextField(); @@ -119,6 +121,15 @@ private void init(BL bl) verticalBox.add(gridPanel); + final var exportPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + getContentPane().add(exportPanel, BorderLayout.NORTH); + { + exportPanel.add(btnExport); + btnExport.setIcon(Assets.getQr16ByBackground(getBackground())); + btnExport.addActionListener(bl::exportQR); + btnExport.addKeyListener(bl.escapeListener); + } + getContentPane().add(verticalBox, BorderLayout.CENTER); { gridPanel.add(new JLabel(LABEL_ACCOUNT_NAME)); diff --git a/src/kryptonbutterfly/totp/ui/add/manual/BL.java b/src/kryptonbutterfly/totp/ui/add/manual/BL.java index 8c36b94..4765219 100644 --- a/src/kryptonbutterfly/totp/ui/add/manual/BL.java +++ b/src/kryptonbutterfly/totp/ui/add/manual/BL.java @@ -1,5 +1,8 @@ package kryptonbutterfly.totp.ui.add.manual; +import java.awt.Color; +import java.awt.Dialog.ModalityType; +import java.awt.EventQueue; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; @@ -21,13 +24,18 @@ import org.apache.commons.codec.binary.Base32; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import kryptonbutterfly.functions.void_.Consumer_; import kryptonbutterfly.monads.opt.Opt; import kryptonbutterfly.totp.TinyTotp; import kryptonbutterfly.totp.TotpConstants; +import kryptonbutterfly.totp.misc.UrlQueryParams; import kryptonbutterfly.totp.misc.Utils; import kryptonbutterfly.totp.prefs.TotpCategory; import kryptonbutterfly.totp.prefs.TotpEntry; import kryptonbutterfly.totp.ui.misc.KeyTypedAdapter; +import kryptonbutterfly.totp.ui.qrexport.QrExportGui; import kryptonbutterfly.util.swing.Logic; import kryptonbutterfly.util.swing.events.GuiCloseEvent; import kryptonbutterfly.util.swing.events.GuiCloseEvent.Result; @@ -144,6 +152,20 @@ private void setIcon(AddKey gui, String name, BufferedImage img) gui.lblIcon.setToolTipText(name); } + void exportQR(ActionEvent ae) + { + gui.if_(gui -> { + final var account = gui.txtAccountname.getText(); + final var secretKey = gui.txtSecretkey.getText(); + final var issuer = gui.txtIssuer.getText(); + final var url = UrlQueryParams.toUrl(account, secretKey, issuer); + Utils.generateQr(url, Color.BLACK, Color.WHITE, 200, ErrorCorrectionLevel.H) + .if_( + qr -> EventQueue + .invokeLater(() -> new QrExportGui(gui, ModalityType.APPLICATION_MODAL, Consumer_.sink(), qr))); + }); + } + void abort(ActionEvent ae) { gui.if_(AddKey::dispose); diff --git a/src/kryptonbutterfly/totp/ui/qrexport/BL.java b/src/kryptonbutterfly/totp/ui/qrexport/BL.java new file mode 100644 index 0000000..e817bb9 --- /dev/null +++ b/src/kryptonbutterfly/totp/ui/qrexport/BL.java @@ -0,0 +1,30 @@ +package kryptonbutterfly.totp.ui.qrexport; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +import kryptonbutterfly.totp.TinyTotp; +import kryptonbutterfly.totp.ui.misc.KeyTypedAdapter; +import kryptonbutterfly.util.swing.Logic; + +final class BL extends Logic +{ + final KeyListener escapeListener = new KeyTypedAdapter(c -> ok(null), KeyEvent.VK_ESCAPE); + + BL(QrExportGui gui) + { + super(gui); + } + + void ok(ActionEvent ae) + { + gui.if_(QrExportGui::dispose); + } + + @Override + protected void disposeAction() + { + gui.if_(TinyTotp.windowStates.qrExport::persistBounds); + } +} diff --git a/src/kryptonbutterfly/totp/ui/qrexport/QrExportGui.java b/src/kryptonbutterfly/totp/ui/qrexport/QrExportGui.java new file mode 100644 index 0000000..d6c7357 --- /dev/null +++ b/src/kryptonbutterfly/totp/ui/qrexport/QrExportGui.java @@ -0,0 +1,57 @@ +package kryptonbutterfly.totp.ui.qrexport; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Window; +import java.awt.image.BufferedImage; +import java.util.function.Consumer; + +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import kryptonbutterfly.totp.TinyTotp; +import kryptonbutterfly.totp.misc.Assets; +import kryptonbutterfly.util.swing.ObservableDialog; +import kryptonbutterfly.util.swing.events.GuiCloseEvent; + +@SuppressWarnings("serial") +public class QrExportGui extends ObservableDialog +{ + private final JButton btnOk = new JButton("ok"); + + public QrExportGui( + Window owner, + ModalityType modality, + Consumer> closeListener, + BufferedImage qr) + { + super(owner, modality, closeListener); + TinyTotp.windowStates.qrExport.setBounds(this); + setTitle("Export Secret"); + setIconImage(Assets.getQr16ByBackground(getBackground()).getImage()); + + add(new JLabel(new ImageIcon(qr)), BorderLayout.CENTER); + + final var footerPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + add(footerPanel, BorderLayout.SOUTH); + footerPanel.add(btnOk); + + businessLogic.if_(this::init); + + setVisible(true); + } + + @Override + protected BL createBusinessLogic(Void args) + { + return new BL(this); + } + + private void init(BL bl) + { + btnOk.addActionListener(bl::ok); + btnOk.addKeyListener(bl.escapeListener); + } +}