Commit 6c2a6071 authored by Lorenzo Pichilli's avatar Lorenzo Pichilli

Added SslCertificate class and X509Certificate class and parser, updated ServerTrustChallenge class

parent b9d14828
## 3.4.0
- Added `requestFocusNodeHref`, `requestImageRef`, `getMetaTags`, `getMetaThemeColor`, `getScrollX`, `getScrollY` webview methods
- Added `requestFocusNodeHref`, `requestImageRef`, `getMetaTags`, `getMetaThemeColor`, `getScrollX`, `getScrollY`, `getCertificate` webview methods
- Added `WebStorage`, `LocalStorage` and `SessionStorage` class to manage `window.localStorage` and `window.sessionStorage` JavaScript [Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)
- Added `supportZoom` webview option also on iOS
- Added `HttpOnly`, `SameSite` set cookie options
......@@ -8,8 +8,8 @@
- Added `animated` option to `scrollTo` and `scrollBy` webview methods
- Added error and message to the `ServerTrustChallenge` class for iOS (class used by the `onReceivedServerTrustAuthRequest` event)
- Added `contentInsetAdjustmentBehavior` webview iOS-specific option
- Added `getCertificate` android-specific webview method
- Added `copy` and `copyWithValue` methods for webview class options
- Added X509Certificate class and parser
- Fixed `zoomBy`, `setOptions` webview methods on Android
- Fixed `databaseEnabled` android webview option default value to `true`
......@@ -20,7 +20,7 @@
- Moved `supportZoom` webview option to cross-platform
- `builtInZoomControls` android webview options changed default value to `true`
- Updated `ServerTrustChallenge` class used by the `onReceivedServerTrustAuthRequest` event
- The method `getOptions`could return null now
- The method `getOptions` could return null now
## 3.3.0+3
......
......@@ -973,7 +973,7 @@ public class InAppBrowserActivity extends AppCompatActivity implements MethodCha
public Map<String, Object> getCertificate() {
if (webView != null)
return webView.getSslCertificate();
return webView.getCertificateMap();
return null;
}
......
......@@ -488,7 +488,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler {
break;
case "getCertificate":
if (webView != null) {
result.success(webView.getSslCertificate());
result.success(webView.getCertificateMap());
} else {
result.success(null);
}
......
......@@ -1864,108 +1864,56 @@ final public class InAppWebView extends InputAwareWebView {
return obj;
}
public Map<String, Object> getSslCertificate() {
SslCertificate sslCertificate = getCertificate();
SslCertificate.DName issuedByName = sslCertificate.getIssuedBy();
Map<String, Object> issuedBy = new HashMap<>();
issuedBy.put("CName", issuedByName.getCName());
issuedBy.put("DName", issuedByName.getDName());
issuedBy.put("OName", issuedByName.getOName());
issuedBy.put("UName", issuedByName.getUName());
SslCertificate.DName issuedToName = sslCertificate.getIssuedTo();
Map<String, Object> issuedTo = new HashMap<>();
issuedTo.put("CName", issuedToName.getCName());
issuedTo.put("DName", issuedToName.getDName());
issuedTo.put("OName", issuedToName.getOName());
issuedTo.put("UName", issuedToName.getUName());
Map<String, Object> x509CertificateMap = new HashMap<>();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
X509Certificate x509Certificate = sslCertificate.getX509Certificate();
if (x509Certificate != null) {
x509CertificateMap.put("basicConstraints", x509Certificate.getBasicConstraints());
try {
x509CertificateMap.put("extendedKeyUsage", x509Certificate.getExtendedKeyUsage());
} catch (CertificateParsingException e) {
x509CertificateMap.put("extendedKeyUsage", null);
}
Map<String, Object> issuerDN = new HashMap<>();
issuerDN.put("name", x509Certificate.getIssuerDN().getName());
x509CertificateMap.put("issuerDN", issuerDN);
x509CertificateMap.put("issuerUniqueID", x509Certificate.getIssuerUniqueID());
Map<String, Object> issuerX500Principal = new HashMap<>();
issuerX500Principal.put("name", x509Certificate.getIssuerX500Principal().getName());
issuerX500Principal.put("encoded", x509Certificate.getIssuerX500Principal().getEncoded());
x509CertificateMap.put("issuerX500Principal", issuerX500Principal);
x509CertificateMap.put("keyUsage", x509Certificate.getKeyUsage());
x509CertificateMap.put("notAfter", x509Certificate.getNotAfter().getTime());
x509CertificateMap.put("notBefore", x509Certificate.getNotBefore().getTime());
x509CertificateMap.put("serialNumber", x509Certificate.getSerialNumber().longValue());
x509CertificateMap.put("sigAlgName", x509Certificate.getSigAlgName());
x509CertificateMap.put("sigAlgOID", x509Certificate.getSigAlgOID());
x509CertificateMap.put("sigAlgParams", x509Certificate.getSigAlgParams());
x509CertificateMap.put("signature", x509Certificate.getSignature());
Map<String, Object> subjectDN = new HashMap<>();
subjectDN.put("name", x509Certificate.getSubjectDN().getName());
x509CertificateMap.put("subjectDN", subjectDN);
x509CertificateMap.put("subjectUniqueID", x509Certificate.getSubjectUniqueID());
Map<String, Object> subjectX500Principal = new HashMap<>();
subjectX500Principal.put("name", x509Certificate.getSubjectX500Principal().getName());
subjectX500Principal.put("encoded", x509Certificate.getSubjectX500Principal().getEncoded());
x509CertificateMap.put("subjectX500Principal", subjectX500Principal);
public Map<String, Object> getCertificateMap() {
return InAppWebView.getCertificateMap(getCertificate());
}
public static Map<String, Object> getCertificateMap(SslCertificate sslCertificate) {
if (sslCertificate != null) {
SslCertificate.DName issuedByName = sslCertificate.getIssuedBy();
Map<String, Object> issuedBy = new HashMap<>();
issuedBy.put("CName", issuedByName.getCName());
issuedBy.put("DName", issuedByName.getDName());
issuedBy.put("OName", issuedByName.getOName());
issuedBy.put("UName", issuedByName.getUName());
SslCertificate.DName issuedToName = sslCertificate.getIssuedTo();
Map<String, Object> issuedTo = new HashMap<>();
issuedTo.put("CName", issuedToName.getCName());
issuedTo.put("DName", issuedToName.getDName());
issuedTo.put("OName", issuedToName.getOName());
issuedTo.put("UName", issuedToName.getUName());
byte[] x509CertificateData = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
x509CertificateMap.put("TBSCertificate", x509Certificate.getTBSCertificate());
X509Certificate certificate = sslCertificate.getX509Certificate();
if (certificate != null) {
x509CertificateData = certificate.getEncoded();
}
} catch (CertificateEncodingException e) {
x509CertificateMap.put("TBSCertificate", null);
e.printStackTrace();
}
x509CertificateMap.put("version", x509Certificate.getVersion());
x509CertificateMap.put("criticalExtensionOIDs", x509Certificate.getCriticalExtensionOIDs());
x509CertificateMap.put("nonCriticalExtensionOIDs", x509Certificate.getNonCriticalExtensionOIDs());
} else {
try {
x509CertificateMap.put("encoded", x509Certificate.getEncoded());
x509CertificateData = Util.getX509CertFromSslCertHack(sslCertificate).getEncoded();
} catch (CertificateEncodingException e) {
x509CertificateMap.put("encoded", null);
e.printStackTrace();
}
}
Map<String, Object> publicKey = new HashMap<>();
publicKey.put("algorithm", x509Certificate.getPublicKey().getAlgorithm());
publicKey.put("encoded", x509Certificate.getPublicKey().getEncoded());
publicKey.put("format", x509Certificate.getPublicKey().getFormat());
x509CertificateMap.put("publicKey", publicKey);
x509CertificateMap.put("type", x509Certificate.getType());
x509CertificateMap.put("hasUnsupportedCriticalExtension", x509Certificate.hasUnsupportedCriticalExtension());
Map<String, Object> obj = new HashMap<>();
obj.put("issuedBy", issuedBy);
obj.put("issuedTo", issuedTo);
obj.put("validNotAfterDate", sslCertificate.getValidNotAfterDate().getTime());
obj.put("validNotBeforeDate", sslCertificate.getValidNotBeforeDate().getTime());
obj.put("x509Certificate", x509CertificateData);
try {
x509Certificate.checkValidity();
x509CertificateMap.put("valid", true);
} catch (CertificateExpiredException e) {
x509CertificateMap.put("valid", false);
} catch (CertificateNotYetValidException e) {
x509CertificateMap.put("valid", false);
}
}
return obj;
}
Map<String, Object> obj = new HashMap<>();
obj.put("issuedBy", issuedBy);
obj.put("issuedTo", issuedTo);
obj.put("validNotAfterDate", sslCertificate.getValidNotAfterDate().getTime());
obj.put("validNotBeforeDate", sslCertificate.getValidNotBeforeDate().getTime());
obj.put("x509Certificate", x509CertificateMap);
return obj;
return null;
}
@Override
......
......@@ -301,7 +301,6 @@ public class InAppWebViewClient extends WebViewClient {
url = new URL(view.getUrl());
} catch (MalformedURLException e) {
e.printStackTrace();
Log.e(LOG_TAG, e.getMessage());
credentialsProposed = null;
previousAuthRequestFailureCount = 0;
......@@ -378,32 +377,6 @@ public class InAppWebViewClient extends WebViewClient {
});
}
/**
* SslCertificate class does not has a public getter for the underlying
* X509Certificate, we can only do this by hack. This only works for andorid 4.0+
* https://groups.google.com/forum/#!topic/android-developers/eAPJ6b7mrmg
*/
public static X509Certificate getX509CertFromSslCertHack(SslCertificate sslCert) {
X509Certificate x509Certificate = null;
Bundle bundle = SslCertificate.saveState(sslCert);
byte[] bytes = bundle.getByteArray("x509-certificate");
if (bytes == null) {
x509Certificate = null;
} else {
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
x509Certificate = (X509Certificate) cert;
} catch (CertificateException e) {
x509Certificate = null;
}
}
return x509Certificate;
}
@Override
public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError error) {
URL url;
......@@ -411,7 +384,6 @@ public class InAppWebViewClient extends WebViewClient {
url = new URL(error.getUrl());
} catch (MalformedURLException e) {
e.printStackTrace();
Log.e(LOG_TAG, e.getMessage());
handler.cancel();
return;
}
......@@ -430,19 +402,7 @@ public class InAppWebViewClient extends WebViewClient {
obj.put("port", port);
obj.put("androidError", error.getPrimaryError());
obj.put("iosError", null);
obj.put("serverCertificate", null);
try {
X509Certificate certificate;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
certificate = error.getCertificate().getX509Certificate();
} else {
certificate = getX509CertFromSslCertHack(error.getCertificate());
}
obj.put("serverCertificate", certificate.getEncoded());
} catch (CertificateEncodingException e) {
e.printStackTrace();
Log.e(LOG_TAG,e.getLocalizedMessage());
}
obj.put("sslCertificate", InAppWebView.getCertificateMap(error.getCertificate()));
String message;
switch (error.getPrimaryError()) {
......@@ -511,7 +471,6 @@ public class InAppWebViewClient extends WebViewClient {
url = new URL(view.getUrl());
} catch (MalformedURLException e) {
e.printStackTrace();
Log.e(LOG_TAG, e.getMessage());
request.cancel();
return;
}
......@@ -658,7 +617,6 @@ public class InAppWebViewClient extends WebViewClient {
uri = new URI(scheme, tempUrl.getUserInfo(), tempUrl.getHost(), tempUrl.getPort(), tempUrl.getPath(), tempUrl.getQuery(), tempUrl.getRef());
} catch (Exception e) {
e.printStackTrace();
Log.d(LOG_TAG, e.getMessage());
return null;
}
}
......@@ -677,7 +635,6 @@ public class InAppWebViewClient extends WebViewClient {
flutterResult = Util.invokeMethodAndWait(channel, "onLoadResourceCustomScheme", obj);
} catch (InterruptedException e) {
e.printStackTrace();
Log.e(LOG_TAG, e.getMessage());
return null;
}
......@@ -691,7 +648,6 @@ public class InAppWebViewClient extends WebViewClient {
response = webView.contentBlockerHandler.checkUrl(webView, url, res.get("content-type").toString());
} catch (Exception e) {
e.printStackTrace();
Log.e(LOG_TAG, e.getMessage());
}
if (response != null)
return response;
......@@ -706,7 +662,6 @@ public class InAppWebViewClient extends WebViewClient {
response = webView.contentBlockerHandler.checkUrl(webView, url);
} catch (Exception e) {
e.printStackTrace();
Log.e(LOG_TAG, e.getMessage());
}
}
return response;
......@@ -763,7 +718,6 @@ public class InAppWebViewClient extends WebViewClient {
flutterResult = Util.invokeMethodAndWait(channel, "shouldInterceptRequest", obj);
} catch (InterruptedException e) {
e.printStackTrace();
Log.e(LOG_TAG, e.getMessage());
return null;
}
......
package com.pichillilorenzo.flutter_inappwebview;
import android.content.res.AssetManager;
import android.net.http.SslCertificate;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
......@@ -8,6 +9,7 @@ import android.os.Looper;
import android.os.Parcelable;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
......@@ -16,6 +18,7 @@ import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.HashMap;
......@@ -205,4 +208,30 @@ public class Util {
throw new RuntimeException(e);
}
}
/**
* SslCertificate class does not has a public getter for the underlying
* X509Certificate, we can only do this by hack. This only works for andorid 4.0+
* https://groups.google.com/forum/#!topic/android-developers/eAPJ6b7mrmg
*/
public static X509Certificate getX509CertFromSslCertHack(SslCertificate sslCert) {
X509Certificate x509Certificate = null;
Bundle bundle = SslCertificate.saveState(sslCert);
byte[] bytes = bundle.getByteArray("x509-certificate");
if (bytes == null) {
x509Certificate = null;
} else {
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
x509Certificate = (X509Certificate) cert;
} catch (CertificateException e) {
x509Certificate = null;
}
}
return x509Certificate;
}
}
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"e2e","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/e2e-0.2.4+4/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.4.4/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.9/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.0+hotfix.6/","dependencies":[]}],"android":[{"name":"e2e","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/e2e-0.2.4+4/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.4.4/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.9/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.0+hotfix.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+3/","dependencies":[]}],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"e2e","dependencies":[]},{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]},{"name":"permission_handler","dependencies":[]}],"date_created":"2020-06-13 01:39:11.912485","version":"1.17.1"}
\ No newline at end of file
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"e2e","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/e2e-0.2.4+4/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.4.4/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.9/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.0+hotfix.6/","dependencies":[]}],"android":[{"name":"e2e","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/e2e-0.2.4+4/","dependencies":[]},{"name":"flutter_downloader","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/flutter_downloader-1.4.4/","dependencies":[]},{"name":"flutter_inappwebview","path":"/Users/lorenzopichilli/Desktop/flutter_inappwebview/","dependencies":[]},{"name":"path_provider","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.9/","dependencies":[]},{"name":"permission_handler","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/permission_handler-5.0.0+hotfix.6/","dependencies":[]}],"macos":[{"name":"path_provider_macos","path":"/Users/lorenzopichilli/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider_macos-0.0.4+3/","dependencies":[]}],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"e2e","dependencies":[]},{"name":"flutter_downloader","dependencies":[]},{"name":"flutter_inappwebview","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_macos"]},{"name":"path_provider_macos","dependencies":[]},{"name":"permission_handler","dependencies":[]}],"date_created":"2020-06-14 18:36:37.641339","version":"1.17.1"}
\ No newline at end of file
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
......@@ -107,7 +108,6 @@ class _InAppWebViewExampleScreenState extends State<InAppWebViewExampleScreen> {
setState(() {
this.url = url;
});
},
onProgressChanged: (InAppWebViewController controller, int progress) {
setState(() {
......
......@@ -26,7 +26,6 @@
<excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/flutter_inappwebview/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/flutter_inappwebview/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/flutter_inappwebview/example/build" />
<excludeFolder url="file://$MODULE_DIR$/example/ios/Flutter/App.framework/flutter_assets/packages" />
<excludeFolder url="file://$MODULE_DIR$/flutter_inappbrowser_tests/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/flutter_inappbrowser_tests/.pub" />
<excludeFolder url="file://$MODULE_DIR$/flutter_inappbrowser_tests/build" />
......
......@@ -491,6 +491,13 @@ public class FlutterWebViewController: FlutterMethodCallDelegate, FlutterPlatfor
result(false)
}
break
case "getCertificate":
if webView != nil {
result(webView!.getCertificateMap())
} else {
result(false)
}
break
default:
result(FlutterMethodNotImplemented)
break
......
......@@ -328,6 +328,55 @@ public class InAppBrowserWebViewController: UIViewController, FlutterPlugin, UIS
result(false)
}
break
case "requestFocusNodeHref":
if webView != nil {
webView!.requestFocusNodeHref { (value, error) in
if let err = error {
print(err.localizedDescription)
result(nil)
return
}
result(value)
}
} else {
result(false)
}
break
case "requestImageRef":
if webView != nil {
webView!.requestImageRef { (value, error) in
if let err = error {
print(err.localizedDescription)
result(nil)
return
}
result(value)
}
} else {
result(false)
}
break
case "getScrollX":
if webView != nil {
result(Int(webView!.scrollView.contentOffset.x))
} else {
result(false)
}
break
case "getScrollY":
if webView != nil {
result(Int(webView!.scrollView.contentOffset.y))
} else {
result(false)
}
break
case "getCertificate":
if webView != nil {
result(webView!.getCertificateMap())
} else {
result(false)
}
break
default:
result(FlutterMethodNotImplemented)
break
......
......@@ -799,6 +799,8 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
var channel: FlutterMethodChannel?
var options: InAppWebViewOptions?
var currentURL: URL?
var x509CertificateData: Data?
static var sslCertificateMap: [String: Data] = [:] // [URL host name : x509Certificate Data]
var startPageTime: Int64 = 0
static var credentialsProposed: [URLCredential] = []
var lastScrollX: CGFloat = 0
......@@ -1863,6 +1865,8 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
}
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
self.x509CertificateData = nil
self.startPageTime = currentTimeInMilliSeconds()
onLoadStart(url: (currentURL?.absoluteString)!)
......@@ -2557,6 +2561,10 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
let data = CFDataGetBytePtr(serverCertificateCFData)
let size = CFDataGetLength(serverCertificateCFData)
serverCertificateData = NSData(bytes: data, length: size)
if (x509CertificateData == nil) {
x509CertificateData = Data(serverCertificateData!)
InAppWebView.sslCertificateMap[challenge.protectionSpace.host] = x509CertificateData;
}
}
let error = secResult != SecTrustResultType.proceed ? secResult.rawValue : nil
......@@ -2591,7 +2599,8 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
"realm": challenge.protectionSpace.realm,
"port": challenge.protectionSpace.port,
"previousFailureCount": challenge.previousFailureCount,
"serverCertificate": serverCertificateData,
"sslCertificate": InAppWebView.getCertificateMap(x509Certificate:
((serverCertificateData != nil) ? Data(serverCertificateData!) : nil)),
"androidError": nil,
"iosError": error,
"message": message,
......@@ -2892,6 +2901,29 @@ public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, WKNavi
self.scrollView.subviews.first?.resignFirstResponder()
}
public func getCertificate() -> Data? {
var x509Certificate = self.x509CertificateData
if x509Certificate == nil, let scheme = url?.scheme, scheme == "https",
let host = url?.host, let cert = InAppWebView.sslCertificateMap[host] {
x509Certificate = cert
}
return x509Certificate
}
public func getCertificateMap() -> [String: Any?]? {
return InAppWebView.getCertificateMap(x509Certificate: getCertificate())
}
public static func getCertificateMap(x509Certificate: Data?) -> [String: Any?]? {
return x509Certificate != nil ? [
"issuedBy": nil,
"issuedTo": nil,
"validNotAfterDate": nil,
"validNotBeforeDate": nil,
"x509Certificate": x509Certificate
] : nil;
}
public func dispose() {
stopLoading()
configuration.userContentController.removeScriptMessageHandler(forName: "consoleLog")
......
......@@ -37,3 +37,4 @@ export 'src/http_auth_credentials_database.dart';
export 'src/web_storage_manager.dart';
export 'src/context_menu.dart';
export 'src/web_storage.dart';
export 'src/X509Certificate/main.dart';
\ No newline at end of file
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'asn1_identifier.dart';
import 'asn1_object.dart';
class ASN1DERDecoder {
static List<ASN1Object> decode({@required List<int> data}) {
var iterator = data.iterator;
return parse(iterator: iterator);
}
static List<ASN1Object> parse({@required Iterator<int> iterator}) {
var result = <ASN1Object>[];
while (iterator.moveNext()) {
var nextValue = iterator.current;
var asn1obj = ASN1Object();
asn1obj.identifier = ASN1Identifier(nextValue);
if (asn1obj.identifier.isConstructed()) {
var contentData = loadSubContent(iterator: iterator);
if (contentData.isEmpty) {
asn1obj.sub = parse(iterator: iterator);
} else {
var subIterator = contentData.iterator;
asn1obj.sub = parse(iterator: subIterator);
}
asn1obj.value = null;
asn1obj.encoded = Uint8List.fromList(contentData);
for (var item in asn1obj.sub) {
item.parent = asn1obj;
}
} else {
if (asn1obj.identifier.typeClass() == ASN1IdentifierClass.UNIVERSAL) {
var contentData = loadSubContent(iterator: iterator);
asn1obj.encoded = Uint8List.fromList(contentData);
// decode the content data with come more convenient format
var tagNumber = asn1obj.identifier.tagNumber();
if (tagNumber == ASN1IdentifierTagNumber.END_OF_CONTENT) {
return result;
}
else if (tagNumber == ASN1IdentifierTagNumber.BOOLEAN) {
var value = contentData.length > 0 ? contentData.first : null;
if (value != null) {
asn1obj.value = value > 0 ? true : false;
}
}
else if (tagNumber == ASN1IdentifierTagNumber.INTEGER) {
while (contentData.length > 0 && contentData.first == 0) {
contentData.removeAt(0); // remove not significant digit
}
asn1obj.value = contentData;
}
else if (tagNumber == ASN1IdentifierTagNumber.NULL) {
asn1obj.value = null;
}
else if (tagNumber == ASN1IdentifierTagNumber.OBJECT_IDENTIFIER) {
asn1obj.value = decodeOid(contentData: contentData);
}
else if ([
ASN1IdentifierTagNumber.UTF8_STRING,
ASN1IdentifierTagNumber.PRINTABLE_STRING,
ASN1IdentifierTagNumber.NUMERIC_STRING,
ASN1IdentifierTagNumber.GENERAL_STRING,
ASN1IdentifierTagNumber.UNIVERSAL_STRING,
ASN1IdentifierTagNumber.CHARACTER_STRING,
ASN1IdentifierTagNumber.T61_STRING].contains(tagNumber)) {
asn1obj.value = utf8.decode(contentData, allowMalformed: true);
}
else if (tagNumber == ASN1IdentifierTagNumber.BMP_STRING) {
asn1obj.value = String.fromCharCodes(contentData);
}
else if ([
ASN1IdentifierTagNumber.VISIBLE_STRING,
ASN1IdentifierTagNumber.IA5_STRING
].contains(tagNumber)) {
asn1obj.value = ascii.decode(contentData, allowInvalid: true);
}
else if (tagNumber == ASN1IdentifierTagNumber.UTC_TIME) {
asn1obj.value = utcTimeToDate(contentData: contentData);
}
else if (tagNumber == ASN1IdentifierTagNumber.GENERALIZED_TIME) {
asn1obj.value = generalizedTimeToDate(contentData: contentData);
}
else if (tagNumber == ASN1IdentifierTagNumber.BIT_STRING) {
if (contentData.length > 0) {
contentData.removeAt(0); // unused bits
}
asn1obj.value = contentData;
}
else if (tagNumber == ASN1IdentifierTagNumber.OCTET_STRING) {
try {
var subIterator = contentData.iterator;
asn1obj.sub = parse(iterator: subIterator);
} catch (e) {
var str;
try {
str = utf8.decode(contentData);
} catch(e) {}
if (str != null) {
asn1obj.value = str;
} else {
asn1obj.value = contentData;
}
}
}
else {
// print("unsupported tag: ${asn1obj.identifier.tagNumber()}");
asn1obj.value = contentData;
}
} else {
// custom/private tag
var contentData = loadSubContent(iterator: iterator);
var str;
try {
str = utf8.decode(contentData);
} catch(e) {}
if (str != null) {
asn1obj.value = str;
} else {
asn1obj.value = contentData;
}
}
}
result.add(asn1obj);
}
return result;
}
static BigInt getContentLength({@required Iterator<int> iterator}) {
if (iterator.moveNext()) {
var first = iterator.current;
if (first != null) {
if ((first & 0x80) != 0) { // long
var octetsToRead = first - 0x80;
var data = <int>[];
for (var i = 0; i < octetsToRead; i++) {
if (iterator.moveNext()) {
var n = iterator.current;
if (n != null) {
data.add(n);
}
}
}
return toIntValue(data) ?? BigInt.from(0);
} else { // short
return BigInt.from(first);
}
}
}
return BigInt.from(0);
}
static List<int> loadSubContent({@required Iterator<int> iterator}) {
var len = getContentLength(iterator: iterator);
int int64MaxValue = 9223372036854775807;
if (len >= BigInt.from(int64MaxValue)) {
return <int>[];
}
var byteArray = <int>[];
for (var i = 0; i < len.toInt(); i++) {
if (iterator.moveNext()) {
var n = iterator.current;
if (n != null) {
byteArray.add(n);
}
} else {
break;
// throw ASN1OutOfBufferError();
}
}
return byteArray;
}
/// Decode DER OID bytes to String with dot notation
static String decodeOid({@required List<int> contentData}) {
if (contentData.isEmpty) {
return "";
}
var oid = "";
var first = contentData.removeAt(0);
oid += "${(first / 40).truncate()}.${first % 40}";
var t = 0;
while (contentData.length > 0) {
var n = contentData.removeAt(0);
t = (t << 7) | (n & 0x7F);
if ((n & 0x80) == 0) {
oid += ".$t";
t = 0;
}
}
return oid;
}
///Converts a UTCTime value to a date.
///
///Note: GeneralizedTime has 4 digits for the year and is used for X.509
///dates past 2049. Parsing that structure hasn't been implemented yet.
///
///[contentData] the UTCTime value to convert.
static DateTime utcTimeToDate({@required List<int> contentData}) {
/* The following formats can be used:
YYMMDDhhmmZ
YYMMDDhhmm+hh'mm'
YYMMDDhhmm-hh'mm'
YYMMDDhhmmssZ
YYMMDDhhmmss+hh'mm'
YYMMDDhhmmss-hh'mm'
Where:
YY is the least significant two digits of the year
MM is the month (01 to 12)
DD is the day (01 to 31)
hh is the hour (00 to 23)
mm are the minutes (00 to 59)
ss are the seconds (00 to 59)
Z indicates that local time is GMT, + indicates that local time is
later than GMT, and - indicates that local time is earlier than GMT
hh' is the absolute value of the offset from GMT in hours
mm' is the absolute value of the offset from GMT in minutes */
String utc;
try {
utc = utf8.decode(contentData);
} catch(e) {}
if (utc == null) {
return null;
}
// if YY >= 50 use 19xx, if YY < 50 use 20xx
var year = int.parse(utc.substring(0, 2), radix: 10);
year = (year >= 50) ? 1900 + year : 2000 + year;
var MM = int.parse(utc.substring(2, 4), radix: 10);
var DD = int.parse(utc.substring(4, 6), radix: 10);
var hh = int.parse(utc.substring(6, 8), radix: 10);
var mm = int.parse(utc.substring(8, 10), radix: 10);
var ss = 0;
int end;
String c;
// not just YYMMDDhhmmZ
if(utc.length > 11) {
// get character after minutes
c = utc[10];
end = 10;
// see if seconds are present
if(c != '+' && c != '-') {
// get seconds
ss = int.parse(utc.substring(10, 12), radix: 10);
end += 2;
}
}
var date = DateTime.utc(
year,
MM,
DD,
hh,
mm,
ss,
0
);
if(end != null) {
// get +/- after end of time
c = utc[end];
if(c == '+' || c == '-') {
// get hours+minutes offset
var hhoffset = int.parse(utc.substring(end + 1, end + 1 + 2), radix: 10);
var mmoffset = int.parse(utc.substring(end + 4, end + 4 + 2), radix: 10);
// calculate offset in milliseconds
var offset = hhoffset * 60 + mmoffset;
offset *= 60000;
var offsetDuration = Duration(milliseconds: offset);
// apply offset
if(c == '+') {
date.subtract(offsetDuration);
} else {
date.add(offsetDuration);
}
}
}
return date;
}
///Converts a GeneralizedTime value to a date.
///
///[contentData] the GeneralizedTime value to convert.
static DateTime generalizedTimeToDate({@required List<int> contentData}) {
/* The following formats can be used:
YYYYMMDDHHMMSS
YYYYMMDDHHMMSS.fff
YYYYMMDDHHMMSSZ
YYYYMMDDHHMMSS.fffZ
YYYYMMDDHHMMSS+hh'mm'
YYYYMMDDHHMMSS.fff+hh'mm'
YYYYMMDDHHMMSS-hh'mm'
YYYYMMDDHHMMSS.fff-hh'mm'
Where:
YYYY is the year
MM is the month (01 to 12)
DD is the day (01 to 31)
hh is the hour (00 to 23)
mm are the minutes (00 to 59)
ss are the seconds (00 to 59)
.fff is the second fraction, accurate to three decimal places
Z indicates that local time is GMT, + indicates that local time is
later than GMT, and - indicates that local time is earlier than GMT
hh' is the absolute value of the offset from GMT in hours
mm' is the absolute value of the offset from GMT in minutes */
String gentime;
try {
gentime = utf8.decode(contentData);
} catch(e) {}
if (gentime == null) {
return null;
}
// if YY >= 50 use 19xx, if YY < 50 use 20xx
var YYYY = int.parse(gentime.substring(0, 4), radix: 10);
var MM = int.parse(gentime.substring(4, 6), radix: 10);
var DD = int.parse(gentime.substring(6, 8), radix: 10);
var hh = int.parse(gentime.substring(8, 10), radix: 10);
var mm = int.parse(gentime.substring(10, 12), radix: 10);
var ss = int.parse(gentime.substring(12, 14), radix: 10);
double fff = 0.0;
var offset = 0;
var isUTC = false;
if(gentime[gentime.length - 1] == 'Z') {
isUTC = true;
}
var end = gentime.length - 5;
var c = gentime[end];
if(c == '+' || c == '-') {
// get hours+minutes offset
var hhoffset = int.parse(gentime.substring(end + 1, end + 1 + 2), radix: 10);
var mmoffset = int.parse(gentime.substring(end + 4, end + 4 + 2), radix: 10);
// calculate offset in milliseconds
offset = hhoffset * 60 + mmoffset;
offset *= 60000;
// apply offset
if(c == '+') {
offset *= -1;
}
isUTC = true;
}
// check for second fraction
if(gentime[14] == '.') {
fff = double.parse(gentime.substring(14)) * 1000;
}
var date = DateTime.utc(
YYYY,
MM,
DD,
hh,
mm,
ss,
fff.toInt()
);
if(isUTC) {
var offsetDuration = Duration(milliseconds: offset);
date.add(offsetDuration);
}
return date;
}
}
BigInt toIntValue(List<int> data) {
if (data.length > 8) {
return null;
}
BigInt value = BigInt.from(0);
for (var index = 0; index < data.length; index++) {
var byte = data[index];
value += BigInt.from(byte << 8*(data.length-index-1));
}
return value;
}
class ASN1OutOfBufferError extends Error {
}
class ASN1ParseError extends Error {
}
\ No newline at end of file
class ASN1DistinguishedNames {
final String _oid;
final String _representation;
const ASN1DistinguishedNames._internal(this._oid, this._representation);
static List<ASN1DistinguishedNames> values = [
ASN1DistinguishedNames.COMMON_NAME,
ASN1DistinguishedNames.DN_QUALIFIER,
ASN1DistinguishedNames.SERIAL_NUMBER,
ASN1DistinguishedNames.GIVEN_NAME,
ASN1DistinguishedNames.SURNAME,
ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME,
ASN1DistinguishedNames.ORGANIZATION_NAME,
ASN1DistinguishedNames.STREET_ADDRESS,
ASN1DistinguishedNames.LOCALITY_NAME,
ASN1DistinguishedNames.STATE_OR_PROVINCE_NAME,
ASN1DistinguishedNames.COUNTRY_NAME,
ASN1DistinguishedNames.EMAIL,
];
static ASN1DistinguishedNames fromValue(String oid) {
return ASN1DistinguishedNames.values.firstWhere((element) => element.oid() == oid, orElse: () => null);
}
String oid() => _oid;
String representation() => _representation;
@override
String toString() => "($_oid, $_representation)";
static const COMMON_NAME = const ASN1DistinguishedNames._internal("2.5.4.3", "CN");
static const DN_QUALIFIER = const ASN1DistinguishedNames._internal("2.5.4.46", "DNQ");
static const SERIAL_NUMBER = const ASN1DistinguishedNames._internal("2.5.4.5", "SERIALNUMBER");
static const GIVEN_NAME = const ASN1DistinguishedNames._internal("2.5.4.42", "GIVENNAME");
static const SURNAME = const ASN1DistinguishedNames._internal("2.5.4.4", "SURNAME");
static const ORGANIZATIONAL_UNIT_NAME = const ASN1DistinguishedNames._internal("2.5.4.11", "OU");
static const ORGANIZATION_NAME = const ASN1DistinguishedNames._internal("2.5.4.10", "O");
static const STREET_ADDRESS = const ASN1DistinguishedNames._internal("2.5.4.9", "STREET");
static const LOCALITY_NAME = const ASN1DistinguishedNames._internal("2.5.4.7", "L");
static const STATE_OR_PROVINCE_NAME = const ASN1DistinguishedNames._internal("2.5.4.8", "ST");
static const COUNTRY_NAME = const ASN1DistinguishedNames._internal("2.5.4.6", "C");
static const EMAIL = const ASN1DistinguishedNames._internal("1.2.840.113549.1.9.1", "E");
bool operator ==(value) => value == _oid;
@override
int get hashCode => _oid.hashCode;
}
\ No newline at end of file
class ASN1IdentifierClass {
final int _value;
const ASN1IdentifierClass._internal(this._value);
static List<ASN1IdentifierClass> values = [
ASN1IdentifierClass.UNIVERSAL,
ASN1IdentifierClass.APPLICATION,
ASN1IdentifierClass.CONTEXT_SPECIFIC,
ASN1IdentifierClass.PRIVATE,
];
static ASN1IdentifierClass fromValue(int value) {
if (value != null)
return ASN1IdentifierClass.values.firstWhere((element) => element.toValue() == value, orElse: () => null);
return null;
}
int toValue() => _value;
static const UNIVERSAL = const ASN1IdentifierClass._internal(0x00);
static const APPLICATION = const ASN1IdentifierClass._internal(0x40);
static const CONTEXT_SPECIFIC = const ASN1IdentifierClass._internal(0x80);
static const PRIVATE = const ASN1IdentifierClass._internal(0xC0);
String toString() {
switch (this.toValue()) {
case 0x00:
return "UNIVERSAL";
case 0x40:
return "APPLICATION";
case 0x80:
return "CONTEXT_SPECIFIC";
case 0xC0:
return "PRIVATE";
}
}
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
}
class ASN1IdentifierTagNumber {
final int _value;
const ASN1IdentifierTagNumber._internal(this._value);
static List<ASN1IdentifierTagNumber> values = [
ASN1IdentifierTagNumber.END_OF_CONTENT,
ASN1IdentifierTagNumber.BOOLEAN,
ASN1IdentifierTagNumber.INTEGER,
ASN1IdentifierTagNumber.BIT_STRING,
ASN1IdentifierTagNumber.OCTET_STRING,
ASN1IdentifierTagNumber.NULL,
ASN1IdentifierTagNumber.OBJECT_IDENTIFIER,
ASN1IdentifierTagNumber.OBJECT_DESCRIPTOR,
ASN1IdentifierTagNumber.EXTERNAL,
ASN1IdentifierTagNumber.READ,
ASN1IdentifierTagNumber.ENUMERATED,
ASN1IdentifierTagNumber.EMBEDDED_PDV,
ASN1IdentifierTagNumber.UTF8_STRING,
ASN1IdentifierTagNumber.RELATIVE_OID,
ASN1IdentifierTagNumber.SEQUENCE,
ASN1IdentifierTagNumber.SET,
ASN1IdentifierTagNumber.NUMERIC_STRING,
ASN1IdentifierTagNumber.PRINTABLE_STRING,
ASN1IdentifierTagNumber.T61_STRING,
ASN1IdentifierTagNumber.VIDEOTEX_STRING,
ASN1IdentifierTagNumber.IA5_STRING,
ASN1IdentifierTagNumber.UTC_TIME,
ASN1IdentifierTagNumber.GENERALIZED_TIME,
ASN1IdentifierTagNumber.GRAPHIC_STRING,
ASN1IdentifierTagNumber.VISIBLE_STRING,
ASN1IdentifierTagNumber.GENERAL_STRING,
ASN1IdentifierTagNumber.UNIVERSAL_STRING,
ASN1IdentifierTagNumber.CHARACTER_STRING,
ASN1IdentifierTagNumber.BMP_STRING,
];
static ASN1IdentifierTagNumber fromValue(int value) {
if (value != null)
return ASN1IdentifierTagNumber.values.firstWhere((element) => element.toValue() == value, orElse: () => null);
return null;
}
int toValue() => _value;
static const END_OF_CONTENT = const ASN1IdentifierTagNumber._internal(0x00);
static const BOOLEAN = const ASN1IdentifierTagNumber._internal(0x01);
static const INTEGER = const ASN1IdentifierTagNumber._internal(0x02);
static const BIT_STRING = const ASN1IdentifierTagNumber._internal(0x03);
static const OCTET_STRING = const ASN1IdentifierTagNumber._internal(0x04);
static const NULL = const ASN1IdentifierTagNumber._internal(0x05);
static const OBJECT_IDENTIFIER = const ASN1IdentifierTagNumber._internal(0x06);
static const OBJECT_DESCRIPTOR = const ASN1IdentifierTagNumber._internal(0x07);
static const EXTERNAL = const ASN1IdentifierTagNumber._internal(0x08);
static const READ = const ASN1IdentifierTagNumber._internal(0x09);
static const ENUMERATED = const ASN1IdentifierTagNumber._internal(0x0A);
static const EMBEDDED_PDV = const ASN1IdentifierTagNumber._internal(0x0B);
static const UTF8_STRING = const ASN1IdentifierTagNumber._internal(0x0C);
static const RELATIVE_OID = const ASN1IdentifierTagNumber._internal(0x0D);
static const SEQUENCE = const ASN1IdentifierTagNumber._internal(0x10);
static const SET = const ASN1IdentifierTagNumber._internal(0x11);
static const NUMERIC_STRING = const ASN1IdentifierTagNumber._internal(0x12);
static const PRINTABLE_STRING = const ASN1IdentifierTagNumber._internal(0x13);
static const T61_STRING = const ASN1IdentifierTagNumber._internal(0x14);
static const VIDEOTEX_STRING = const ASN1IdentifierTagNumber._internal(0x15);
static const IA5_STRING = const ASN1IdentifierTagNumber._internal(0x16);
static const UTC_TIME = const ASN1IdentifierTagNumber._internal(0x17);
static const GENERALIZED_TIME = const ASN1IdentifierTagNumber._internal(0x18);
static const GRAPHIC_STRING = const ASN1IdentifierTagNumber._internal(0x19);
static const VISIBLE_STRING = const ASN1IdentifierTagNumber._internal(0x1A);
static const GENERAL_STRING = const ASN1IdentifierTagNumber._internal(0x1B);
static const UNIVERSAL_STRING = const ASN1IdentifierTagNumber._internal(0x1C);
static const CHARACTER_STRING = const ASN1IdentifierTagNumber._internal(0x1D);
static const BMP_STRING = const ASN1IdentifierTagNumber._internal(0x1E);
String toString() {
switch (this.toValue()) {
case 0x00:
return "END_OF_CONTENT";
case 0x01:
return "BOOLEAN";
case 0x02:
return "INTEGER";
case 0x03:
return "BIT_STRING";
case 0x04:
return "OCTET_STRING";
case 0x05:
return "NULL";
case 0x06:
return "OBJECT_IDENTIFIER";
case 0x07:
return "OBJECT_DESCRIPTOR";
case 0x08:
return "EXTERNAL";
case 0x09:
return "READ";
case 0x0A:
return "ENUMERATED";
case 0x0B:
return "EMBEDDED_PDV";
case 0x0C:
return "UTF8_STRING";
case 0x0D:
return "RELATIVE_OID";
case 0x10:
return "SEQUENCE";
case 0x11:
return "SET";
case 0x12:
return "NUMERIC_STRING";
case 0x13:
return "PRINTABLE_STRING";
case 0x14:
return "T61_STRING";
case 0x15:
return "VIDEOTEX_STRING";
case 0x16:
return "IA5_STRING";
case 0x17:
return "UTC_TIME";
case 0x18:
return "GENERALIZED_TIME";
case 0x19:
return "GRAPHIC_STRING";
case 0x1A:
return "VISIBLE_STRING";
case 0x1B:
return "GENERAL_STRING";
case 0x1C:
return "UNIVERSAL_STRING";
case 0x1D:
return "CHARACTER_STRING";
case 0x1E:
return "BMP_STRING";
}
}
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
}
class ASN1Identifier {
int rawValue;
ASN1Identifier(this.rawValue);
bool isPrimitive() {
return (rawValue & 0x20) == 0;
}
bool isConstructed() {
return (rawValue & 0x20) != 0;
}
ASN1IdentifierTagNumber tagNumber() {
return ASN1IdentifierTagNumber.fromValue(rawValue & 0x1F) ?? ASN1IdentifierTagNumber.END_OF_CONTENT;
}
ASN1IdentifierClass typeClass() {
for (var tc in [ASN1IdentifierClass.APPLICATION, ASN1IdentifierClass.CONTEXT_SPECIFIC, ASN1IdentifierClass.PRIVATE]) {
if ((rawValue & tc.toValue()) == tc.toValue()) {
return tc;
}
}
return ASN1IdentifierClass.UNIVERSAL;
}
String get description {
var tc = typeClass();
var tn = tagNumber();
if (tc == ASN1IdentifierClass.UNIVERSAL) {
return tn.toString();
} else {
return "$tc(${tn.toValue()})";
}
}
@override
String toString() {
return description;
}
}
\ No newline at end of file
import 'dart:typed_data';
import 'x509_certificate.dart';
import 'asn1_identifier.dart';
import 'oid.dart';
class ASN1Object {
/// This property contains the DER encoded object
Uint8List encoded;
/// This property contains the decoded Swift object whenever is possible
dynamic value;
ASN1Identifier identifier;
List<ASN1Object> sub;
ASN1Object parent;
ASN1Object subAtIndex(int index) {
if (sub != null && index >= 0 && index < sub.length) {
return sub[index];
}
return null;
}
int subCount() {
return sub?.length ?? 0;
}
ASN1Object findOid({OID oid, String oidValue}) {
oidValue = oid != null ? oid.toValue() : oidValue;
for (var child in (sub ?? <ASN1Object>[])) {
if (child.identifier?.tagNumber() == ASN1IdentifierTagNumber.OBJECT_IDENTIFIER) {
if (child.value == oidValue) {
return child;
}
}
else {
var result = child.findOid(oidValue: oidValue);
if (result != null) {
return result;
}
}
}
return null;
}
String get description {
return printAsn1();
}
String printAsn1({insets = ""}) {
var output = insets;
output += identifier?.description?.toUpperCase() ?? "";
output += (value != null ? ": $value" : "");
if (identifier?.typeClass() == ASN1IdentifierClass.UNIVERSAL && identifier?.tagNumber() == ASN1IdentifierTagNumber.OBJECT_IDENTIFIER) {
var descr = OID.fromValue(value?.toString() ?? "")?.name();
if (descr != null) {
output += " ($descr)";
}
}
output += sub != null && sub.length > 0 ? " {" : "";
output += "\n";
for (var item in (sub ?? <ASN1Object>[])) {
output += item.printAsn1(insets: insets + " ");
}
output += sub != null && sub.length > 0 ? "}\n" : "";
return output;
}
@override
String toString() {
return description;
}
ASN1Object atIndex(X509BlockPosition x509blockPosition) {
if (sub != null && x509blockPosition.index < sub.length) {
return sub[x509blockPosition.index];
}
return null;
}
}
\ No newline at end of file
export 'asn1_decoder.dart';
export 'asn1_distinguished_names.dart';
export 'asn1_identifier.dart';
export 'asn1_object.dart';
export 'oid.dart';
export 'x509_certificate.dart';
export 'x509_extension.dart';
export 'x509_public_key.dart';
class OID {
final String _value;
const OID._internal(this._value);
static List<OID> values = [
OID.etsiQcsCompliance,
OID.etsiQcsRetentionPeriod,
OID.etsiQcsQcSSCD,
OID.dsa,
OID.ecPublicKey,
OID.prime256v1,
OID.ecdsaWithSHA256,
OID.ecdsaWithSHA512,
OID.rsaEncryption,
OID.md2WithRSAEncryption,
OID.md4WithRSAEncryption,
OID.md5WithRSAEncryption,
OID.sha1WithRSAEncryption,
OID.RSAES_OAEP,
OID.mgf1,
OID.pSpecified,
OID.RSASSA_PSS,
OID.sha256WithRSAEncryption,
OID.sha384WithRSAEncryption,
OID.sha512WithRSAEncryption,
OID.pkcs7data,
OID.pkcs7signedData,
OID.pkcs7envelopedData,
OID.emailAddress,
OID.signingCertificateV2,
OID.contentType,
OID.messageDigest,
OID.signingTime,
OID.dsaWithSha1,
OID.certificateExtension,
OID.jurisdictionOfIncorporationSP,
OID.jurisdictionOfIncorporationC,
OID.authorityInfoAccess,
OID.qcStatements,
OID.cps,
OID.unotice,
OID.serverAuth,
OID.clientAuth,
OID.ocsp,
OID.caIssuers,
OID.dateOfBirth,
OID.desCBC,
OID.sha1,
OID.sha256,
OID.sha384,
OID.sha512,
OID.md5,
OID.VeriSignEVpolicy,
OID.extendedValidation,
OID.organizationValidated,
OID.subjectKeyIdentifier,
OID.keyUsage,
OID.subjectAltName,
OID.issuerAltName,
OID.basicConstraints,
OID.cRLDistributionPoints,
OID.certificatePolicies,
OID.authorityKeyIdentifier,
OID.extKeyUsage,
OID.subjectDirectoryAttributes,
OID.organizationName,
OID.organizationalUnitName,
OID.businessCategory,
OID.postalCode,
OID.commonName,
OID.surname,
OID.givenName,
OID.dnQualifier,
OID.serialNumber,
OID.countryName,
OID.localityName,
OID.stateOrProvinceName,
OID.streetAddress,
OID.desEDE3CBC,
OID.aes128CBC,
OID.aes192CBC,
OID.aes256CBC,
OID.nsCertType,
OID.nsComment,
OID.privateKeyUsagePeriod,
OID.cRLNumber,
OID.cRLReason,
OID.expirationDate,
OID.instructionCode,
OID.invalidityDate,
OID.deltaCRLIndicator,
OID.issuingDistributionPoint,
OID.certificateIssuer,
OID.nameConstraints,
OID.policyMappings,
OID.policyConstraints,
OID.freshestCRL,
OID.inhibitAnyPolicy,
OID.codeSigning,
OID.emailProtection,
OID.timeStamping,
];
static OID fromValue(String value) {
return OID.values.firstWhere((element) => element.toValue() == value, orElse: () => null);
}
String toValue() => _value;
String name() => _oidMapName[this._value];
@override
String toString() => "($_value, ${name()})";
static const etsiQcsCompliance = const OID._internal("0.4.0.1862.1.1");
static const etsiQcsRetentionPeriod = const OID._internal("0.4.0.1862.1.3");
static const etsiQcsQcSSCD = const OID._internal("0.4.0.1862.1.4");
static const dsa = const OID._internal("1.2.840.10040.4.1");
static const ecPublicKey = const OID._internal("1.2.840.10045.2.1");
static const prime256v1 = const OID._internal("1.2.840.10045.3.1.7");
static const ecdsaWithSHA256 = const OID._internal("1.2.840.10045.4.3.2");
static const ecdsaWithSHA512 = const OID._internal("1.2.840.10045.4.3.4");
static const rsaEncryption = const OID._internal("1.2.840.113549.1.1.1");
static const md2WithRSAEncryption = const OID._internal("1.2.840.113549.1.1.2");
static const md4WithRSAEncryption = const OID._internal("1.2.840.113549.1.1.3");
static const md5WithRSAEncryption = const OID._internal("1.2.840.113549.1.1.4");
static const sha1WithRSAEncryption = const OID._internal("1.2.840.113549.1.1.5");
static const RSAES_OAEP = const OID._internal("1.2.840.113549.1.1.7");
static const mgf1 = const OID._internal(".2.840.113549.1.1.8");
static const pSpecified = const OID._internal(".2.840.113549.1.1.9");
static const RSASSA_PSS = const OID._internal(".2.840.113549.1.1.10");
static const sha256WithRSAEncryption = const OID._internal("1.2.840.113549.1.1.11");
static const sha384WithRSAEncryption = const OID._internal("1.2.840.113549.1.1.12");
static const sha512WithRSAEncryption = const OID._internal("1.2.840.113549.1.1.13");
static const pkcs7data = const OID._internal("1.2.840.113549.1.7.1");
static const pkcs7signedData = const OID._internal("1.2.840.113549.1.7.2");
static const pkcs7envelopedData = const OID._internal("1.2.840.113549.1.7.3");
static const emailAddress = const OID._internal("1.2.840.113549.1.9.1");
static const signingCertificateV2 = const OID._internal("1.2.840.113549.1.9.16.2.47");
static const contentType = const OID._internal("1.2.840.113549.1.9.3");
static const messageDigest = const OID._internal("1.2.840.113549.1.9.4");
static const signingTime = const OID._internal("1.2.840.113549.1.9.5");
static const dsaWithSha1 = const OID._internal("1.2.840.10040.4.3");
static const certificateExtension = const OID._internal("1.3.6.1.4.1.11129.2.4.2");
static const jurisdictionOfIncorporationSP = const OID._internal("1.3.6.1.4.1.311.60.2.1.2");
static const jurisdictionOfIncorporationC = const OID._internal("1.3.6.1.4.1.311.60.2.1.3");
static const authorityInfoAccess = const OID._internal("1.3.6.1.5.5.7.1.1");
static const qcStatements = const OID._internal("1.3.6.1.5.5.7.1.3");
static const cps = const OID._internal("1.3.6.1.5.5.7.2.1");
static const unotice = const OID._internal("1.3.6.1.5.5.7.2.2");
static const serverAuth = const OID._internal("1.3.6.1.5.5.7.3.1");
static const clientAuth = const OID._internal("1.3.6.1.5.5.7.3.2");
static const ocsp = const OID._internal("1.3.6.1.5.5.7.48.1");
static const caIssuers = const OID._internal("1.3.6.1.5.5.7.48.2");
static const dateOfBirth = const OID._internal("1.3.6.1.5.5.7.9.1");
static const desCBC = const OID._internal("1.3.14.3.2.7");
static const sha1 = const OID._internal("1.3.14.3.2.26");
static const sha256 = const OID._internal("2.16.840.1.101.3.4.2.1");
static const sha384 = const OID._internal("2.16.840.1.101.3.4.2.2");
static const sha512 = const OID._internal("2.16.840.1.101.3.4.2.3");
static const md5 = const OID._internal("1.2.840.113549.2.5");
static const VeriSignEVpolicy = const OID._internal("2.16.840.1.113733.1.7.23.6");
static const extendedValidation = const OID._internal("2.23.140.1.1");
static const organizationValidated = const OID._internal("2.23.140.1.2.2");
static const subjectKeyIdentifier = const OID._internal("2.5.29.14");
static const keyUsage = const OID._internal("2.5.29.15");
static const subjectAltName = const OID._internal("2.5.29.17");
static const issuerAltName = const OID._internal("2.5.29.18");
static const basicConstraints = const OID._internal("2.5.29.19");
static const cRLDistributionPoints = const OID._internal("2.5.29.31");
static const certificatePolicies = const OID._internal("2.5.29.32");
static const authorityKeyIdentifier = const OID._internal("2.5.29.35");
static const extKeyUsage = const OID._internal("2.5.29.37");
static const subjectDirectoryAttributes = const OID._internal("2.5.29.9");
static const organizationName = const OID._internal("2.5.4.10");
static const organizationalUnitName = const OID._internal("2.5.4.11");
static const businessCategory = const OID._internal("2.5.4.15");
static const postalCode = const OID._internal("2.5.4.17");
static const commonName = const OID._internal("2.5.4.3");
static const surname = const OID._internal("2.5.4.4");
static const givenName = const OID._internal("2.5.4.42");
static const dnQualifier = const OID._internal("2.5.4.46");
static const serialNumber = const OID._internal("2.5.4.5");
static const countryName = const OID._internal("2.5.4.6");
static const localityName = const OID._internal("2.5.4.7");
static const stateOrProvinceName = const OID._internal("2.5.4.8");
static const streetAddress = const OID._internal("2.5.4.9");
static const desEDE3CBC = const OID._internal("1.2.840.113549.3.7");
static const aes128CBC = const OID._internal("2.16.840.1.101.3.4.1.2");
static const aes192CBC = const OID._internal("2.16.840.1.101.3.4.1.22");
static const aes256CBC = const OID._internal("2.16.840.1.101.3.4.1.42");
static const nsCertType = const OID._internal("2.16.840.1.113730.1.1");
static const nsComment = const OID._internal("2.16.840.1.113730.1.13");
static const privateKeyUsagePeriod = const OID._internal("2.5.29.16");
static const cRLNumber = const OID._internal("2.5.29.20");
static const cRLReason = const OID._internal("2.5.29.21");
static const expirationDate = const OID._internal("2.5.29.22");
static const instructionCode = const OID._internal("2.5.29.23");
static const invalidityDate = const OID._internal("2.5.29.24");
static const deltaCRLIndicator = const OID._internal("2.5.29.27");
static const issuingDistributionPoint = const OID._internal("2.5.29.28");
static const certificateIssuer = const OID._internal("2.5.29.29");
static const nameConstraints = const OID._internal("2.5.29.30");
static const policyMappings = const OID._internal("2.5.29.33");
static const policyConstraints = const OID._internal("2.5.29.36");
static const freshestCRL = const OID._internal("2.5.29.46");
static const inhibitAnyPolicy = const OID._internal("2.5.29.54");
static const codeSigning = const OID._internal("1.3.6.1.5.5.7.3.3");
static const emailProtection = const OID._internal("1.3.6.1.5.5.7.3.4");
static const timeStamping = const OID._internal("1.3.6.1.5.5.7.3.8");
bool operator ==(value) => value == _value;
@override
int get hashCode => _value.hashCode;
static const Map<String, String> _oidMapName = {
"0.4.0.1862.1.1": "etsiQcsCompliance",
"0.4.0.1862.1.3": "etsiQcsRetentionPeriod",
"0.4.0.1862.1.4": "etsiQcsQcSSCD",
"1.2.840.10040.4.1": "dsa",
"1.2.840.10045.2.1": "ecPublicKey",
"1.2.840.10045.3.1.7": "prime256v1",
"1.2.840.10045.4.3.2": "ecdsaWithSHA256",
"1.2.840.10045.4.3.4": "ecdsaWithSHA512",
"1.2.840.113549.1.1.1": "rsaEncryption",
"1.2.840.113549.1.1.2": "md2WithRSAEncryption",
"1.2.840.113549.1.1.3": "md4WithRSAEncryption",
"1.2.840.113549.1.1.4": "md5WithRSAEncryption",
"1.2.840.113549.1.1.5": "sha1WithRSAEncryption",
"1.2.840.113549.1.1.7": "RSAES-OAEP",
".2.840.113549.1.1.8": "mgf1",
".2.840.113549.1.1.9": "pSpecified",
".2.840.113549.1.1.10": "RSASSA-PSS",
"1.2.840.113549.1.1.11": "sha256WithRSAEncryption",
"1.2.840.113549.1.1.12": "sha384WithRSAEncryption",
"1.2.840.113549.1.1.13": "sha512WithRSAEncryption",
"1.2.840.113549.1.7.1": "data",
"1.2.840.113549.1.7.2": "signedData",
"1.2.840.113549.1.9.1": "emailAddress",
"1.2.840.113549.1.9.16.2.47": "signingCertificateV2",
"1.2.840.113549.1.9.3": "contentType",
"1.2.840.113549.1.9.4": "messageDigest",
"1.2.840.113549.1.9.5": "signingTime",
"1.2.840.10040.4.3": 'dsa-with-sha1',
"1.3.6.1.4.1.11129.2.4.2": "certificateExtension",
"1.3.6.1.4.1.311.60.2.1.2": "jurisdictionOfIncorporationSP",
"1.3.6.1.4.1.311.60.2.1.3": "jurisdictionOfIncorporationC",
"1.3.6.1.5.5.7.1.1": "authorityInfoAccess",
"1.3.6.1.5.5.7.1.3": "qcStatements",
"1.3.6.1.5.5.7.2.1": "cps",
"1.3.6.1.5.5.7.2.2": "unotice",
"1.3.6.1.5.5.7.3.1": "serverAuth",
"1.3.6.1.5.5.7.3.2": "clientAuth",
"1.3.6.1.5.5.7.48.1": "ocsp",
"1.3.6.1.5.5.7.48.2": "caIssuers",
"1.3.6.1.5.5.7.9.1": "dateOfBirth",
"1.3.14.3.2.7": "desCBC",
"1.3.14.3.2.26": "sha1",
"2.16.840.1.101.3.4.2.1": "sha256",
"2.16.840.1.101.3.4.2.2": "sha384",
"2.16.840.1.101.3.4.2.3": "sha512",
"1.2.840.113549.2.5": "md5",
"2.16.840.1.113733.1.7.23.6": "VeriSign EV policy",
"2.23.140.1.1": "extendedValidation",
"2.23.140.1.2.2": "extendedValidation",
"2.5.29.14": "subjectKeyIdentifier",
"2.5.29.15": "keyUsage",
"2.5.29.17": "subjectAltName",
"2.5.29.18": "issuerAltName",
"2.5.29.19": "basicConstraints",
"2.5.29.31": "cRLDistributionPoints",
"2.5.29.32": "certificatePolicies",
"2.5.29.35": "authorityKeyIdentifier",
"2.5.29.37": "extKeyUsage",
"2.5.29.9": "subjectDirectoryAttributes",
"2.5.4.10": "organizationName",
"2.5.4.11": "organizationalUnitName",
"2.5.4.15": "businessCategory",
"2.5.4.17": "postalCode",
"2.5.4.3": "commonName",
"2.5.4.4": "surname",
"2.5.4.42": "givenName",
"2.5.4.46": "dnQualifier",
"2.5.4.5": "serialNumber",
"2.5.4.6": "countryName",
"2.5.4.7": "localityName",
"2.5.4.8": "stateOrProvinceName",
"2.5.4.9": "streetAddress",
"1.2.840.113549.3.7": "des-EDE3-CBC",
"2.16.840.1.101.3.4.1.2": "aes128-CBC",
"2.16.840.1.101.3.4.1.22": "aes192-CBC",
"2.16.840.1.101.3.4.1.42": "aes256-CBC",
"2.16.840.1.113730.1.1": "nsCertType",
"2.16.840.1.113730.1.13": "nsComment",
"2.5.29.16": "privateKeyUsagePeriod",
"2.5.29.20": "cRLNumber",
"2.5.29.21": "cRLReason",
"2.5.29.22": "expirationDate",
"2.5.29.23": "instructionCode",
"2.5.29.24": "invalidityDate",
"2.5.29.27": "deltaCRLIndicator",
"2.5.29.28": "issuingDistributionPoint",
"2.5.29.29": "certificateIssuer",
"2.5.29.30": "nameConstraints",
"2.5.29.33": "policyMappings",
"2.5.29.36": "policyConstraints",
"2.5.29.46": "freshestCRL",
"2.5.29.54": "inhibitAnyPolicy",
"1.3.6.1.5.5.7.3.3": "codeSigning",
"1.3.6.1.5.5.7.3.4": "emailProtection",
"1.3.6.1.5.5.7.3.8": "timeStamping",
};
}
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'asn1_decoder.dart';
import 'asn1_object.dart';
import 'oid.dart';
import 'x509_public_key.dart';
import 'x509_extension.dart';
import 'asn1_distinguished_names.dart';
///Class that represents a X.509 certificate.
///This provides a standard way to access all the attributes of an X.509 certificate.
class X509Certificate {
List<ASN1Object> asn1;
ASN1Object block1;
///Returns the encoded form of this certificate. It is
///assumed that each certificate type would have only a single
///form of encoding; for example, X.509 certificates would
///be encoded as ASN.1 DER.
Uint8List encoded;
static const beginPemBlock = "-----BEGIN CERTIFICATE-----";
static const endPemBlock = "-----END CERTIFICATE-----";
X509Certificate({ASN1Object asn1}) {
if (asn1 != null) {
var block1 = asn1.subAtIndex(0);
if (block1 == null) {
throw ASN1ParseError();
}
}
}
static X509Certificate fromData({@required Uint8List data}) {
var decoded = utf8.decode(data, allowMalformed: true);
if (decoded.contains(X509Certificate.beginPemBlock)) {
return X509Certificate.fromPemData(pem: data);
} else {
return X509Certificate.fromDerData(der: data);
}
}
static X509Certificate fromDerData({@required Uint8List der}) {
var asn1 = ASN1DERDecoder.decode(data: der.toList(growable: true));
if (asn1.length > 0) {
var block1 = asn1.first?.subAtIndex(0);
if (block1 != null) {
var certificate = X509Certificate();
certificate.asn1 = asn1;
certificate.block1 = block1;
certificate.encoded = der;
return certificate;
}
}
throw ASN1ParseError();
}
static X509Certificate fromPemData({@required Uint8List pem}) {
var derData = X509Certificate.decodeToDER(pemData: pem);
if (derData == null) {
throw ASN1ParseError();
}
return X509Certificate.fromDerData(der: derData);
}
///Read possible PEM encoding
static Uint8List decodeToDER({@required pemData}) {
var pem = String.fromCharCodes(pemData);
if (pem != null && pem.contains(X509Certificate.beginPemBlock)) {
var lines = pem.split("\n");
var base64buffer = "";
var certLine = false;
for (var line in lines) {
if (line == X509Certificate.endPemBlock) {
certLine = false;
}
if (certLine) {
base64buffer += line;
}
if (line == X509Certificate.beginPemBlock) {
certLine = true;
}
}
Uint8List derDataDecoded;
try {
derDataDecoded = Uint8List.fromList(utf8.encode(base64buffer));
} catch (e) {}
if (derDataDecoded != null) {
return derDataDecoded;
}
}
return null;
}
String get description =>
asn1.fold("", (value, element) => value + element.description + "\n");
///Checks that the given date is within the certificate's validity period.
bool checkValidity({DateTime date}) {
if (date == null) {
date = DateTime.now();
}
if (notBefore != null && notAfter != null) {
return date.isAfter(notBefore) && date.isBefore(notAfter);
}
return false;
}
///Gets the version (version number) value from the certificate.
int get version {
var v = firstLeafValue(block: block1) as List<int>;
if (v != null) {
var index = toIntValue(v);
if (index != null) {
return index.toInt() + 1;
}
}
return null;
}
///Gets the serialNumber value from the certificate.
List<int> get serialNumber =>
block1.atIndex(X509BlockPosition.serialNumber)?.value as List<int>;
///Returns the issuer (issuer distinguished name) value from the certificate as a String.
String get issuerDistinguishedName {
var issuerBlock = block1.atIndex(X509BlockPosition.issuer);
if (issuerBlock != null) {
return blockDistinguishedName(block: issuerBlock);
}
return null;
}
List<String> get issuerOIDs {
var result = <String>[];
var issuerBlock = block1.atIndex(X509BlockPosition.issuer);
if (issuerBlock != null) {
for (var sub in (issuerBlock.sub ?? <ASN1Object>[])) {
var value = firstLeafValue(block: sub) as String;
if (value != null) {
result.add(value);
}
}
}
return result;
}
String issuer({String oid, ASN1DistinguishedNames dn}) {
if (oid == null && dn != null) {
oid = dn.oid();
}
if (oid != null) {
var issuerBlock = block1.atIndex(X509BlockPosition.issuer);
if (issuerBlock != null) {
var oidBlock = issuerBlock.findOid(oidValue: oid);
if (oidBlock != null) {
var sub = oidBlock.parent?.sub;
if (sub != null && sub.length > 0) {
return sub.last.value as String;
} else {
return null;
}
}
}
}
return null;
}
///Returns the subject (subject distinguished name) value from the certificate as a String.
String get subjectDistinguishedName {
var subjectBlock = block1.atIndex(X509BlockPosition.subject);
if (subjectBlock != null) {
return blockDistinguishedName(block: subjectBlock);
}
return null;
}
List<String> get subjectOIDs {
var result = <String>[];
var subjectBlock = block1.atIndex(X509BlockPosition.subject);
if (subjectBlock != null) {
for (var sub in (subjectBlock.sub ?? <ASN1Object>[])) {
var value = firstLeafValue(block: sub) as String;
if (value != null) {
result.add(value);
}
}
}
return result;
}
String subject({String oid, ASN1DistinguishedNames dn}) {
if (oid == null && dn != null) {
oid = dn.oid();
}
if (oid != null) {
var subjectBlock = block1.atIndex(X509BlockPosition.subject);
if (subjectBlock != null) {
var oidBlock = subjectBlock.findOid(oidValue: oid);
if (oidBlock != null) {
var sub = oidBlock.parent?.sub;
if (sub != null && sub.length > 0) {
return sub.last.value as String;
} else {
return null;
}
}
}
}
return null;
}
///Gets the notBefore date from the validity period of the certificate.
DateTime get notBefore =>
block1.atIndex(X509BlockPosition.dateValidity)?.subAtIndex(0)?.value
as DateTime;
///Gets the notAfter date from the validity period of the certificate.
DateTime get notAfter {
var value = block1
.atIndex(X509BlockPosition.dateValidity)
?.subAtIndex(1)
?.value as DateTime;
return value;
}
/// Gets the signature value (the raw signature bits) from the certificate.
List<int> get signature => asn1[0].subAtIndex(2)?.value as List<int>;
/// Gets the signature algorithm name for the certificate signature algorithm.
String get sigAlgName => OID.fromValue(sigAlgOID ?? "")?.name();
/// Gets the signature algorithm OID string from the certificate.
String get sigAlgOID => block1.subAtIndex(2)?.subAtIndex(0)?.value as String;
/// Gets the DER-encoded signature algorithm parameters from this certificate's signature algorithm.
List<int> get sigAlgParams => null;
///Gets a boolean array representing bits of the KeyUsage extension, (OID = 2.5.29.15).
///```
///KeyUsage ::= BIT STRING {
///digitalSignature (0),
///nonRepudiation (1),
///keyEncipherment (2),
///dataEncipherment (3),
///keyAgreement (4),
///keyCertSign (5),
///cRLSign (6),
///encipherOnly (7),
///decipherOnly (8)
///}
///```
List<bool> get keyUsage {
var result = <bool>[];
var oidBlock = block1.findOid(oid: OID.keyUsage);
if (oidBlock != null) {
var sub = oidBlock.parent?.sub;
if (sub != null && sub.length > 0) {
var data = sub.last.subAtIndex(0)?.value as List<int>;
int bits = (data != null && data.length > 0) ? data.first ?? 0 : 0;
for (var index = 0; index < 7; index++) {
var value = bits & (1 << index).toUnsigned(8) != 0;
result.insert(0, value);
}
}
}
return result;
}
///Gets a list of Strings representing the OBJECT IDENTIFIERs of the ExtKeyUsageSyntax field of
///the extended key usage extension, (OID = 2.5.29.37).
List<String> get extendedKeyUsage =>
extensionObject(oid: OID.extKeyUsage)?.valueAsStrings ?? <String>[];
///Gets a collection of subject alternative names from the SubjectAltName extension, (OID = 2.5.29.17).
List<String> get subjectAlternativeNames =>
extensionObject(oid: OID.subjectAltName)?.valueAsStrings ?? <String>[];
///Gets a collection of issuer alternative names from the IssuerAltName extension, (OID = 2.5.29.18).
List<String> get issuerAlternativeNames =>
extensionObject(oid: OID.issuerAltName)?.valueAsStrings ?? <String>[];
///Gets the informations of the public key from this certificate.
X509PublicKey get publicKey {
var pkBlock = block1.atIndex(X509BlockPosition.publicKey);
if (pkBlock != null) {
return X509PublicKey(pkBlock: pkBlock);
}
return null;
}
///Get a list of critical extension OID codes
List<String> get criticalExtensionOIDs {
var extensionBlocks = this.extensionBlocks;
if (extensionBlocks == null) {
return <String>[];
}
return extensionBlocks
.map((block) => X509Extension(block: block))
.where((extension) => extension.isCritical)
.map((extension) => extension.oid)
.toList();
}
///Get a list of non critical extension OID codes
List<String> get nonCriticalExtensionOIDs {
var extensionBlocks = this.extensionBlocks;
if (extensionBlocks == null) {
return <String>[];
}
return extensionBlocks
.map((block) => X509Extension(block: block))
.where((extension) => !extension.isCritical)
.map((extension) => extension.oid)
.toList();
}
///Gets the certificate constraints path length from the
///critical BasicConstraints extension, (OID = 2.5.29.19).
int get basicConstraints => extensionObject(oid: OID.basicConstraints)?.value as int ?? -1;
List<ASN1Object> get extensionBlocks =>
block1.atIndex(X509BlockPosition.extensions)?.subAtIndex(0)?.sub;
///Gets the extension information of the given OID code or enum.
X509Extension extensionObject({String oidValue, OID oid}) {
if (oidValue == null && oid != null) {
oidValue = oid.toValue();
}
if (oidValue != null) {
var block = block1
.atIndex(X509BlockPosition.extensions)
?.findOid(oidValue: oidValue)
?.parent;
if (block != null) {
return X509Extension(block: block);
}
}
return null;
}
///Format subject/issuer information in RFC1779
String blockDistinguishedName({@required ASN1Object block}) {
var result = "";
List<ASN1DistinguishedNames> oidNames = [
ASN1DistinguishedNames.COMMON_NAME,
ASN1DistinguishedNames.DN_QUALIFIER,
ASN1DistinguishedNames.SERIAL_NUMBER,
ASN1DistinguishedNames.GIVEN_NAME,
ASN1DistinguishedNames.SURNAME,
ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME,
ASN1DistinguishedNames.ORGANIZATION_NAME,
ASN1DistinguishedNames.STREET_ADDRESS,
ASN1DistinguishedNames.LOCALITY_NAME,
ASN1DistinguishedNames.STATE_OR_PROVINCE_NAME,
ASN1DistinguishedNames.COUNTRY_NAME,
ASN1DistinguishedNames.EMAIL
];
for (var oidName in oidNames) {
var oidBlock = block.findOid(oidValue: oidName.oid());
if (oidBlock != null) {
if (result.isNotEmpty) {
result += ", ";
}
result += oidName.representation();
result += "=";
var sub = oidBlock.parent?.sub;
if (sub != null && sub.length > 0) {
var value = sub.last.value as String;
if (value != null) {
var specialChar = ",+=\n<>#;\\";
var quote = "";
for (var i = 0; i < value.length; i++) {
var char = value[i];
if (specialChar.contains(char)) {
quote = "\"";
}
}
result += quote;
result += value;
result += quote;
}
}
}
}
return result;
}
@override
String toString() {
return description;
}
Map<String, dynamic> toMap() {
return {
"basicConstraints": basicConstraints,
"subjectAlternativeNames": subjectAlternativeNames,
"issuerAlternativeNames": issuerAlternativeNames,
"extendedKeyUsage": extendedKeyUsage,
"issuerDistinguishedName": issuerDistinguishedName,
"keyUsage": keyUsage,
"notAfter": notAfter,
"notBefore": notBefore,
"serialNumber": serialNumber,
"sigAlgName": sigAlgName,
"sigAlgOID": sigAlgOID,
"sigAlgParams": sigAlgParams,
"signature": signature,
"subjectDistinguishedName": subjectDistinguishedName,
"version": version,
"criticalExtensionOIDs": criticalExtensionOIDs,
"nonCriticalExtensionOIDs": nonCriticalExtensionOIDs,
"encoded": encoded,
"publicKey": publicKey?.toMap(),
};
}
Map<String, dynamic> toJson() {
return toMap();
}
}
dynamic firstLeafValue({@required ASN1Object block}) {
var sub = block.sub;
if (sub != null && sub.length > 0) {
var subFirst = sub.first;
if (subFirst != null) {
return firstLeafValue(block: subFirst);
}
}
return block.value;
}
enum X509BlockPosition {
version,
serialNumber,
signatureAlg,
issuer,
dateValidity,
subject,
publicKey,
extensions
}
import 'x509_certificate.dart';
import 'asn1_object.dart';
import 'oid.dart';
class X509Extension {
ASN1Object block;
X509Extension({this.block});
String get oid => block.subAtIndex(0)?.value;
String get name => OID.fromValue(oid ?? "")?.name();
bool get isCritical {
if ((block.sub?.length ?? 0) > 2) {
return block.subAtIndex(1)?.value ?? false;
}
return false;
}
dynamic get value {
var sub = block.sub;
if (sub != null && sub.length > 0) {
var valueBlock = sub.last;
if (valueBlock != null) {
return firstLeafValue(block: valueBlock);
}
}
return null;
}
ASN1Object get valueAsBlock {
var sub = block.sub;
if (sub != null && sub.length > 0) {
return sub.last;
}
return null;
}
List<String> get valueAsStrings {
var result = <String>[];
var sub = <ASN1Object>[];
try {
sub = block.sub?.last?.sub?.last?.sub;
} catch (e) {}
for (var item in sub) {
var name = item.value;
if (name != null) {
result.add(name);
}
}
return result;
}
}
\ No newline at end of file
import 'dart:typed_data';
import 'asn1_decoder.dart';
import 'asn1_object.dart';
import 'oid.dart';
class X509PublicKey {
ASN1Object pkBlock;
X509PublicKey({this.pkBlock});
String get algOid => pkBlock?.subAtIndex(0)?.subAtIndex(0)?.value;
String get algName => OID.fromValue(algOid ?? "")?.name();
String get algParams => pkBlock?.subAtIndex(0)?.subAtIndex(1)?.value;
Uint8List get encoded {
var oid = OID.fromValue(algOid);
var keyData = pkBlock?.subAtIndex(1)?.value ?? null;
if (oid != null && algOid != null && keyData != null) {
if (oid == OID.ecPublicKey) {
return Uint8List.fromList(keyData);
} else if (oid == OID.rsaEncryption) {
List<ASN1Object> publicKeyAsn1Objects;
try {
publicKeyAsn1Objects = ASN1DERDecoder.decode(data: keyData.toList(growable: true));
} catch(e) {}
if (publicKeyAsn1Objects != null && publicKeyAsn1Objects.length > 0) {
var publicKeyModulus = publicKeyAsn1Objects.first?.subAtIndex(0)?.value;
if (publicKeyModulus != null) {
return Uint8List.fromList(publicKeyModulus);
}
}
}
}
return null;
}
Map<String, dynamic> toMap() {
return {
"algOid": algOid,
"algName": algName,
"algParams": algParams,
"encoded": encoded,
};
}
Map<String, dynamic> toJson() {
return toMap();
}
}
\ No newline at end of file
import 'dart:developer';
import 'dart:io';
import 'dart:async';
import 'dart:collection';
......@@ -9,6 +10,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'X509Certificate/asn1_distinguished_names.dart';
import 'X509Certificate/x509_certificate.dart';
import 'package:html/parser.dart' show parse;
......@@ -369,7 +372,37 @@ class InAppWebViewController {
int androidError = call.arguments["androidError"];
int iosError = call.arguments["iosError"];
String message = call.arguments["message"];
Uint8List serverCertificate = call.arguments["serverCertificate"];
Map<String, dynamic> sslCertificateMap = call.arguments["sslCertificate"]?.cast<String, dynamic>();
SslCertificate sslCertificate;
if (sslCertificateMap != null) {
if (Platform.isIOS) {
try {
X509Certificate x509certificate = X509Certificate.fromData(data: sslCertificateMap["x509Certificate"]);
sslCertificate = SslCertificate(
issuedBy: SslCertificateDName(
CName: x509certificate.issuer(dn: ASN1DistinguishedNames.COMMON_NAME) ?? "",
DName: x509certificate.issuerDistinguishedName ?? "",
OName: x509certificate.issuer(dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? "",
UName: x509certificate.issuer(dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME) ?? ""),
issuedTo: SslCertificateDName(
CName: x509certificate.subject(dn: ASN1DistinguishedNames.COMMON_NAME) ?? "",
DName: x509certificate.subjectDistinguishedName ?? "",
OName: x509certificate.subject(dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? "",
UName: x509certificate.subject(dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME) ?? ""),
validNotAfterDate: x509certificate.notAfter,
validNotBeforeDate: x509certificate.notBefore,
x509Certificate: x509certificate,
);
} catch(e, stacktrace) {
print(e);
print(stacktrace);
return null;
}
} else {
sslCertificate = SslCertificate.fromMap(sslCertificateMap);
}
}
AndroidSslError androidSslError = androidError != null ? AndroidSslError.fromValue(androidError) : null;
IOSSslError iosSslError = iosError != null ? IOSSslError.fromValue(iosError) : null;
......@@ -381,7 +414,7 @@ class InAppWebViewController {
androidError: androidSslError,
iosError: iosSslError,
message: message,
serverCertificate: serverCertificate);
sslCertificate: sslCertificate);
if (_webview != null &&
_webview.onReceivedServerTrustAuthRequest != null)
return (await _webview.onReceivedServerTrustAuthRequest(
......@@ -1655,6 +1688,46 @@ class InAppWebViewController {
return await _channel.invokeMethod('getScrollY', args);
}
///Gets the SSL certificate for the main top-level page or null if there is no certificate (the site is not secure).
///
///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#getCertificate()
Future<SslCertificate> getCertificate() async {
Map<String, dynamic> args = <String, dynamic>{};
Map<String, dynamic> sslCertificateMap = (await _channel.invokeMethod('getCertificate', args))?.cast<String, dynamic>();
if (sslCertificateMap != null) {
if (Platform.isIOS) {
try {
X509Certificate x509certificate = X509Certificate.fromData(data: sslCertificateMap["x509Certificate"]);
return SslCertificate(
issuedBy: SslCertificateDName(
CName: x509certificate.issuer(dn: ASN1DistinguishedNames.COMMON_NAME) ?? "",
DName: x509certificate.issuerDistinguishedName ?? "",
OName: x509certificate.issuer(dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? "",
UName: x509certificate.issuer(dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME) ?? ""),
issuedTo: SslCertificateDName(
CName: x509certificate.subject(dn: ASN1DistinguishedNames.COMMON_NAME) ?? "",
DName: x509certificate.subjectDistinguishedName ?? "",
OName: x509certificate.subject(dn: ASN1DistinguishedNames.ORGANIZATION_NAME) ?? "",
UName: x509certificate.subject(dn: ASN1DistinguishedNames.ORGANIZATIONAL_UNIT_NAME) ?? ""),
validNotAfterDate: x509certificate.notAfter,
validNotBeforeDate: x509certificate.notBefore,
x509Certificate: x509certificate,
);
} catch(e, stacktrace) {
print(e);
print(stacktrace);
return null;
}
} else {
return SslCertificate.fromMap(sslCertificateMap);
}
}
return null;
}
///Gets the default user agent.
///
///**Official Android API**: https://developer.android.com/reference/android/webkit/WebSettings#getDefaultUserAgent(android.content.Context)
......@@ -1794,16 +1867,6 @@ class AndroidInAppWebViewController {
return await _controller._channel.invokeMethod('clearHistory', args);
}
///Gets the SSL certificate for the main top-level page or null if there is no certificate (the site is not secure).
///
///**Official Android API**: https://developer.android.com/reference/android/webkit/WebView#getCertificate()
Future<AndroidSslCertificate> getCertificate() async {
Map<String, dynamic> args = <String, dynamic>{};
var sslCertificateMap = (await _controller._channel.invokeMethod('getCertificate', args))?.cast<String, dynamic>();
return sslCertificateMap != null ? AndroidSslCertificate.fromMap(sslCertificateMap) : null;
}
///Clears the client certificate preferences stored in response to proceeding/cancelling client cert requests.
///Note that WebView automatically clears these preferences when the system keychain is updated.
///The preferences are shared by all the WebViews that are created by the embedder application.
......
......@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:typed_data';
import 'dart:convert';
import 'X509Certificate/x509_certificate.dart';
import 'package:uuid/uuid.dart';
import 'package:flutter/foundation.dart';
......@@ -924,15 +925,15 @@ class ServerTrustChallenge {
///**NOTE**: on iOS this value is always an empty string.
String message;
///The `X509Certificate` used to create the server SSL certificate.
Uint8List serverCertificate;
///The SSL certificate used for this challenge.
SslCertificate sslCertificate;
ServerTrustChallenge(
{@required this.protectionSpace,
this.androidError,
this.iosError,
this.message,
this.serverCertificate})
this.sslCertificate})
: assert(protectionSpace != null);
Map<String, dynamic> toMap() {
......@@ -941,7 +942,7 @@ class ServerTrustChallenge {
"androidError": androidError?.toValue(),
"iosError": iosError?.toValue(),
"message": message,
"serverCertificate": serverCertificate
"sslCertificate": sslCertificate?.toMap()
};
}
......@@ -3741,14 +3742,25 @@ class IOSUIScrollViewContentInsetAdjustmentBehavior {
int get hashCode => _value.hashCode;
}
class AndroidSslCertificate {
AndroidSslCertificateDName issuedBy;
AndroidSslCertificateDName issuedTo;
int validNotAfterDate;
int validNotBeforeDate;
AndroidX509Certificate x509Certificate;
///SSL certificate info (certificate details) class.
class SslCertificate {
AndroidSslCertificate({
///Name of the entity this certificate is issued by
SslCertificateDName issuedBy;
///Name of the entity this certificate is issued to
SslCertificateDName issuedTo;
///Not-after date from the validity period
DateTime validNotAfterDate;
///Not-before date from the validity period
DateTime validNotBeforeDate;
///The original source certificate, if available.
X509Certificate x509Certificate;
SslCertificate({
this.issuedBy,
this.issuedTo,
this.validNotAfterDate,
......@@ -3756,13 +3768,21 @@ class AndroidSslCertificate {
this.x509Certificate
});
static AndroidSslCertificate fromMap(Map<String, dynamic> map) {
return map != null ? AndroidSslCertificate(
issuedBy: AndroidSslCertificateDName.fromMap(map["issuedBy"]?.cast<String, dynamic>()),
issuedTo: AndroidSslCertificateDName.fromMap(map["issuedTo"]?.cast<String, dynamic>()),
validNotAfterDate: map["validNotAfterDate"],
validNotBeforeDate: map["validNotBeforeDate"],
x509Certificate: AndroidX509Certificate.fromMap(map["x509Certificate"]?.cast<String, dynamic>()),
static SslCertificate fromMap(Map<String, dynamic> map) {
X509Certificate x509Certificate;
try {
x509Certificate = X509Certificate.fromData(data: map["x509Certificate"]);
} catch (e, stacktrace) {
print(e);
print(stacktrace);
}
return map != null ? SslCertificate(
issuedBy: SslCertificateDName.fromMap(map["issuedBy"]?.cast<String, dynamic>()),
issuedTo: SslCertificateDName.fromMap(map["issuedTo"]?.cast<String, dynamic>()),
validNotAfterDate: DateTime.fromMillisecondsSinceEpoch(map["validNotAfterDate"]),
validNotBeforeDate: DateTime.fromMillisecondsSinceEpoch(map["validNotBeforeDate"]),
x509Certificate: x509Certificate,
) : null;
}
......@@ -3770,8 +3790,8 @@ class AndroidSslCertificate {
return {
"issuedBy": issuedBy?.toMap(),
"issuedTo": issuedTo?.toMap(),
"validNotAfterDate": validNotAfterDate,
"validNotBeforeDate": validNotBeforeDate,
"validNotAfterDate": validNotAfterDate.millisecondsSinceEpoch,
"validNotBeforeDate": validNotBeforeDate.millisecondsSinceEpoch,
"x509Certificate": x509Certificate?.toMap(),
};
}
......@@ -3786,25 +3806,30 @@ class AndroidSslCertificate {
}
}
class AndroidSslCertificateDName {
///Distinguished name helper class. Used by [SslCertificate].
class SslCertificateDName {
///Common-name (CN) component of the name
// ignore: non_constant_identifier_names
String CName;
///Distinguished name (normally includes CN, O, and OU names)
// ignore: non_constant_identifier_names
String DName;
///Organization (O) component of the name
// ignore: non_constant_identifier_names
String OName;
///Organizational Unit (OU) component of the name
// ignore: non_constant_identifier_names
String UName;
// ignore: non_constant_identifier_names
AndroidSslCertificateDName({this.CName, this.DName, this.OName, this.UName});
static AndroidSslCertificateDName fromMap(Map<String, dynamic> map) {
return map != null ? AndroidSslCertificateDName(
CName: map["CName"],
DName: map["DName"],
OName: map["OName"],
UName: map["UName"],
SslCertificateDName({this.CName = "", this.DName = "", this.OName = "", this.UName = ""});
static SslCertificateDName fromMap(Map<String, dynamic> map) {
return map != null ? SslCertificateDName(
CName: map["CName"] ?? "",
DName: map["DName"] ?? "",
OName: map["OName"] ?? "",
UName: map["UName"] ?? "",
) : null;
}
......@@ -3826,229 +3851,3 @@ class AndroidSslCertificateDName {
return toMap().toString();
}
}
class AndroidX509Certificate {
int basicConstraints;
List<String> extendedKeyUsage;
AndroidX509CertificatePrincipal issuerDN;
List<bool> issuerUniqueID;
AndroidX500Principal issuerX500Principal;
List<bool> keyUsage;
int notAfter;
int notBefore;
int serialNumber;
String sigAlgName;
String sigAlgOID;
Uint8List sigAlgParams;
Uint8List signature;
AndroidX509CertificatePrincipal subjectDN;
List<bool> subjectUniqueID;
AndroidX500Principal subjectX500Principal;
// ignore: non_constant_identifier_names
Uint8List TBSCertificate;
int version;
Set<String> criticalExtensionOIDs;
Set<String> nonCriticalExtensionOIDs;
Uint8List encoded;
AndroidX509CertificatePublicKey publicKey;
String type;
bool hasUnsupportedCriticalExtension;
bool valid;
AndroidX509Certificate({
this.basicConstraints,
this.extendedKeyUsage,
this.issuerDN,
this.issuerUniqueID,
this.issuerX500Principal,
this.keyUsage,
this.notAfter,
this.notBefore,
this.serialNumber,
this.sigAlgName,
this.sigAlgOID,
this.sigAlgParams,
this.signature,
this.subjectDN,
this.subjectUniqueID,
this.subjectX500Principal,
// ignore: non_constant_identifier_names
this.TBSCertificate,
this.version,
this.criticalExtensionOIDs,
this.nonCriticalExtensionOIDs,
this.encoded,
this.publicKey,
this.type,
this.hasUnsupportedCriticalExtension,
this.valid
});
static AndroidX509Certificate fromMap(Map<String, dynamic> map) {
return map != null ? AndroidX509Certificate(
basicConstraints: map["basicConstraints"],
extendedKeyUsage: map["extendedKeyUsage"],
issuerDN: AndroidX509CertificatePrincipal.fromMap(map["issuerDN"]),
issuerUniqueID: map["issuerUniqueID"],
issuerX500Principal: AndroidX500Principal.fromMap(map["issuerX500Principal"]),
keyUsage: map["keyUsage"],
notAfter: map["notAfter"],
notBefore: map["notBefore"],
serialNumber: map["serialNumber"],
sigAlgName: map["sigAlgName"],
sigAlgOID: map["sigAlgOID"],
sigAlgParams: map["sigAlgParams"],
signature: map["signature"],
subjectDN: AndroidX509CertificatePrincipal.fromMap(map["subjectDN"]),
subjectUniqueID: map["subjectUniqueID"],
subjectX500Principal: AndroidX500Principal.fromMap(map["subjectX500Principal"]),
TBSCertificate: map["TBSCertificate"],
version: map["version"],
criticalExtensionOIDs: map["criticalExtensionOIDs"],
nonCriticalExtensionOIDs: map["nonCriticalExtensionOIDs"],
encoded: map["encoded"],
publicKey: AndroidX509CertificatePublicKey.fromMap(map["publicKey"]),
type: map["type"],
hasUnsupportedCriticalExtension: map["hasUnsupportedCriticalExtension"],
valid: map["valid"]
) : null;
}
Map<String, dynamic> toMap() {
return {
"basicConstraints": basicConstraints,
"extendedKeyUsage": extendedKeyUsage,
"issuerDN": issuerDN?.toMap(),
"issuerUniqueID": issuerUniqueID,
"issuerX500Principal": issuerX500Principal?.toMap(),
"keyUsage": keyUsage,
"notAfter": notAfter,
"notBefore": notBefore,
"serialNumber": serialNumber,
"sigAlgName": sigAlgName,
"sigAlgOID": sigAlgOID,
"sigAlgParams": sigAlgParams,
"signature": signature,
"subjectDN": subjectDN?.toMap(),
"subjectUniqueID": subjectUniqueID,
"subjectX500Principal": subjectX500Principal?.toMap(),
"TBSCertificate": TBSCertificate,
"version": version,
"criticalExtensionOIDs": criticalExtensionOIDs,
"nonCriticalExtensionOIDs": nonCriticalExtensionOIDs,
"encoded": encoded,
"publicKey": publicKey?.toMap(),
"type": type,
"hasUnsupportedCriticalExtension": hasUnsupportedCriticalExtension,
"valid": valid,
};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}
class AndroidX509CertificatePrincipal {
String name;
AndroidX509CertificatePrincipal({
this.name
});
static AndroidX509CertificatePrincipal fromMap(Map<String, dynamic> map) {
return map != null ? AndroidX509CertificatePrincipal(
name: map["name"],
) : null;
}
Map<String, dynamic> toMap() {
return {
"name": name,
};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}
class AndroidX500Principal {
String name;
Uint8List encoded;
AndroidX500Principal({
this.name,
this.encoded
});
static AndroidX500Principal fromMap(Map<String, dynamic> map) {
return map != null ? AndroidX500Principal(
name: map["name"],
encoded: map["encoded"],
) : null;
}
Map<String, dynamic> toMap() {
return {
"name": name,
"encoded": encoded,
};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}
class AndroidX509CertificatePublicKey {
String algorithm;
Uint8List encoded;
String format;
AndroidX509CertificatePublicKey({
this.algorithm,
this.encoded,
this.format
});
static AndroidX509CertificatePublicKey fromMap(Map<String, dynamic> map) {
return map != null ? AndroidX509CertificatePublicKey(
algorithm: map["algorithm"],
encoded: map["encoded"],
format: map["format"],
) : null;
}
Map<String, dynamic> toMap() {
return {
"algorithm": algorithm,
"encoded": encoded,
"format": format,
};
}
Map<String, dynamic> toJson() {
return this.toMap();
}
@override
String toString() {
return toMap().toString();
}
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment