From 9619941f366d9eb8dd0469261435b63f637cd0c9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 5 Oct 2021 18:48:03 -0400 Subject: [PATCH] Support for ICAO's Visible Digital Seals --- app/components/VDSTestCard.js | 164 ++++++++++++++++++++++++++++++++++ app/components/VDSVaxCard.js | 163 +++++++++++++++++++++++++++++++++ app/screens/Entry.js | 6 ++ app/screens/QRReader.js | 7 +- app/screens/QRResult.js | 4 + app/utils/ImportVDS.js | 29 ++++++ package-lock.json | 33 +++++++ package.json | 1 + 8 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 app/components/VDSTestCard.js create mode 100644 app/components/VDSVaxCard.js create mode 100644 app/utils/ImportVDS.js diff --git a/app/components/VDSTestCard.js b/app/components/VDSTestCard.js new file mode 100644 index 0000000..580fbee --- /dev/null +++ b/app/components/VDSTestCard.js @@ -0,0 +1,164 @@ +import React, {Component} from 'react'; +import { View, Image, Button, FlatList, TouchableOpacity } from 'react-native'; +import { Text, Divider } from 'react-native-elements'; +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; + +import { CardStyles as styles } from '../themes/CardStyles' + +import Moment from 'moment'; + +const DISEASE = { + "RA01":"COVID-19", + "RA01.0":"COVID-19" +}; + +const VACCINE_TYPES = { + "XM68M6": "Unspecified", + "XM1NL1": "Inactivated virus", + "XM5DF6": "Live attenuated virus",  + "XM9QW8": "Non-replicating viral vector",  + "XM0CX4": "Replicating viral vector", + "XM5JC5": "Virus protein subunit", + "XM1J92": "Virus-like particle (VLP)",  + "XM6AT1": "DNA based",  + "XM0GQ8": "RNA based" +} + + + +export default class VDSTestCard extends Component { + + showQR = (card) => { + this.props.navigation.navigate({name: 'QRShow', params: { + qr: card.rawQR, + title: this.formatPerson(), + detail: this.formatCI(), + signedBy: this.formatSignedBy() + } + }); + } + + cert = () => { + return this.props.detail.cert ? this.props.detail.cert : this.props.detail; + } + + formatDoB = () => { + if (this.cert().data.msg.pid.dob === undefined || this.cert().data.msg.pid.dob === "") return ""; + return "DoB: " + Moment(this.cert().data.msg.pid.dob).format('MMM DD, YYYY') + } + + formatCI = () => { + return "ID: " + this.cert().data.msg.pid.i; + } + + formatUVCI = () => { + return "Vax ID: " + this.cert().data.msg.uvci; + } + + formatExpiresOn = () => { + if (this.cert().exp === undefined || this.cert().exp === "") return ""; + return Moment(this.cert().exp*1000).format('MMM DD, YYYY') + } + + formatPerson = () => { + if (this.cert().data.msg.pid.n) { + let name = this.cert().data.msg.pid.n.replace(" ", ", "); + names = name.split(" "); + for (i=2; i { + let line = "Signed by "; + if (this.cert().data.hdr.is) + line += this.cert().data.hdr.is; + else + line += this.props.detail.pub_key.toLowerCase(); + + return line; + } + + renderCard = () => { + return ( + + + {Moment(this.props.detail.scanDate).format('MMM DD, hh:mma')} - Vaccination + this.props.removeItem(this.props.detail.signature)} solid/> + + + + {this.formatPerson()} + + + + {this.formatDoB()}. {this.formatCI()} + + + + {this.formatUVCI()} + + + + + (this.props.detail.signature+item.des)} + renderItem={({item}) => { + return ( + + + (this.props.detail.signature+item.des+subitem.dvc)} + renderItem={ (subitem) => { + console.log(subitem); + return ( + + + + {DISEASE[item.dis]} Vaccine {subitem.item.seq}/{item.vd.length} + + + + {item.nam} (#{subitem.item.lot}) + + + + Date: {subitem.item.dvc} + + + + Location: {subitem.item.adm}, {subitem.item.ctr} + + + + + ) + }} /> + + ) + }} /> + + + + {this.formatSignedBy()} + + + ); + } + + + render() { + return this.props.pressable ? + ( this.showQR(this.props.detail)}> + {this.renderCard()} + + ) : this.renderCard(); + } +} \ No newline at end of file diff --git a/app/components/VDSVaxCard.js b/app/components/VDSVaxCard.js new file mode 100644 index 0000000..bbf5daa --- /dev/null +++ b/app/components/VDSVaxCard.js @@ -0,0 +1,163 @@ +import React, {Component} from 'react'; +import { View, Image, Button, FlatList, TouchableOpacity } from 'react-native'; +import { Text, Divider } from 'react-native-elements'; +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; + +import { CardStyles as styles } from '../themes/CardStyles' + +import Moment from 'moment'; + +const DISEASE = { + "RA01":"COVID-19", + "RA01.0":"COVID-19" +}; + +const VACCINE_TYPES = { + "XM68M6": "Unspecified", + "XM1NL1": "Inactivated virus", + "XM5DF6": "Live attenuated virus",  + "XM9QW8": "Non-replicating viral vector",  + "XM0CX4": "Replicating viral vector", + "XM5JC5": "Virus protein subunit", + "XM1J92": "Virus-like particle (VLP)",  + "XM6AT1": "DNA based",  + "XM0GQ8": "RNA based" +} + + + +export default class VDSVaxCard extends Component { + + showQR = (card) => { + this.props.navigation.navigate({name: 'QRShow', params: { + qr: card.rawQR, + title: this.formatPerson(), + detail: this.formatCI(), + signedBy: this.formatSignedBy() + } + }); + } + + cert = () => { + return this.props.detail.cert ? this.props.detail.cert : this.props.detail; + } + + formatDoB = () => { + if (this.cert().data.msg.pid.dob === undefined || this.cert().data.msg.pid.dob === "") return ""; + return "DoB: " + Moment(this.cert().data.msg.pid.dob).format('MMM DD, YYYY') + } + + formatCI = () => { + return "ID: " + this.cert().data.msg.pid.i; + } + + formatUVCI = () => { + return "Vax ID: " + this.cert().data.msg.uvci; + } + + formatExpiresOn = () => { + if (this.cert().exp === undefined || this.cert().exp === "") return ""; + return Moment(this.cert().exp*1000).format('MMM DD, YYYY') + } + + formatPerson = () => { + if (this.cert().data.msg.pid.n) { + let name = this.cert().data.msg.pid.n.replace(" ", ", "); + names = name.split(" "); + for (i=2; i { + let line = "Signed by "; + if (this.cert().data.hdr.is) + line += this.cert().data.hdr.is; + else + line += this.props.detail.pub_key.toLowerCase(); + + return line; + } + + renderCard = () => { + return ( + + + {Moment(this.props.detail.scanDate).format('MMM DD, hh:mma')} - Vaccination + this.props.removeItem(this.props.detail.signature)} solid/> + + + + {this.formatPerson()} + + + + {this.formatDoB()}. {this.formatCI()} + + + + {this.formatUVCI()} + + + + + this.props.detail.signature+item.nam} + renderItem={({item}) => { + return ( + + + this.props.detail.signature+item.nam+subitem.dvc} + renderItem={ (subitem) => { + return ( + + + + {DISEASE[item.dis]} Vaccine {subitem.item.seq}/{item.vd.length} + + + + {item.nam} (#{subitem.item.lot}) + + + + Date: {subitem.item.dvc} + + + + Location: {subitem.item.adm}, {subitem.item.ctr} + + + + + ) + }} /> + + ) + }} /> + + + + {this.formatSignedBy()} + + + ); + } + + + render() { + return this.props.pressable ? + ( this.showQR(this.props.detail)}> + {this.renderCard()} + + ) : this.renderCard(); + } +} \ No newline at end of file diff --git a/app/screens/Entry.js b/app/screens/Entry.js index 527d3e5..d20f536 100644 --- a/app/screens/Entry.js +++ b/app/screens/Entry.js @@ -18,6 +18,8 @@ import PassKeyCard from './../components/PassKeyCard'; import SHCCard from './../components/SHCCard'; import DCCCard from './../components/DCCCard'; import DCCUYCard from './../components/DCCUYCard'; +import VDSVaxCard from './../components/VDSVaxCard'; +import VDSTestCard from './../components/VDSTestCard'; import { listCards, removeCard } from './../utils/StorageManager'; @@ -121,6 +123,10 @@ function Entry({ navigation }) { return if (item.format === "DCC" && item.type === "UY") return + if (item.format === "VDS" && item.type === "icao.vacc") + return + if (item.format === "VDS" && item.type === "icao.test") + return }} /> diff --git a/app/screens/QRReader.js b/app/screens/QRReader.js index 80e5825..06358d7 100644 --- a/app/screens/QRReader.js +++ b/app/screens/QRReader.js @@ -9,6 +9,7 @@ import {importPCF} from '../utils/ImportPCF'; import {importDivoc} from '../utils/ImportDivoc'; import {importSHC} from '../utils/ImportSHC'; import {importDCC} from '../utils/ImportDCC'; +import {importVDS} from '../utils/ImportVDS'; const screenHeight = Math.round(Dimensions.get('window').height); @@ -65,7 +66,11 @@ function QRReader({ navigation }) { } if (e.data && e.data.startsWith("{")) { - await checkResult(await importDivoc(e.data)); + if (e.data.includes("icao")) { + await checkResult(await importVDS(e.data)); + } else { + await checkResult(await importDivoc(e.data)); + } return; } diff --git a/app/screens/QRResult.js b/app/screens/QRResult.js index 66d2963..098cde8 100644 --- a/app/screens/QRResult.js +++ b/app/screens/QRResult.js @@ -15,6 +15,8 @@ import PassKeyCard from './../components/PassKeyCard'; import SHCCard from './../components/SHCCard'; import DCCCard from './../components/DCCCard'; import DCCUYCard from './../components/DCCUYCard'; +import VDSVAXCard from './../components/VDSVaxCard'; +import VDSTESTCard from './../components/VDSTestCard'; import { removeCard } from './../utils/StorageManager'; @@ -56,6 +58,8 @@ function QRResult({ navigation, route }) { { qr.type === "FHIRBundle" && } { qr.type === "DCC" && } { qr.type === "UY" && } + { qr.type === "icao.vacc" && } + { qr.type === "icao.test" && } { + let payload = await VDS.unpackAndVerify(certificateData); + + if (payload) { + let baseCard = { + format: "VDS", + type: payload.data.hdr.t, + pub_key: payload.sig.cer, + signature: payload.sig.sigvl, + scanDate: new Date().toJSON(), + verified: "Valid", + rawQR: certificateData + }; + + baseCard.cert = payload; + + await saveCard(baseCard); + + return {status: "OK", payload: baseCard}; + } else { + return {status: "Could not verify"}; + } +} + +export { importVDS } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 789d411..e58f35d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@pathcheck/dcc-sdk": "^0.0.21", "@pathcheck/divoc-sdk": "^0.1.1", "@pathcheck/shc-sdk": "^0.0.3", + "@pathcheck/vds-sdk": "^0.0.5", "@react-native-async-storage/async-storage": "^1.15.4", "@react-navigation/native": "^5.9.4", "@react-navigation/stack": "^5.14.4", @@ -3313,6 +3314,17 @@ "pako": "^2.0.3" } }, + "node_modules/@pathcheck/vds-sdk": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@pathcheck/vds-sdk/-/vds-sdk-0.0.5.tgz", + "integrity": "sha512-dJNRN5rG4Q510O3QmxyOAfH/iV/9tIpsegY0tc8rgPLzZst0hpYqhAq677lSUAtCaU49eh1MtNSwD7T/D9c7ZQ==", + "dependencies": { + "@fidm/asn1": "^1.0.4", + "esm": "^3.2.25", + "isomorphic-webcrypto": "^2.3.8", + "json-canonicalize": "^1.0.4" + } + }, "node_modules/@peculiar/asn1-schema": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.38.tgz", @@ -11744,6 +11756,11 @@ "node": ">=4" } }, + "node_modules/json-canonicalize": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/json-canonicalize/-/json-canonicalize-1.0.4.tgz", + "integrity": "sha512-YNr/ePzgReHwlnAm3EVV1pcimwesI+1DZr5v7WBKOc1zE1t7pjxWAPRxJFT3ll6flLIdRe0DPia/8cl2FLAZNA==" + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -20325,6 +20342,17 @@ "pako": "^2.0.3" } }, + "@pathcheck/vds-sdk": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@pathcheck/vds-sdk/-/vds-sdk-0.0.5.tgz", + "integrity": "sha512-dJNRN5rG4Q510O3QmxyOAfH/iV/9tIpsegY0tc8rgPLzZst0hpYqhAq677lSUAtCaU49eh1MtNSwD7T/D9c7ZQ==", + "requires": { + "@fidm/asn1": "^1.0.4", + "esm": "^3.2.25", + "isomorphic-webcrypto": "^2.3.8", + "json-canonicalize": "^1.0.4" + } + }, "@peculiar/asn1-schema": { "version": "2.0.38", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.0.38.tgz", @@ -26824,6 +26852,11 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, + "json-canonicalize": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/json-canonicalize/-/json-canonicalize-1.0.4.tgz", + "integrity": "sha512-YNr/ePzgReHwlnAm3EVV1pcimwesI+1DZr5v7WBKOc1zE1t7pjxWAPRxJFT3ll6flLIdRe0DPia/8cl2FLAZNA==" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", diff --git a/package.json b/package.json index be84aab..201d4da 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@pathcheck/dcc-sdk": "^0.0.21", "@pathcheck/divoc-sdk": "^0.1.2", "@pathcheck/shc-sdk": "^0.0.3", + "@pathcheck/vds-sdk": "^0.0.5", "@react-native-async-storage/async-storage": "^1.15.4", "@react-navigation/native": "^5.9.4", "@react-navigation/stack": "^5.14.4",