├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── sample-03-rs ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── config.rs │ ├── crypto.rs │ ├── error.rs │ ├── key.rs │ └── main.rs ├── sample-03 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── data │ └── db.template.json ├── package.json ├── src │ ├── common │ │ ├── comm.js │ │ ├── env.js │ │ ├── index.js │ │ ├── key.js │ │ ├── params.js │ │ └── post-get.js │ ├── encrypt-browser.js │ ├── encrypt-node.js │ ├── encrypt-universal.js │ ├── post-get-browser.html │ ├── post-get-node.js │ └── registered-data-list.html ├── webpack.baseconfig.json └── webpack.config.js ├── sample-04-rs ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── config.rs │ ├── crypto.rs │ ├── error.rs │ ├── key.rs │ └── main.rs ├── sample-04 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── data │ └── db.template.json ├── package.json ├── src │ ├── derive-key.js │ ├── encrypt.js │ ├── index.js │ ├── post-get-browser.html │ ├── post-get-node.js │ ├── post-get.js │ └── util │ │ ├── comm.js │ │ ├── env.js │ │ ├── params.js │ │ └── pkcs7.js ├── webpack.baseconfig.json └── webpack.config.js ├── sample-05-rs ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── config.rs │ ├── crypto.rs │ ├── ecc.rs │ ├── error.rs │ ├── key.rs │ ├── main.rs │ ├── rsa.rs │ └── util.rs ├── sample-05 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── package.json ├── src │ ├── commands-browser.html │ ├── commands-node.js │ ├── derive-key.js │ ├── encryptAES.js │ ├── index.js │ ├── test-apis.js │ └── util │ │ ├── env.js │ │ └── format.js ├── webpack.baseconfig.json └── webpack.config.js ├── sample-06-rs ├── .gitignore ├── Cargo.toml ├── README.md └── src │ ├── config.rs │ ├── ecc.rs │ ├── error.rs │ ├── hash.rs │ ├── main.rs │ ├── rsa.rs │ └── util.rs ├── sample-06 ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── package.json ├── src │ ├── commands-browser.html │ ├── commands-node.js │ ├── index.js │ ├── test-apis.js │ └── util │ │ ├── env.js │ │ └── format.js ├── webpack.baseconfig.json └── webpack.config.js ├── sample-07-09-rs ├── .dockerignore ├── Cargo.toml ├── README.md ├── assets │ ├── auth.js │ ├── fido2testlib.bundle.js │ └── index.html ├── docker │ ├── .env.example │ ├── Dockerfile │ ├── docker-compose.yml │ ├── entrypoint.sh │ └── run.sh ├── frontend-lib │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── karma.conf.js │ ├── package.json │ ├── sample.crt │ ├── sample.html │ ├── src │ │ ├── assertion.ts │ │ ├── attestation.ts │ │ ├── credential.ts │ │ ├── env.ts │ │ ├── index.ts │ │ └── util.ts │ ├── test │ │ ├── credential-params.ts │ │ ├── misc.spec.ts │ │ ├── prepare.ts │ │ └── test.spec.ts │ ├── tsconfig.json │ ├── webpack.baseconfig.js │ ├── webpack.config.js │ └── webpack.tsconfig.json ├── src │ ├── constants.rs │ ├── error.rs │ ├── log.rs │ ├── main.rs │ ├── startup.rs │ └── webauthn.rs └── start.sh ├── slides2023-10.pdf ├── slides2023-11.pdf ├── slides2024-01.pdf ├── slides2024-02.pdf ├── slides2024-03.pdf ├── slides2024-04.pdf ├── slides2024-05.pdf ├── slides2024-06.pdf ├── slides2024-07-09.pdf └── slides2024-10.pdf /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic set up for three package managers 2 | 3 | version: 2 4 | updates: 5 | 6 | # Maintain dependencies for npm 7 | - package-ecosystem: "npm" 8 | directory: "/sample-03" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | prod-dependency: 13 | dependency-type: 'production' 14 | dev-dependency: 15 | dependency-type: 'development' 16 | 17 | - package-ecosystem: "npm" 18 | directory: "/sample-04" 19 | schedule: 20 | interval: "weekly" 21 | groups: 22 | prod-dependency: 23 | dependency-type: 'production' 24 | dev-dependency: 25 | dependency-type: 'development' 26 | 27 | - package-ecosystem: "npm" 28 | directory: "/sample-05" 29 | schedule: 30 | interval: "weekly" 31 | groups: 32 | prod-dependency: 33 | dependency-type: 'production' 34 | dev-dependency: 35 | dependency-type: 'development' 36 | 37 | - package-ecosystem: "npm" 38 | directory: "/sample-06" 39 | schedule: 40 | interval: "weekly" 41 | groups: 42 | prod-dependency: 43 | dependency-type: 'production' 44 | dev-dependency: 45 | dependency-type: 'development' 46 | 47 | - package-ecosystem: "cargo" 48 | directory: "/sample-03-rs" 49 | schedule: 50 | interval: "weekly" 51 | groups: 52 | prod-dependency: 53 | dependency-type: 'production' 54 | dev-dependency: 55 | dependency-type: 'development' 56 | 57 | - package-ecosystem: "cargo" 58 | directory: "/sample-04-rs" 59 | schedule: 60 | interval: "weekly" 61 | groups: 62 | prod-dependency: 63 | dependency-type: 'production' 64 | dev-dependency: 65 | dependency-type: 'development' 66 | 67 | - package-ecosystem: "cargo" 68 | directory: "/sample-05-rs" 69 | schedule: 70 | interval: "weekly" 71 | groups: 72 | prod-dependency: 73 | dependency-type: 'production' 74 | dev-dependency: 75 | dependency-type: 'development' 76 | 77 | - package-ecosystem: "cargo" 78 | directory: "/sample-06-rs" 79 | schedule: 80 | interval: "weekly" 81 | groups: 82 | prod-dependency: 83 | dependency-type: 'production' 84 | dev-dependency: 85 | dependency-type: 'development' 86 | 87 | - package-ecosystem: "cargo" 88 | directory: "/sample-07-09-rs" 89 | schedule: 90 | interval: "weekly" 91 | groups: 92 | prod-dependency: 93 | dependency-type: 'production' 94 | dev-dependency: 95 | dependency-type: 'development' 96 | 97 | - package-ecosystem: "cargo" 98 | directory: "/" 99 | schedule: 100 | interval: "weekly" 101 | groups: 102 | prod-dependency: 103 | dependency-type: 'production' 104 | dev-dependency: 105 | dependency-type: 'development' 106 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | - name: Run unit tests 21 | run: | 22 | cargo test --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | pnpm-lock.yaml 3 | Cargo.lock 4 | private/ 5 | private-*/ 6 | private_*/ 7 | obsolete/ 8 | .idea/ 9 | target/ 10 | .vscode 11 | .env 12 | log/ 13 | node_modules/ 14 | dist/ 15 | 16 | ## Core latex/pdflatex auxiliary files: 17 | *.aux 18 | *.lof 19 | *.log 20 | *.lot 21 | *.fls 22 | *.out 23 | *.toc 24 | *.fmt 25 | *.fot 26 | *.cb 27 | *.cb2 28 | .*.lb 29 | 30 | ## Intermediate documents: 31 | *.dvi 32 | *.xdv 33 | *-converted-to.* 34 | # these rules might exclude image files for figures etc. 35 | # *.ps 36 | # *.eps 37 | # *.pdf 38 | 39 | ## Generated if empty string is given at "Please type another file name for output:" 40 | .pdf 41 | 42 | ## Bibliography auxiliary files (bibtex/biblatex/biber): 43 | *.bbl 44 | *.bcf 45 | *.blg 46 | *-blx.aux 47 | *-blx.bib 48 | *.run.xml 49 | 50 | ## Build tool auxiliary files: 51 | *.fdb_latexmk 52 | *.synctex 53 | *.synctex(busy) 54 | *.synctex.gz 55 | *.synctex.gz(busy) 56 | *.pdfsync 57 | 58 | ## Build tool directories for auxiliary files 59 | # latexrun 60 | latex.out/ 61 | 62 | ## Auxiliary and intermediate files from other packages: 63 | # algorithms 64 | *.alg 65 | *.loa 66 | 67 | # achemso 68 | acs-*.bib 69 | 70 | # amsthm 71 | *.thm 72 | 73 | # beamer 74 | *.nav 75 | *.pre 76 | *.snm 77 | *.vrb 78 | 79 | # changes 80 | *.soc 81 | 82 | # comment 83 | *.cut 84 | 85 | # cprotect 86 | *.cpt 87 | 88 | # elsarticle (documentclass of Elsevier journals) 89 | *.spl 90 | 91 | # endnotes 92 | *.ent 93 | 94 | # fixme 95 | *.lox 96 | 97 | # feynmf/feynmp 98 | *.mf 99 | *.mp 100 | *.t[1-9] 101 | *.t[1-9][0-9] 102 | *.tfm 103 | 104 | #(r)(e)ledmac/(r)(e)ledpar 105 | *.end 106 | *.?end 107 | *.[1-9] 108 | *.[1-9][0-9] 109 | *.[1-9][0-9][0-9] 110 | *.[1-9]R 111 | *.[1-9][0-9]R 112 | *.[1-9][0-9][0-9]R 113 | *.eledsec[1-9] 114 | *.eledsec[1-9]R 115 | *.eledsec[1-9][0-9] 116 | *.eledsec[1-9][0-9]R 117 | *.eledsec[1-9][0-9][0-9] 118 | *.eledsec[1-9][0-9][0-9]R 119 | 120 | # glossaries 121 | *.acn 122 | *.acr 123 | *.glg 124 | *.glo 125 | *.gls 126 | *.glsdefs 127 | *.lzo 128 | *.lzs 129 | 130 | # uncomment this for glossaries-extra (will ignore makeindex's style files!) 131 | # *.ist 132 | 133 | # gnuplottex 134 | *-gnuplottex-* 135 | 136 | # gregoriotex 137 | *.gaux 138 | *.gtex 139 | 140 | # htlatex 141 | *.4ct 142 | *.4tc 143 | *.idv 144 | *.lg 145 | *.trc 146 | *.xref 147 | 148 | # hyperref 149 | *.brf 150 | 151 | # knitr 152 | *-concordance.tex 153 | # TODO Comment the next line if you want to keep your tikz graphics files 154 | *.tikz 155 | *-tikzDictionary 156 | 157 | # listings 158 | *.lol 159 | 160 | # luatexja-ruby 161 | *.ltjruby 162 | 163 | # makeidx 164 | *.idx 165 | *.ilg 166 | *.ind 167 | 168 | # minitoc 169 | *.maf 170 | *.mlf 171 | *.mlt 172 | *.mtc[0-9]* 173 | *.slf[0-9]* 174 | *.slt[0-9]* 175 | *.stc[0-9]* 176 | 177 | # minted 178 | _minted* 179 | *.pyg 180 | 181 | # morewrites 182 | *.mw 183 | 184 | # nomencl 185 | *.nlg 186 | *.nlo 187 | *.nls 188 | 189 | # pax 190 | *.pax 191 | 192 | # pdfpcnotes 193 | *.pdfpc 194 | 195 | # sagetex 196 | *.sagetex.sage 197 | *.sagetex.py 198 | *.sagetex.scmd 199 | 200 | # scrwfile 201 | *.wrt 202 | 203 | # sympy 204 | *.sout 205 | *.sympy 206 | sympy-plots-for-*.tex/ 207 | 208 | # pdfcomment 209 | *.upa 210 | *.upb 211 | 212 | # pythontex 213 | *.pytxcode 214 | pythontex-files-*/ 215 | 216 | # tcolorbox 217 | *.listing 218 | 219 | # thmtools 220 | *.loe 221 | 222 | # TikZ & PGF 223 | *.dpth 224 | *.md5 225 | *.auxlock 226 | 227 | # todonotes 228 | *.tdo 229 | 230 | # vhistory 231 | *.hst 232 | *.ver 233 | 234 | # easy-todo 235 | *.lod 236 | 237 | # xcolor 238 | *.xcp 239 | 240 | # xmpincl 241 | *.xmpi 242 | 243 | # xindy 244 | *.xdy 245 | 246 | # xypic precompiled matrices and outlines 247 | *.xyc 248 | *.xyd 249 | 250 | # endfloat 251 | *.ttt 252 | *.fff 253 | 254 | # Latexian 255 | TSWLatexianTemp* 256 | 257 | ## Editors: 258 | # WinEdt 259 | *.bak 260 | *.sav 261 | 262 | # Texpad 263 | .texpadtmp 264 | 265 | # LyX 266 | *.lyx~ 267 | 268 | # Kile 269 | *.backup 270 | 271 | # gummi 272 | .*.swp 273 | 274 | # KBibTeX 275 | *~[0-9]* 276 | 277 | # TeXnicCenter 278 | *.tps 279 | 280 | # auto folder when using emacs and auctex 281 | ./auto/* 282 | *.el 283 | 284 | # expex forward references with \gathertags 285 | *-tags.tex 286 | 287 | # standalone packages 288 | *.sta 289 | 290 | # Makeindex log files 291 | *.lpz 292 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "sample-03-rs", 5 | "sample-04-rs", 6 | "sample-05-rs", 7 | "sample-06-rs", 8 | "sample-07-09-rs", 9 | ] 10 | resolver = "2" 11 | 12 | [profile.release] 13 | codegen-units = 1 14 | incremental = false 15 | lto = "fat" 16 | opt-level = 3 17 | panic = "abort" 18 | strip = true 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Security Engineering 2 | 3 | Lecture slides and some program sources of a lecture ``Security Engineering'' at University of Hyogo 4 | 5 | ## Licence 6 | 7 | Sample codes are published under MIT Licence. 8 | 9 | サンプルコードはMITライセンスで頒布。 10 | 11 | Slides, their TeX source codes, images are published under [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode). 12 | 13 | スライドやそのソース、画像などは [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode) にて頒布。 14 | 15 | ## Contents 16 | 17 | - Standardized cryptographic techniques 18 | - FIDO2 WebAuthn 19 | - Hybrid Public Key Encryption in IETF 20 | 21 | Past years' content are contained in 202x branches as archives. 22 | 23 | 昨年度以前の資料は、202xブランチにアーカイブされています。 24 | -------------------------------------------------------------------------------- /sample-03-rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /sample-03-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli03" 3 | authors = ["Jun Kurihara"] 4 | description = "Rust version of sample-03" 5 | repository = "https://github.com/junkurihara/lecture-security_engineering" 6 | version = "0.1.0" 7 | edition = "2021" 8 | publish = false 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | aes = "0.8.4" 14 | anyhow = "1.0.91" 15 | base64 = "0.22.1" 16 | cbc = { version = "0.1.2", features = ["alloc"] } 17 | clap = { version = "4.5.20", features = [ 18 | "std", 19 | "cargo", 20 | "wrap_help", 21 | "derive", 22 | ] } 23 | pbkdf2 = "0.12.2" 24 | rand = "0.8.5" 25 | reqwest = { version = "0.12.8", features = ["json"] } 26 | serde = { version = "1.0.213", features = ["derive"] } 27 | sha2 = "0.10.8" 28 | tokio = { version = "1.41.0", default-features = false, features = [ 29 | "net", 30 | "rt-multi-thread", 31 | "time", 32 | "sync", 33 | "macros", 34 | ] } 35 | 36 | [dev-dependencies] 37 | hex-literal = "0.4.1" 38 | -------------------------------------------------------------------------------- /sample-03-rs/README.md: -------------------------------------------------------------------------------- 1 | # sample-03-rs 2 | 3 | Rust implementation of [`sample-03`](../sample-03/), which is fully compatible with the original version. 4 | 5 | ## Build 6 | 7 | ```shell: 8 | $ cargo build --release 9 | ``` 10 | 11 | Then you have an executable binary `./target/release/cli`. 12 | 13 | ## Usage 14 | 15 | ```shell: 16 | $ ./target/release/cli03 -h 17 | Rust version of sample-03 18 | 19 | Usage: cli03 20 | 21 | Commands: 22 | get Get ciphertext or plaintext object from the json server 23 | post Post ciphertext or plaintext object to the json server 24 | help Print this message or the help of the given subcommand(s) 25 | 26 | Options: 27 | -h, --help Print help 28 | -V, --version Print version 29 | ``` 30 | 31 | ```shell: 32 | $ ./target/release/post -h 33 | Post ciphertext or plaintext object to the json server 34 | 35 | Usage: cli03 post [OPTIONS] 36 | 37 | Arguments: 38 | Plaintext data string 39 | 40 | Options: 41 | -k, --key Key string 42 | -e, --encrypt Encrypt given data by AES-CBC 43 | -r, --remote Post to the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 44 | -h, --help Print help 45 | ``` 46 | 47 | ```shell: 48 | $ ./target/release/get -h 49 | Get ciphertext or plaintext object from the json server 50 | 51 | Usage: cli03 get [OPTIONS] 52 | 53 | Arguments: 54 | Id number of the target data on the server 55 | 56 | Options: 57 | -k, --key Key string 58 | -d, --decrypt Decrypt given data by AES-CBC 59 | -r, --remote Get from the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 60 | -h, --help Print help 61 | ``` 62 | -------------------------------------------------------------------------------- /sample-03-rs/src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgAction, Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct ClapArgs { 6 | #[clap(subcommand)] 7 | pub subcommand: SubCommands, 8 | } 9 | 10 | #[derive(Debug, Subcommand)] 11 | pub enum SubCommands { 12 | /// Get ciphertext or plaintext object from the json server 13 | Get { 14 | /// Id number of the target data on the server 15 | id: usize, 16 | 17 | /// Key string 18 | #[arg(short, long)] 19 | key: Option, 20 | 21 | /// Decrypt given data by AES-CBC 22 | #[arg(short, long, action = ArgAction::SetTrue)] 23 | decrypt: bool, 24 | 25 | /// Get from the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 26 | #[arg(short, long, action = ArgAction::SetTrue)] 27 | remote: bool, 28 | }, 29 | /// Post ciphertext or plaintext object to the json server 30 | Post { 31 | /// Plaintext data string 32 | data: String, 33 | 34 | /// Key string 35 | #[arg(short, long)] 36 | key: Option, 37 | 38 | /// Encrypt given data by AES-CBC 39 | #[arg(short, long, action = ArgAction::SetTrue)] 40 | encrypt: bool, 41 | 42 | /// Post to the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 43 | #[arg(short, long, action = ArgAction::SetTrue)] 44 | remote: bool, 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /sample-03-rs/src/crypto.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::*, key::BinaryKey}; 2 | use aes::cipher::{ 3 | block_padding::Pkcs7, 4 | generic_array::{ 5 | typenum::{U16, U32}, 6 | GenericArray, 7 | }, 8 | BlockDecryptMut, BlockEncryptMut, KeyIvInit, 9 | }; 10 | use base64::{engine::general_purpose, Engine as _}; 11 | use rand::RngCore; 12 | type Aes256CbcEnc = cbc::Encryptor; 13 | type Aes256CbcDec = cbc::Decryptor; 14 | 15 | pub struct Encrypted { 16 | pub data: Vec, 17 | pub iv: Vec, 18 | } 19 | 20 | impl Encrypted { 21 | #[allow(dead_code)] 22 | pub fn data_to_base64(&self) -> String { 23 | general_purpose::STANDARD.encode(&self.data) 24 | } 25 | #[allow(dead_code)] 26 | pub fn iv_to_base64(&self) -> String { 27 | general_purpose::STANDARD.encode(&self.iv) 28 | } 29 | } 30 | 31 | pub fn encrypt(data: &[u8], key: &BinaryKey, iv: Option<&[u8]>) -> Result { 32 | let key_array: &GenericArray = GenericArray::from_slice(&key.key); 33 | let iv = match iv { 34 | None => { 35 | let mut iv = [0u8; 16]; 36 | rand::thread_rng().fill_bytes(&mut iv); 37 | iv.to_vec() 38 | } 39 | Some(v) => v.to_vec(), 40 | }; 41 | let iv: &GenericArray = GenericArray::from_slice(&iv); 42 | 43 | let encrypted = Aes256CbcEnc::new(key_array, iv).encrypt_padded_vec_mut::(data); 44 | 45 | Ok(Encrypted { 46 | data: encrypted, 47 | iv: iv.to_vec(), 48 | }) 49 | } 50 | 51 | pub fn decrypt(encrypted: &Encrypted, key: &BinaryKey) -> Result> { 52 | let key_array: &GenericArray = GenericArray::from_slice(&key.key); 53 | let iv: &GenericArray = GenericArray::from_slice(&encrypted.iv); 54 | Aes256CbcDec::new(key_array, iv) 55 | .decrypt_padded_vec_mut::(&encrypted.data) 56 | .map_err(|e| anyhow!(e)) 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | #[test] 64 | fn aes_cbc_works() -> Result<()> { 65 | let data = b"secret".as_slice(); 66 | let key = BinaryKey::try_new("password", 32, None)?; 67 | let encrypted = encrypt(data, &key, None)?; 68 | let decrypted = decrypt(&encrypted, &key)?; 69 | 70 | assert_eq!(&decrypted, data); 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | fn aes_cbc_test_vector() -> Result<()> { 76 | let data = b"hello my super secret world!!!"; 77 | let key = BinaryKey::try_new( 78 | "my secret key", 79 | 32, 80 | Some("jbfL016yS9RUb8Sf+6m+Pm2L1Io7u1SpqHsr+R6RTu4="), 81 | )?; 82 | let iv = general_purpose::STANDARD.decode("zuwTPW7nrWon6nEhyrzzxA==")?; 83 | let encrypted_data = general_purpose::STANDARD.decode("EoeSsv5BFr6s1jZh3iMM1Pxa+wA4UxQnM30J2027kJU=")?; 84 | 85 | let encrypted = encrypt(data, &key, Some(&iv))?; 86 | 87 | assert_eq!(encrypted.data, encrypted_data); 88 | assert_eq!(iv, encrypted.iv); 89 | 90 | let dec = decrypt( 91 | &Encrypted { 92 | data: encrypted_data, 93 | iv, 94 | }, 95 | &key, 96 | )?; 97 | 98 | assert_eq!(data.as_slice(), &dec); 99 | Ok(()) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /sample-03-rs/src/error.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub use anyhow::{anyhow, bail, ensure, Context, Result}; 3 | -------------------------------------------------------------------------------- /sample-03-rs/src/key.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use base64::{engine::general_purpose, Engine as _}; 3 | use pbkdf2::pbkdf2_hmac; 4 | use rand::RngCore; 5 | use sha2::Sha256; 6 | 7 | const SALT_LEN: usize = 32; 8 | const ITERATION: u32 = 2048; 9 | 10 | pub struct BinaryKey { 11 | pub key: Vec, 12 | pub salt: String, 13 | } 14 | 15 | impl BinaryKey { 16 | pub fn try_new(password: &str, len: usize, salt: Option<&str>) -> Result { 17 | let (salt_bin, salt_base64) = match salt { 18 | Some(v) => (general_purpose::STANDARD.decode(v)?, v.to_string()), 19 | None => { 20 | let mut buf = [0u8; SALT_LEN]; 21 | rand::thread_rng().fill_bytes(&mut buf); 22 | (buf.to_vec(), general_purpose::STANDARD.encode(buf)) 23 | } 24 | }; 25 | 26 | let mut key_bin = vec![Default::default(); len]; 27 | 28 | pbkdf2_hmac::(password.as_bytes(), &salt_bin, ITERATION, &mut key_bin); 29 | 30 | Ok(Self { 31 | key: key_bin, 32 | salt: salt_base64, 33 | }) 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | mod tests { 39 | use super::*; 40 | use hex_literal::hex; 41 | 42 | #[test] 43 | fn gen_binary_key_with_salt() -> Result<()> { 44 | let salt = hex!("dc04deff5a33c22df3aa82085f9c2d0f5477af73cd500dfe53162d70ba096a03").as_slice(); 45 | let salt_base64 = general_purpose::STANDARD.encode(salt); 46 | 47 | let binary_key = BinaryKey::try_new("password", 32, Some(&salt_base64))?; 48 | assert_eq!( 49 | binary_key.key.as_slice(), 50 | hex!("bf3d09d429fbf71bbb384a6421447da32096ff8a010c7042d3e29194237792d2") 51 | ); 52 | assert_eq!(&binary_key.salt, "3ATe/1ozwi3zqoIIX5wtD1R3r3PNUA3+UxYtcLoJagM="); 53 | Ok(()) 54 | } 55 | 56 | #[test] 57 | fn gen_binary_key_without_salt() -> Result<()> { 58 | let binary_key = BinaryKey::try_new("password", 32, None)?; 59 | 60 | let salt = binary_key.salt; 61 | 62 | let binary_key_new = BinaryKey::try_new("password", 32, Some(&salt))?; 63 | 64 | assert_eq!(binary_key.key, binary_key_new.key); 65 | assert_eq!(salt, binary_key_new.salt); 66 | Ok(()) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /sample-03-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod crypto; 3 | mod error; 4 | mod key; 5 | 6 | use crate::{ 7 | crypto::{decrypt, encrypt, Encrypted}, 8 | error::*, 9 | key::BinaryKey, 10 | }; 11 | use base64::{engine::general_purpose, Engine as _}; 12 | use clap::Parser; 13 | use config::{ClapArgs, SubCommands}; 14 | use serde::Deserialize; 15 | use std::collections::HashMap; 16 | 17 | const LOCAL_SRV: &str = "http://localhost:3000/data"; 18 | const REMOTE_SRV: &str = "https://e2e.secarchlab.net/data"; 19 | 20 | #[tokio::main] 21 | pub async fn main() -> Result<()> { 22 | let _ = include_str!("../Cargo.toml"); 23 | let args = ClapArgs::parse(); 24 | 25 | match &args.subcommand { 26 | SubCommands::Get { 27 | id, 28 | key, 29 | decrypt, 30 | remote, 31 | } => { 32 | if (key.is_none() && *decrypt) || (key.is_some() && !*decrypt) { 33 | bail!("when -d is specified, -k must be simultaneously specified") 34 | } 35 | get_data(id, key.as_ref().map(|x| x.as_str()), remote).await?; 36 | } 37 | SubCommands::Post { 38 | data, 39 | key, 40 | encrypt, 41 | remote, 42 | } => { 43 | if (key.is_none() && *encrypt) || (key.is_some() && !*encrypt) { 44 | bail!("when -e is specified, -k must be simultaneously specified") 45 | } 46 | post_data(data, key.as_ref().map(|x| x.as_str()), remote).await?; 47 | } 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | async fn post_data(data: &str, key: Option<&str>, remote: &bool) -> Result<()> { 54 | let mut body = HashMap::new(); 55 | match key { 56 | Some(key) => { 57 | println!("Encrypt data"); 58 | let binary_key = BinaryKey::try_new(key, 32, None)?; 59 | let encrypted = encrypt(data.as_bytes(), &binary_key, None)?; 60 | let data = encrypted.data_to_base64(); 61 | let iv = encrypted.iv_to_base64(); 62 | body.insert("data", data); 63 | body.insert("iv", iv); 64 | body.insert("salt", binary_key.salt); 65 | } 66 | None => { 67 | body.insert("data", data.to_string()); 68 | } 69 | } 70 | 71 | let client = reqwest::Client::new(); 72 | let srv = if *remote { REMOTE_SRV } else { LOCAL_SRV }; 73 | let res = client.post(srv).json(&body).send().await?; 74 | let post_res = res.json::().await?; 75 | println!("Registered id: {:?}", post_res.id); 76 | 77 | Ok(()) 78 | } 79 | 80 | async fn get_data(id: &usize, key: Option<&str>, remote: &bool) -> Result<()> { 81 | let client = reqwest::Client::new(); 82 | let srv = if *remote { REMOTE_SRV } else { LOCAL_SRV }; 83 | let res = client.get(format!("{srv}/{id}")).send().await?; 84 | let get_res = res.json::().await?; 85 | 86 | let retrieved_data = match (key, get_res.salt, get_res.iv) { 87 | (Some(key), Some(salt), Some(iv)) => { 88 | println!("Decrypt data"); 89 | let binary_data = general_purpose::STANDARD.decode(get_res.data)?; 90 | let binary_key = BinaryKey::try_new(key, 32, Some(&salt))?; 91 | let binary_iv = general_purpose::STANDARD.decode(iv)?; 92 | let dec = decrypt( 93 | &Encrypted { 94 | data: binary_data, 95 | iv: binary_iv, 96 | }, 97 | &binary_key, 98 | )?; 99 | String::from_utf8(dec)? 100 | } 101 | (None, None, None) => get_res.data, 102 | _ => { 103 | bail!("Invalid data format or ungiven key for the id: {}", get_res.id) 104 | } 105 | }; 106 | println!("Retrieved data: {retrieved_data}"); 107 | Ok(()) 108 | } 109 | 110 | #[derive(Deserialize, Debug)] 111 | struct PostResponse { 112 | pub id: usize, 113 | } 114 | 115 | #[derive(Deserialize, Debug)] 116 | struct GetResponse { 117 | pub id: usize, 118 | pub data: String, 119 | pub iv: Option, 120 | pub salt: Option, 121 | } 122 | -------------------------------------------------------------------------------- /sample-03/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "@babel/preset-env", { 4 | "targets": { 5 | "browsers": [ 6 | "last 2 chrome versions", 7 | "last 2 firefox versions" 8 | ] 9 | }, 10 | "useBuiltIns": false 11 | } ] 12 | ], 13 | "ignore": [ "node_modules" ], 14 | "only": [ "src", "test" ], 15 | "plugins": [ 16 | [ 17 | "@babel/plugin-transform-runtime", 18 | { 19 | "@babel/polyfill": false, 20 | "regenerator": true 21 | } 22 | ] 23 | ], 24 | "env": { 25 | "production": { 26 | }, 27 | "development": { 28 | }, 29 | "test": { 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sample-03/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 2 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "always" 28 | ], 29 | "arrow-body-style": "error", 30 | "arrow-parens": "error", 31 | "arrow-spacing": "error", 32 | "generator-star-spacing": "error", 33 | "no-duplicate-imports": "error", 34 | "no-useless-computed-key": "error", 35 | "no-useless-constructor": "error", 36 | "no-useless-rename": "error", 37 | "no-var": "error", 38 | "object-shorthand": "error", 39 | "prefer-arrow-callback": "error", 40 | "prefer-const": "error", 41 | "prefer-rest-params": "error", 42 | "prefer-spread": "error", 43 | "prefer-template": "error", 44 | "rest-spread-spacing": "error", 45 | "template-curly-spacing": "error", 46 | "yield-star-spacing": "error" 47 | }, 48 | "globals": { 49 | "window": false, 50 | "_window": false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sample-03/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | obsolete/ 4 | thumbs.db 5 | yarn-error.log 6 | .idea/ 7 | .vscode/ 8 | .coverage/ 9 | .nyc_output/ 10 | dist/ 11 | coverage/ 12 | data/db.json 13 | -------------------------------------------------------------------------------- /sample-03/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jun Kurihara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sample-03/data/db.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | ], 4 | "author": { 5 | "name": "jun kurihara" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample-03/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e-security-class-sample", 3 | "version": "0.1.0", 4 | "description": "sample for the class", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "preinstall": "npx only-allow pnpm", 9 | "start": "cp data/db.template.json data/db.json && ./node_modules/.bin/json-server --watch data/db.json", 10 | "execute": "node -r @babel/register src/post-get-node.js", 11 | "build": "./node_modules/.bin/webpack --mode development --config webpack.config.js", 12 | "cleanup": "rm -rf ./dist coverage .nyc_output; rm -rf ./test/html/*.bundle.js; rm -rf ./test/html/test.html; rm -rf ./node_modules" 13 | }, 14 | "author": "Jun Kurihara", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@babel/cli": "7.26.4", 18 | "@babel/core": "7.26.0", 19 | "@babel/plugin-transform-regenerator": "7.25.9", 20 | "@babel/plugin-transform-runtime": "7.25.9", 21 | "@babel/preset-env": "7.26.0", 22 | "@babel/register": "7.25.9", 23 | "@babel/eslint-parser": "7.25.9", 24 | "babel-loader": "9.2.1", 25 | "cross-env": "7.0.3", 26 | "eslint": "9.17.0", 27 | "webpack": "5.97.1", 28 | "webpack-cli": "6.0.1" 29 | }, 30 | "dependencies": { 31 | "@babel/runtime": "~7.26.0", 32 | "commander": "~12.1.0", 33 | "cross-fetch": "~4.1.0", 34 | "js-crypto-utils": "1.0.7", 35 | "js-encoding-utils": "0.7.3", 36 | "json-server": "~0.17.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample-03/src/common/comm.js: -------------------------------------------------------------------------------- 1 | import {getFetch} from './env.js'; 2 | 3 | /** 4 | * make RESTful API call to the security hub api. 5 | * @param method {String} - 'GET' or 'POST' 6 | * @param requestUrl {String} - url connected with the endpoint defined above. 7 | * @param payload {Object} - request body. 8 | * @param headers - header params. 9 | * @param mode 10 | * @return {Promise<*>} 11 | */ 12 | export const makeApiCall = async ({method, requestUrl, payload=null, headers=null, mode='cors'}) => { 13 | //logger.debug('make API call to AWS API Gateway'); 14 | const fetch = getFetch(); 15 | const body = (payload)? JSON.stringify(payload) : null; 16 | const response = await fetch(requestUrl, { 17 | method, 18 | body, 19 | headers, 20 | mode 21 | }); 22 | 23 | let success = false; 24 | if (response.status >= 200 && response.status < 300) { 25 | success = true; 26 | } 27 | 28 | const responseJson = await response.json(); 29 | if(success) { 30 | return responseJson; 31 | } 32 | else { 33 | const err = Object.assign({status: response.status, statusText: response.statusText}, responseJson); 34 | throw new Error(JSON.stringify(err)); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /sample-03/src/common/env.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | import jscu from 'js-crypto-utils'; 3 | 4 | /** 5 | * Get jscu 6 | */ 7 | export const getJscu = () => { 8 | const global = Function('return this;')(); 9 | if (typeof window !== 'undefined'){ 10 | return window.jscu; 11 | } 12 | else{ 13 | global.jscu = jscu; 14 | return jscu; 15 | } 16 | }; 17 | 18 | /** 19 | * Get fetch 20 | */ 21 | export const getFetch = () => { 22 | // node-fetch in aws sdk 23 | const global = Function('return this;')(); 24 | if (typeof window === 'undefined'){ 25 | global.fetch = fetch; 26 | return fetch; 27 | } 28 | else { 29 | return window.fetch; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /sample-03/src/common/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | */ 4 | import {getMyData, postMyData, getAllEntries} from './post-get'; 5 | export {getAllEntries, getMyData, postMyData}; 6 | export default {getAllEntries, getMyData, postMyData}; 7 | -------------------------------------------------------------------------------- /sample-03/src/common/key.js: -------------------------------------------------------------------------------- 1 | import {getJscu} from './env'; 2 | import jseu from 'js-encoding-utils'; 3 | 4 | export const strToBinaryKey = async (str, len, salt=null) => { 5 | const jscu = getJscu(); 6 | 7 | // derive key from password 8 | // following params (salt, iterationCount, aesKeyLen, hash) must be shared with receiver. 9 | if(!salt){ 10 | salt = jscu.random.getRandomBytes(32); // Uint8Array -> must be shared with receiver 11 | } 12 | else { 13 | salt = jseu.encoder.decodeBase64(salt); 14 | } 15 | 16 | const iterationCount = 2048; // must be shared with receiver 17 | const hash = 'SHA-256'; // SHA-384, SHA-512, etc. 18 | 19 | const key = await jscu.pbkdf.pbkdf2( 20 | str, 21 | salt, 22 | iterationCount, 23 | len, 24 | hash 25 | ).catch( (e) => { 26 | throw new Error(`failed to derive binary key from string key: ${e.message}`); 27 | }); 28 | return {key, salt: jseu.encoder.encodeBase64(salt)}; 29 | }; 30 | -------------------------------------------------------------------------------- /sample-03/src/common/params.js: -------------------------------------------------------------------------------- 1 | export const mockDataUrl = 'http://localhost:3000/data'; // for HTTP local mock server 2 | export const remoteDataUrl = 'https://e2e.secarchlab.net/data'; // for HTTPS remote server 3 | -------------------------------------------------------------------------------- /sample-03/src/common/post-get.js: -------------------------------------------------------------------------------- 1 | import {makeApiCall} from './comm'; 2 | import {mockDataUrl, remoteDataUrl} from './params'; 3 | import {strToBinaryKey} from './key'; 4 | import jseu from 'js-encoding-utils'; 5 | 6 | import * as webapi from '../encrypt-browser'; 7 | import * as nodeapi from '../encrypt-node'; 8 | import * as universalapi from '../encrypt-universal'; 9 | 10 | /** 11 | * Post data 12 | * @param data {string} - plaintext data to be encrypted 13 | * @param key {string} - the string password 14 | * @param encrypt {boolean} - encrypt or plaintext 15 | * @param remote {boolean} - fetch remote json-server if true 16 | * @param universal {boolean} - use jscu if true 17 | * @return {Promise<*>} 18 | */ 19 | export const postMyData = async ({data, key='', encrypt=false, remote=false, universal=false}) => { 20 | const payload = {}; 21 | if(encrypt) { 22 | //////////////////////// 23 | // encrypt data here!! 24 | const keyObj = await strToBinaryKey(key, 32); 25 | console.log(`Note: Derived key binary in base64: ${jseu.encoder.encodeBase64(keyObj.key)}`); 26 | //////////////////////// 27 | // universal api or dedicated apis 28 | let encryptedObj; 29 | if (universal) encryptedObj = await universalapi.encrypt(data, keyObj.key); 30 | else encryptedObj = (typeof window !== 'undefined') 31 | ? await webapi.encrypt(data, keyObj.key) 32 | : await nodeapi.encrypt(data, keyObj.key); 33 | //////////////////////// 34 | payload.data = encryptedObj.data; 35 | payload.iv = encryptedObj.iv; 36 | payload.salt = keyObj.salt; 37 | //////////////////////// 38 | } 39 | else payload.data = data; 40 | 41 | 42 | const response = await makeApiCall({ 43 | method: 'POST', 44 | requestUrl: (remote) ? remoteDataUrl : mockDataUrl, 45 | payload, 46 | headers: {'Content-Type': 'application/json'}, 47 | mode: 'cors' 48 | }); 49 | 50 | return {id: response.id}; 51 | }; 52 | 53 | 54 | /** 55 | * Get data 56 | * @param dataId - id registered in the json server 57 | * @param key {string} - the string password 58 | * @param remote {boolean} - fetch remote json-server if true 59 | * @param universal {boolean} - use jscu if true 60 | * @return {Promise<*>} 61 | */ 62 | export const getMyData = async ({id, key='', decrypt=false, remote=false, universal=false}) => { 63 | const data = await makeApiCall({ 64 | method: 'GET', 65 | requestUrl: `${(remote) ? remoteDataUrl : mockDataUrl}/${id}`, 66 | headers: {'Content-Type': 'application/json'}, 67 | mode: 'cors' 68 | }); 69 | 70 | if(decrypt) { 71 | //////////////////////// 72 | // decryption data here!! 73 | if(!data.data || !data.iv || !data.salt) throw new Error(`Maybe Unencrypted data => contents: ${data}`); 74 | const keyObj = await strToBinaryKey(key, 32, data.salt); 75 | console.log(`Note: Derived key binary in base64: ${jseu.encoder.encodeBase64(keyObj.key)}`); 76 | //////////////////////// 77 | // universal api or dedicated apis 78 | let decrypted; 79 | if(universal) decrypted = await universalapi.decrypt(data.data, keyObj.key, data.iv); 80 | else decrypted = (typeof window !== 'undefined') 81 | ? await webapi.decrypt(data.data, keyObj.key, data.iv) 82 | : await nodeapi.decrypt(data.data, keyObj.key, data.iv); 83 | //////////////////////// 84 | return {data: decrypted}; 85 | //////////////////////// 86 | } 87 | else return data; 88 | }; 89 | 90 | /** 91 | * Get all entries without decryption 92 | * @param remote {boolean} - fetch remote json-server if true 93 | * @return {Promise<*>} 94 | */ 95 | export const getAllEntries = async (remote=false) => makeApiCall({ 96 | method: 'GET', 97 | requestUrl: (remote) ? `${remoteDataUrl}` : `${mockDataUrl}`, 98 | headers: {'Content-Type': 'application/json'}, 99 | mode: 'cors' 100 | }); 101 | -------------------------------------------------------------------------------- /sample-03/src/encrypt-browser.js: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | 3 | /** 4 | * Encrypt data here 5 | * @param data {string} - plaintext data to be encrypted 6 | * @param key {Uint8Array} - 256bit key 7 | * @return {Promise<{data: *, iv: *}>} 8 | */ 9 | export const encrypt = async (data, key) => { 10 | const crypto = window.crypto; 11 | 12 | const iv = crypto.getRandomValues(new Uint8Array(16)); 13 | const importedKey = await crypto.subtle.importKey( 14 | 'raw', 15 | key, 16 | { name: 'AES-CBC' }, 17 | false, //whether the key is extractable (i.e. can be used in exportKey) 18 | ['encrypt', 'decrypt'] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey" 19 | ); 20 | 21 | const encrypted = await crypto.subtle.encrypt( 22 | { name: 'AES-CBC', iv }, 23 | importedKey, //from generateKey or importKey above 24 | jseu.encoder.stringToArrayBuffer(data) //ArrayBuffer of data you want to encrypt 25 | ); 26 | 27 | return { 28 | data: jseu.encoder.encodeBase64(new Uint8Array(encrypted)), 29 | iv: jseu.encoder.encodeBase64(iv) 30 | }; 31 | }; 32 | 33 | /** 34 | * Decrypt data 35 | * @param data {string} - encrypted data in base64 36 | * @param key {Uint8Array} - 256bit key 37 | * @param iv {string} - iv in base64 38 | * @return {Promise<*|void|Promise|IDBRequest|[]>} 39 | */ 40 | export const decrypt = async (data, key, iv) => { 41 | const crypto = window.crypto; 42 | 43 | const importedKey = await crypto.subtle.importKey( 44 | 'raw', 45 | key, 46 | { name: 'AES-CBC' }, 47 | false, //whether the key is extractable (i.e. can be used in exportKey) 48 | ['encrypt', 'decrypt'] //can be "encrypt", "decrypt", "wrapKey", or "unwrapKey" 49 | ); 50 | 51 | const decrypted = await crypto.subtle.decrypt( 52 | { name: 'AES-CBC', iv: jseu.encoder.decodeBase64(iv) }, 53 | importedKey, //from generateKey or importKey above 54 | jseu.encoder.decodeBase64(data) 55 | ); 56 | 57 | return jseu.encoder.arrayBufferToString(decrypted); 58 | }; 59 | -------------------------------------------------------------------------------- /sample-03/src/encrypt-node.js: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | 3 | /** 4 | * Encrypt data here 5 | * @param data {string} - plaintext data to be encrypted 6 | * @param key {Uint8Array} - 256bit key 7 | * @return {Promise<{data: *, iv: *}>} 8 | */ 9 | export const encrypt = async (data, key) => { 10 | const uint8data = jseu.encoder.stringToArrayBuffer(data); 11 | const crypto = require('crypto'); 12 | const algorithm = 'aes-256-cbc'; 13 | const iv = crypto.randomBytes(16); // Initialization vector. 14 | const cipher = crypto.createCipheriv(algorithm, key, iv); 15 | 16 | let encrypted = cipher.update(uint8data, 'utf8', 'base64'); 17 | encrypted += cipher.final('base64'); 18 | return { 19 | data: encrypted, 20 | iv: jseu.encoder.encodeBase64(new Uint8Array(iv)) 21 | }; 22 | }; 23 | 24 | 25 | /** 26 | * Decrypt data 27 | * @param data {string} - encrypted data in base64 28 | * @param key {Uint8Array} - 256bit key 29 | * @param iv {string} - iv in base64 30 | * @return {Promise<*|void|Promise|IDBRequest|[]>} 31 | */ 32 | export const decrypt = async (data, key, iv) => { 33 | const crypto = require('crypto'); 34 | 35 | const algorithm = 'aes-256-cbc'; 36 | if (!iv) iv = Buffer.alloc(16, 0); // Initialization vector. 37 | const uint8iv = jseu.encoder.decodeBase64(iv); 38 | 39 | const decipher = crypto.createDecipheriv(algorithm, key, uint8iv); 40 | 41 | let decrypted = decipher.update(data, 'base64', 'utf8'); 42 | decrypted += decipher.final(); 43 | return decrypted; 44 | }; 45 | -------------------------------------------------------------------------------- /sample-03/src/encrypt-universal.js: -------------------------------------------------------------------------------- 1 | // Works both in Node.js and Browsers by using "jscu" 2 | 3 | import jseu from 'js-encoding-utils'; 4 | import {getJscu} from './common/env'; 5 | 6 | /** 7 | * Encrypt data here 8 | * @param data {string} - plaintext data to be encrypted 9 | * @param key {Uint8Array} - 256bit key 10 | * @return {Promise<{data: *, iv: *}>} 11 | */ 12 | export const encrypt = async (data, key) => { 13 | const jscu = getJscu(); 14 | 15 | const iv = jscu.random.getRandomBytes(16); 16 | const encrypted = await jscu.aes.encrypt( 17 | jseu.encoder.stringToArrayBuffer(data), 18 | key, 19 | {name: 'AES-CBC', iv} 20 | ); 21 | 22 | return { 23 | data: jseu.encoder.encodeBase64(encrypted), 24 | iv: jseu.encoder.encodeBase64(iv) 25 | }; 26 | }; 27 | 28 | /** 29 | * Decrypt data 30 | * @param data {string} - encrypted data in base64 31 | * @param key {Uint8Array} - 256bit key 32 | * @param iv {string} - iv in base64 33 | * @return {Promise<*|void|Promise|IDBRequest|[]>} 34 | */ 35 | export const decrypt = async (data, key, iv) => { 36 | const jscu = getJscu(); 37 | 38 | const decrypted = await jscu.aes.decrypt( 39 | jseu.encoder.decodeBase64(data), 40 | key, 41 | {name: 'AES-CBC', iv: jseu.encoder.decodeBase64(iv)} 42 | ); 43 | 44 | return jseu.encoder.arrayBufferToString(decrypted); 45 | }; 46 | -------------------------------------------------------------------------------- /sample-03/src/post-get-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | E2E Encryption Test 6 | 7 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /sample-03/src/post-get-node.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { postMyData, getMyData } from "./common/post-get"; 4 | import { Command } from "commander"; 5 | 6 | const pgm = new Command(); 7 | pgm.version("0.0.1"); 8 | 9 | /// for post 10 | pgm 11 | .command("post ", "") 12 | .description("Post data to json-server") 13 | .option("-e, --encrypt", "Encrypt with string key") 14 | .option("-k, --key ", "String key for encryption") 15 | .option("-r, --remote", "Register to remote server (zettant.com)") 16 | .option("-u, --universal", "Use universal crypto library (jscu)") 17 | .action(async (data, options) => { 18 | if (options.encrypt && !options.key) { 19 | console.error("String key required for encryption"); 20 | process.exit(1); 21 | } 22 | if (options.encrypt) { 23 | console.log(`Register encrypted data to ${options.remote ? "remote" : "local"} server`); 24 | console.log(`Data: ${data}`); 25 | console.log(`Key: ${options.key}`); 26 | const res = await postMyData({ 27 | data, 28 | key: options.key, 29 | encrypt: true, 30 | remote: options.remote, 31 | universal: options.universal, 32 | }); 33 | console.log(`Registered id: ${res.id}`); 34 | } else { 35 | console.log(`Register plaintext data to ${options.remote ? "remote" : "local"} server`); 36 | console.log(`Data: ${data}`); 37 | const res = await postMyData({ 38 | data, 39 | encrypt: false, 40 | remote: options.remote, 41 | }); 42 | console.log(`Registered id: ${res.id}`); 43 | } 44 | }); 45 | 46 | /// for get 47 | pgm 48 | .command("get ", "") 49 | .description("Get data from json-server") 50 | .option("-d, --decrypt", "Decrypt with string key") 51 | .option("-k, --key ", "String key for decryption") 52 | .option("-r, --remote", "Retrieve from remote server (zettant.com)") 53 | .option("-u, --universal", "Use universal crypto library (jscu)") 54 | .action(async (id, options) => { 55 | if (options.decrypt && !options.key) { 56 | console.error("String key required for decryption"); 57 | process.exit(1); 58 | } 59 | if (options.decrypt) { 60 | console.log(`Retrieve encrypted data to ${options.remote ? "remote" : "local"} server`); 61 | console.log(`Id: ${id}`); 62 | console.log(`Key: ${options.key}`); 63 | const res = await getMyData({ 64 | id, 65 | key: options.key, 66 | decrypt: options.decrypt, 67 | remote: options.remote, 68 | universal: options.universal, 69 | }); 70 | console.log(`Decrypted data: ${res.data}`); 71 | } else { 72 | console.log(`Retrieve plaintext data to ${options.remote ? "remote" : "local"} server`); 73 | console.log(`Registered Id: ${id}`); 74 | const res = await getMyData({ 75 | id, 76 | decrypt: false, 77 | remote: options.remote, 78 | }); 79 | console.log(`Retrieved data: ${res.data}`); 80 | } 81 | }); 82 | 83 | pgm.parse(process.argv); 84 | -------------------------------------------------------------------------------- /sample-03/src/registered-data-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | E2E Encryption Test: Registered Data List 6 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /sample-03/webpack.baseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "libName": "e2eTest" 3 | } 4 | -------------------------------------------------------------------------------- /sample-03/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const webpack = require('webpack'); 3 | const base = require('./webpack.baseconfig.json'); 4 | 5 | const config = { 6 | entry: ['./src/common/index.js'], 7 | 8 | output: { 9 | filename: `${base.libName}.bundle.js`, 10 | chunkFilename: '[name].js', 11 | path: path.resolve(__dirname, './dist'), 12 | publicPath: path.resolve(__dirname, './dist'), 13 | library: base.libName, 14 | libraryTarget: 'umd', 15 | globalObject: 'this' // for node js import 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx', '.mjs'], 19 | modules: ['node_modules'] 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(m|)js$/, 25 | use: [{ 26 | loader: 'babel-loader' 27 | }], 28 | exclude: path.join(__dirname, 'node_modules') // exclude: /node_modules/ 29 | }, 30 | ], 31 | }, 32 | externals: { 33 | crypto: true, 34 | 'cross-fetch': true 35 | } 36 | }; 37 | 38 | module.exports = (env, argv) => { 39 | config.mode = (typeof argv.mode !== 'undefined' && argv.mode === 'production') ? argv.mode : 'development'; 40 | 41 | if (config.mode === 'production') console.log('Webpack for production'); 42 | else{ 43 | console.log('Webpack for development'); 44 | config.devtool = 'inline-source-map'; // add inline source map 45 | } 46 | 47 | return config; 48 | }; 49 | -------------------------------------------------------------------------------- /sample-04-rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /sample-04-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli04" 3 | authors = ["Jun Kurihara"] 4 | description = "Rust version of sample-04" 5 | repository = "https://github.com/junkurihara/lecture-security_engineering" 6 | version = "0.1.0" 7 | edition = "2021" 8 | publish = false 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | aes = "0.8.4" 14 | anyhow = "1.0.91" 15 | base64 = "0.22.1" 16 | cbc = { version = "0.1.2", features = ["alloc"] } 17 | clap = { version = "4.5.20", features = ["std", "cargo", "wrap_help", "derive"] } 18 | hkdf = "0.12.4" 19 | pbkdf2 = "0.12.2" 20 | rand = "0.8.5" 21 | reqwest = { version = "0.12.8", features = ["json"] } 22 | serde = { version = "1.0.213", features = ["derive"] } 23 | sha2 = "0.10.8" 24 | tokio = { version = "1.41.0", default-features = false, features = [ 25 | "net", 26 | "rt-multi-thread", 27 | "time", 28 | "sync", 29 | "macros", 30 | ] } 31 | 32 | [dev-dependencies] 33 | hex-literal = "0.4.1" 34 | -------------------------------------------------------------------------------- /sample-04-rs/README.md: -------------------------------------------------------------------------------- 1 | # sample-04-rs 2 | 3 | Rust implementation of [`sample-04`](../sample-04/), which is fully compatible with the original version. 4 | 5 | ## Build 6 | 7 | ```shell: 8 | $ cargo build --release 9 | ``` 10 | 11 | Then you have an executable binary `./target/release/cli04`. 12 | 13 | ## Usage 14 | 15 | ```shell: 16 | $ ./target/release/cli04 -h 17 | Rust version of sample-04 18 | 19 | Usage: cli04 20 | 21 | Commands: 22 | get Get ciphertext or plaintext object from the json server 23 | post Post ciphertext or plaintext object to the json server 24 | gen-secret Generate master secret 25 | help Print this message or the help of the given subcommand(s) 26 | 27 | Options: 28 | -h, --help Print help 29 | -V, --version Print version 30 | ``` 31 | 32 | ```shell: 33 | $ ./target/release/cli04 post -h 34 | Post ciphertext or plaintext object to the json server 35 | 36 | Usage: cli04 post [OPTIONS] 37 | 38 | Arguments: 39 | Plaintext data string 40 | 41 | Options: 42 | -p, --password Password 43 | -m, --master Master secret in base64 44 | -r, --remote Post to the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 45 | -h, --help Print help 46 | ``` 47 | 48 | ```shell: 49 | $ ./target/release/cli04 get -h 50 | Get ciphertext or plaintext object from the json server 51 | 52 | Usage: cli04 get [OPTIONS] 53 | 54 | Arguments: 55 | Id number of the target data on the server 56 | 57 | Options: 58 | -p, --password Password 59 | -m, --master Master secret in base64 60 | -r, --remote Get from the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 61 | -h, --help Print help 62 | ``` 63 | 64 | ```shell: 65 | $ ./target/release/cli04 gen-secret -h 66 | Generate master secret 67 | 68 | Usage: cli04 gen-secret 69 | 70 | Arguments: 71 | Length of secret 72 | 73 | Options: 74 | -h, --help Print help 75 | ``` 76 | -------------------------------------------------------------------------------- /sample-04-rs/src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::{ArgAction, Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct ClapArgs { 6 | #[clap(subcommand)] 7 | pub subcommand: SubCommands, 8 | } 9 | 10 | #[allow(non_snake_case, non_camel_case_types)] 11 | #[derive(Debug, Subcommand)] 12 | pub enum SubCommands { 13 | /// Get ciphertext or plaintext object from the json server 14 | Get { 15 | /// Id number of the target data on the server 16 | id: usize, 17 | 18 | /// Password 19 | #[arg(short, long)] 20 | password: Option, 21 | 22 | /// Master secret in base64 23 | #[arg(short, long)] 24 | master: Option, 25 | 26 | /// Get from the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 27 | #[arg(short, long, action = ArgAction::SetTrue)] 28 | remote: bool, 29 | }, 30 | /// Post ciphertext or plaintext object to the json server 31 | Post { 32 | /// Plaintext data string 33 | data: String, 34 | 35 | /// Password 36 | #[arg(short, long)] 37 | password: Option, 38 | 39 | /// Master secret in base64 40 | #[arg(short, long)] 41 | master: Option, 42 | 43 | /// Post to the preset remote server (e2e.secarchlab.net) otherwise localhost:3000 44 | #[arg(short, long, action = ArgAction::SetTrue)] 45 | remote: bool, 46 | }, 47 | /// Generate master secret 48 | Gen_Secret { 49 | /// Length of secret 50 | len: usize, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /sample-04-rs/src/crypto.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::*, key::BinaryKey}; 2 | use aes::cipher::{ 3 | block_padding::Pkcs7, 4 | generic_array::{ 5 | typenum::{U16, U32}, 6 | GenericArray, 7 | }, 8 | BlockDecryptMut, BlockEncryptMut, KeyIvInit, 9 | }; 10 | use base64::{engine::general_purpose, Engine as _}; 11 | use rand::RngCore; 12 | type Aes256CbcEnc = cbc::Encryptor; 13 | type Aes256CbcDec = cbc::Decryptor; 14 | 15 | pub struct Encrypted { 16 | pub data: Vec, 17 | pub iv: Vec, 18 | } 19 | 20 | impl Encrypted { 21 | #[allow(dead_code)] 22 | pub fn data_to_base64(&self) -> String { 23 | general_purpose::STANDARD.encode(&self.data) 24 | } 25 | #[allow(dead_code)] 26 | pub fn iv_to_base64(&self) -> String { 27 | general_purpose::STANDARD.encode(&self.iv) 28 | } 29 | } 30 | 31 | pub fn encrypt(data: &[u8], key: &BinaryKey, iv: Option<&[u8]>) -> Result { 32 | let key_array: &GenericArray = GenericArray::from_slice(&key.key); 33 | let iv = match iv { 34 | None => { 35 | let mut iv = [0u8; 16]; 36 | rand::thread_rng().fill_bytes(&mut iv); 37 | iv.to_vec() 38 | } 39 | Some(v) => v.to_vec(), 40 | }; 41 | let iv: &GenericArray = GenericArray::from_slice(&iv); 42 | 43 | let encrypted = Aes256CbcEnc::new(key_array, iv).encrypt_padded_vec_mut::(data); 44 | 45 | Ok(Encrypted { 46 | data: encrypted, 47 | iv: iv.to_vec(), 48 | }) 49 | } 50 | 51 | pub fn decrypt(encrypted: &Encrypted, key: &BinaryKey) -> Result> { 52 | let key_array: &GenericArray = GenericArray::from_slice(&key.key); 53 | let iv: &GenericArray = GenericArray::from_slice(&encrypted.iv); 54 | Aes256CbcDec::new(key_array, iv) 55 | .decrypt_padded_vec_mut::(&encrypted.data) 56 | .map_err(|e| anyhow!(e)) 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use super::*; 62 | 63 | #[test] 64 | fn aes_cbc_works() -> Result<()> { 65 | let data = b"secret".as_slice(); 66 | let key = BinaryKey::try_new_pbkdf2("password", 32, None, None)?; 67 | let encrypted = encrypt(data, &key, None)?; 68 | let decrypted = decrypt(&encrypted, &key)?; 69 | 70 | assert_eq!(&decrypted, data); 71 | Ok(()) 72 | } 73 | 74 | #[test] 75 | fn aes_cbc_test_vector() -> Result<()> { 76 | let data = b"hello my super secret world!!!"; 77 | let key = BinaryKey::try_new_pbkdf2( 78 | "my secret key", 79 | 32, 80 | Some("jbfL016yS9RUb8Sf+6m+Pm2L1Io7u1SpqHsr+R6RTu4="), 81 | None, 82 | )?; 83 | let iv = general_purpose::STANDARD.decode("zuwTPW7nrWon6nEhyrzzxA==")?; 84 | let encrypted_data = general_purpose::STANDARD.decode("EoeSsv5BFr6s1jZh3iMM1Pxa+wA4UxQnM30J2027kJU=")?; 85 | 86 | let encrypted = encrypt(data, &key, Some(&iv))?; 87 | 88 | assert_eq!(encrypted.data, encrypted_data); 89 | assert_eq!(iv, encrypted.iv); 90 | 91 | let dec = decrypt( 92 | &Encrypted { 93 | data: encrypted_data, 94 | iv, 95 | }, 96 | &key, 97 | )?; 98 | 99 | assert_eq!(data.as_slice(), &dec); 100 | Ok(()) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sample-04-rs/src/error.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub use anyhow::{anyhow, bail, ensure, Context, Result}; 3 | -------------------------------------------------------------------------------- /sample-04-rs/src/key.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use base64::{engine::general_purpose, Engine as _}; 3 | use hkdf::Hkdf; 4 | use pbkdf2::pbkdf2_hmac; 5 | use rand::RngCore; 6 | use sha2::Sha256; 7 | 8 | const SALT_LEN: usize = 32; 9 | const ITERATION: u32 = 2048; 10 | 11 | pub struct BinaryKey { 12 | pub key: Vec, 13 | pub salt: String, 14 | } 15 | 16 | impl BinaryKey { 17 | pub fn try_new_pbkdf2(password: &str, len: usize, salt: Option<&str>, iter: Option<&u32>) -> Result { 18 | let (salt_bin, salt_base64) = match salt { 19 | Some(v) => (general_purpose::STANDARD.decode(v)?, v.to_string()), 20 | None => { 21 | let mut buf = [0u8; SALT_LEN]; 22 | rand::thread_rng().fill_bytes(&mut buf); 23 | (buf.to_vec(), general_purpose::STANDARD.encode(buf)) 24 | } 25 | }; 26 | let iter = match iter { 27 | Some(v) => v, 28 | None => &ITERATION, 29 | }; 30 | 31 | let mut key_bin = vec![Default::default(); len]; 32 | 33 | pbkdf2_hmac::(password.as_bytes(), &salt_bin, *iter, &mut key_bin); 34 | 35 | Ok(Self { 36 | key: key_bin, 37 | salt: salt_base64, 38 | }) 39 | } 40 | 41 | pub fn try_new_hdkf(master: &str, len: usize, salt: Option<&str>) -> Result { 42 | let (salt_bin, salt_base64) = match salt { 43 | Some(v) => (general_purpose::STANDARD.decode(v)?, v.to_string()), 44 | None => { 45 | let mut buf = [0u8; SALT_LEN]; 46 | rand::thread_rng().fill_bytes(&mut buf); 47 | (buf.to_vec(), general_purpose::STANDARD.encode(buf)) 48 | } 49 | }; 50 | let info = b""; 51 | let ikm = general_purpose::STANDARD.decode(master)?; 52 | let hkdf = Hkdf::::new(Some(&salt_bin[..]), &ikm); 53 | let mut okm = vec![Default::default(); len]; 54 | hkdf.expand(info, &mut okm).map_err(|e| anyhow!(e))?; 55 | 56 | let key_bin: &[u8] = &okm[..]; 57 | Ok(Self { 58 | key: key_bin.to_vec(), 59 | salt: salt_base64, 60 | }) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | use hex_literal::hex; 68 | 69 | #[test] 70 | fn gen_binary_key_with_salt() -> Result<()> { 71 | // pbkdf2 72 | let salt = hex!("dc04deff5a33c22df3aa82085f9c2d0f5477af73cd500dfe53162d70ba096a03").as_slice(); 73 | let salt_base64 = general_purpose::STANDARD.encode(salt); 74 | 75 | let binary_key = BinaryKey::try_new_pbkdf2("password", 32, Some(&salt_base64), None)?; 76 | assert_eq!( 77 | binary_key.key.as_slice(), 78 | hex!("bf3d09d429fbf71bbb384a6421447da32096ff8a010c7042d3e29194237792d2") 79 | ); 80 | assert_eq!(&binary_key.salt, "3ATe/1ozwi3zqoIIX5wtD1R3r3PNUA3+UxYtcLoJagM="); 81 | 82 | // hkdf 83 | let mut ikm = Vec::with_capacity(32); 84 | for i in 0..32 { 85 | ikm.push(i as u8) 86 | } 87 | let ikm_base64 = general_purpose::STANDARD.encode(&ikm); 88 | let salt_base64 = general_purpose::STANDARD.encode(&ikm); 89 | 90 | let binary_key = BinaryKey::try_new_hdkf(&ikm_base64, 144, Some(&salt_base64))?; 91 | 92 | let test_vector = "fJHB6pVraz09Ognk2NRFR/DKdsK0cnFQORjocdWbv6YaAV7m9LmrZhT2O8v1yBEZXBbEaqiRfV59VGWVd5L685jh6IHoZWoTN50i8JLMogXrnB/mvCSwLEMjY4dTxbHspz88XS+94aKvl/Hql9+IGfnOWNAcb6brgCEoD1rb7pmYT2FzIVk3qLWNTO2QtTl1"; 93 | assert_eq!( 94 | binary_key.key.as_slice(), 95 | general_purpose::STANDARD.decode(test_vector)? 96 | ); 97 | Ok(()) 98 | } 99 | 100 | #[test] 101 | fn gen_binary_key_without_salt() -> Result<()> { 102 | let binary_key = BinaryKey::try_new_pbkdf2("password", 32, None, None)?; 103 | 104 | let salt = binary_key.salt; 105 | 106 | let binary_key_new = BinaryKey::try_new_pbkdf2("password", 32, Some(&salt), None)?; 107 | 108 | assert_eq!(binary_key.key, binary_key_new.key); 109 | assert_eq!(salt, binary_key_new.salt); 110 | Ok(()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /sample-04/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "@babel/preset-env", { 4 | "targets": { 5 | "browsers": [ 6 | "last 2 chrome versions", 7 | "last 2 firefox versions", 8 | "IE 11", 9 | "last 2 Edge versions" 10 | ] 11 | }, 12 | "useBuiltIns": false 13 | } ] 14 | ], 15 | "ignore": [ "node_modules" ], 16 | "only": [ "src", "test" ], 17 | "plugins": [ 18 | [ 19 | "@babel/plugin-transform-runtime", 20 | { 21 | "@babel/polyfill": false, 22 | "regenerator": true 23 | } 24 | ] 25 | ], 26 | "env": { 27 | "production": { 28 | }, 29 | "development": { 30 | }, 31 | "test": { 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample-04/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 2 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "always" 28 | ], 29 | "arrow-body-style": "error", 30 | "arrow-parens": "error", 31 | "arrow-spacing": "error", 32 | "generator-star-spacing": "error", 33 | "no-duplicate-imports": "error", 34 | "no-useless-computed-key": "error", 35 | "no-useless-constructor": "error", 36 | "no-useless-rename": "error", 37 | "no-var": "error", 38 | "object-shorthand": "error", 39 | "prefer-arrow-callback": "error", 40 | "prefer-const": "error", 41 | "prefer-rest-params": "error", 42 | "prefer-spread": "error", 43 | "prefer-template": "error", 44 | "rest-spread-spacing": "error", 45 | "template-curly-spacing": "error", 46 | "yield-star-spacing": "error" 47 | }, 48 | "globals": { 49 | "window": false, 50 | "_window": false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sample-04/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | obsolete/ 4 | thumbs.db 5 | yarn-error.log 6 | .idea/ 7 | .vscode/ 8 | .coverage/ 9 | .nyc_output/ 10 | dist/ 11 | coverage/ 12 | data/db.json 13 | -------------------------------------------------------------------------------- /sample-04/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jun Kurihara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sample-04/data/db.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | ], 4 | "author": { 5 | "name": "jun kurihara" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample-04/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e-security-class-sample", 3 | "version": "0.1.0", 4 | "description": "sample for the class", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "preinstall": "npx only-allow pnpm", 9 | "start": "cp data/db.template.json data/db.json && ./node_modules/.bin/json-server --watch data/db.json", 10 | "execute": "node -r @babel/register src/post-get-node.js", 11 | "build": "./node_modules/.bin/webpack --mode development --config webpack.config.js", 12 | "cleanup": "rm -rf ./dist coverage .nyc_output; rm -rf ./test/html/*.bundle.js; rm -rf ./test/html/test.html; rm -rf ./node_modules" 13 | }, 14 | "author": "Jun Kurihara", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@babel/cli": "7.26.4", 18 | "@babel/core": "7.26.0", 19 | "@babel/plugin-transform-regenerator": "7.25.9", 20 | "@babel/plugin-transform-runtime": "7.25.9", 21 | "@babel/preset-env": "7.26.0", 22 | "@babel/register": "7.25.9", 23 | "@babel/eslint-parser": "7.25.9", 24 | "babel-loader": "9.2.1", 25 | "cross-env": "7.0.3", 26 | "eslint": "9.17.0", 27 | "webpack": "5.97.1", 28 | "webpack-cli": "6.0.1" 29 | }, 30 | "dependencies": { 31 | "@babel/runtime": "~7.26.0", 32 | "commander": "~12.1.0", 33 | "cross-fetch": "~4.1.0", 34 | "js-crypto-utils": "1.0.7", 35 | "js-encoding-utils": "0.7.3", 36 | "json-server": "~0.17.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample-04/src/derive-key.js: -------------------------------------------------------------------------------- 1 | import {getJscu} from './util/env'; 2 | import jseu from 'js-encoding-utils'; 3 | 4 | 5 | /** 6 | * Execute PBKDF2 7 | * @param password {string} - string password 8 | * @param len {number} - length of key in bytes 9 | * @param salt {string|null} - Uint8Array salt in Base64 10 | * @param hash {string} - Hash used in PBKDF2 algorithm, like 'SHA-256' 11 | * @param iterationCount {number} - Iteration count in PBKDF2 algorithm. 12 | * @return {Promise<{key: *, kdfParams: {salt: *, hash: *, iterationCount: *}}>} 13 | */ 14 | export const deriveKeyFromPassword = async (password, len, salt=null, hash='SHA-256', iterationCount=2048) => { 15 | const jscu = getJscu(); 16 | 17 | // derive key from password 18 | // following params (salt, iterationCount, aesKeyLen, hash) must be shared with receiver. 19 | if(!salt){ 20 | salt = jscu.random.getRandomBytes(32); // Uint8Array -> must be shared with receiver 21 | } 22 | else { 23 | salt = jseu.encoder.decodeBase64(salt); 24 | } 25 | 26 | const key = await jscu.pbkdf.pbkdf2( 27 | password, 28 | salt, 29 | iterationCount, 30 | len, 31 | hash 32 | ).catch( (e) => { 33 | throw new Error(`failed to derive binary key from string password: ${e.message}`); 34 | }); 35 | return { 36 | key, 37 | kdfParams: { // pbkdf2 parameters that must be shared with receiver 38 | salt: jseu.encoder.encodeBase64(salt), 39 | hash, 40 | iterationCount 41 | } 42 | }; 43 | }; 44 | 45 | /** 46 | * HKDF for perfect forward secrecy 47 | * @param masterSecret {string} - master secret (binary seed) in Base64 48 | * @param len {number} - output key length in byte 49 | * @param salt {string|null} - Uint8Array salt in Base64 50 | * @param hash {string} - Hash used in PBKDF2 algorithm, like 'SHA-256' 51 | * @return {Promise<{kdfParams: {salt: *, hash: *}, key: *}>} 52 | */ 53 | export const deriveKeyFromMasterSecret = async (masterSecret, len, salt=null, hash='SHA-256') => { 54 | const jscu = getJscu(); 55 | 56 | // derive key from master secret binary 57 | // following params (salt, iterationCount, aesKeyLen, hash) must be shared with receiver. 58 | if(!salt){ 59 | salt = jscu.random.getRandomBytes(32); // Uint8Array -> must be shared with receiver 60 | } 61 | else { 62 | salt = jseu.encoder.decodeBase64(salt); 63 | } 64 | 65 | const keyObj = await jscu.hkdf.compute( 66 | jseu.encoder.decodeBase64(masterSecret), 67 | hash, 68 | len, 69 | '', // 'info' field for RFC5869. This could be always blank. 70 | salt 71 | ).catch( (e) => { 72 | throw new Error(`failed to derive binary key from master secret binary: ${e.message}`); 73 | }); 74 | return { 75 | key: keyObj.key, 76 | kdfParams: { // pbkdf2 parameters that must be shared with receiver 77 | salt: jseu.encoder.encodeBase64(keyObj.salt), 78 | hash 79 | } 80 | }; 81 | }; 82 | 83 | /** 84 | * Generate Base64 encoded binary (used as master secret) 85 | * @param len {number} - length of master secret in bytes 86 | * @return {*} - master secret encoded into Base64 87 | */ 88 | export const generateBase64MasterSecret = (len) => { 89 | const jscu = getJscu(); 90 | const sec = jscu.random.getRandomBytes(len); 91 | return jseu.encoder.encodeBase64(sec); 92 | }; 93 | -------------------------------------------------------------------------------- /sample-04/src/encrypt.js: -------------------------------------------------------------------------------- 1 | // Works both in Node.js and Browsers by using "jscu" 2 | 3 | import jseu from 'js-encoding-utils'; 4 | import {getJscu} from './util/env'; 5 | import {pkcs7Padding} from './util/pkcs7'; 6 | 7 | /** 8 | * Encrypt data here 9 | * @param data {string} - plaintext data to be encrypted 10 | * @param key {Uint8Array} - 256bit key 11 | * @return {Promise<{data: *, iv: *}>} 12 | */ 13 | export const encrypt = async (data, key) => { 14 | const jscu = getJscu(); 15 | 16 | const iv = jscu.random.getRandomBytes(16); 17 | const encrypted = await jscu.aes.encrypt( 18 | jseu.encoder.stringToArrayBuffer(data), 19 | key, 20 | {name: 'AES-CBC', iv} 21 | ); 22 | 23 | return { 24 | data: jseu.encoder.encodeBase64(encrypted), 25 | iv: jseu.encoder.encodeBase64(iv) 26 | }; 27 | }; 28 | 29 | /** 30 | * Decrypt data 31 | * @param data {string} - encrypted data in base64 32 | * @param key {Uint8Array} - 256bit key 33 | * @param iv {string} - iv in base64 34 | * @return {Promise<*|void|Promise|IDBRequest|[]>} 35 | */ 36 | export const decrypt = async (data, key, iv) => { 37 | const jscu = getJscu(); 38 | 39 | const decrypted = await jscu.aes.decrypt( 40 | jseu.encoder.decodeBase64(data), 41 | key, 42 | {name: 'AES-CBC', iv: jseu.encoder.decodeBase64(iv)} 43 | ); 44 | 45 | return jseu.encoder.arrayBufferToString(decrypted); 46 | }; 47 | 48 | 49 | 50 | ///////////////////////////// 51 | // pseudo simulation of aes ecb mode 52 | // ecb mode is implemented by tweaking cbc-mode 53 | const AESBLOCK = 16; 54 | //// DO NOT USE ECB MODE IN PRODUCTION 55 | export const encryptECB = async (data, key) => { 56 | const jscu = getJscu(); 57 | 58 | const iv = new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); 59 | const uint8data = jseu.encoder.stringToArrayBuffer(data); 60 | const pad = pkcs7Padding(AESBLOCK - (uint8data.length % AESBLOCK), AESBLOCK); 61 | 62 | const plaintext = new Uint8Array( uint8data.length + pad.length ); 63 | plaintext.set(uint8data); 64 | plaintext.set(pad, uint8data.length); 65 | 66 | const blockNum = plaintext.length / AESBLOCK; 67 | const encrypted = new Uint8Array( plaintext.length ); 68 | 69 | for(let i = 0; i < blockNum; i++){ 70 | const block = plaintext.slice(i * AESBLOCK, (i+1) * AESBLOCK); 71 | const x = await jscu.aes.encrypt( 72 | block, 73 | key, 74 | {name: 'AES-CBC', iv} 75 | ); 76 | 77 | // prune trailer 16 bytes that always correspond to padding bytes in this case 78 | encrypted.set(x.slice(0,16), i*AESBLOCK); 79 | } 80 | 81 | return { 82 | data: jseu.encoder.encodeBase64(encrypted) 83 | }; 84 | }; 85 | -------------------------------------------------------------------------------- /sample-04/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | */ 4 | import {getMyData, postMyData} from './post-get'; 5 | import {generateBase64MasterSecret} from './derive-key'; 6 | import {encryptECB, encrypt} from './encrypt'; 7 | 8 | export {getMyData, postMyData, generateBase64MasterSecret, encryptECB, encrypt}; 9 | export default {getMyData, postMyData, generateBase64MasterSecret, encryptECB, encrypt}; 10 | -------------------------------------------------------------------------------- /sample-04/src/post-get-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | E2E Encryption Test 6 | 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample-04/src/post-get-node.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { postMyData, getMyData } from "./post-get"; 4 | import { Command } from "commander"; 5 | import { generateBase64MasterSecret } from "./derive-key"; 6 | import { encryptECB, encrypt } from "./encrypt"; 7 | import { getJscu } from "./util/env"; 8 | import jseu from "js-encoding-utils"; 9 | 10 | const pgm = new Command(); 11 | pgm.version("0.0.1"); 12 | 13 | /// for post 14 | pgm 15 | .command("post ", "") 16 | .description("Post data to json-server") 17 | .option("-p, --password ", "String password for encryption") 18 | .option("-m, --masterSecret ", "Base64 encoded master secret binary") 19 | .option("-h, --hash ", "Hash algorithm for key derivation", "SHA-256") 20 | .option("-i, --iterationCount ", "Iteration count for password-based key derivation", 2048) 21 | .option("-r, --remote", "Register to remote server (zettant.com)") 22 | .action(async (data, options) => { 23 | if (!options.password && !options.masterSecret) { 24 | console.error("Either string password or Base64-encoded master secret is required for encryption"); 25 | process.exit(1); 26 | } 27 | console.log(`> Register encrypted data to ${options.remote ? "remote" : "local"} server`); 28 | console.log(`> Data: ${data}`); 29 | if (options.password) console.log(`> Password: ${options.password}`); 30 | else console.log(`> Master secret: ${options.masterSecret}`); 31 | //////////////////////// 32 | // Derive key then encrypt 33 | const params = Object.assign( 34 | { data, remote: options.remote, hash: options.hash }, 35 | options.password 36 | ? { password: options.password, iterationCount: options.iterationCount } 37 | : { masterSecret: options.masterSecret } 38 | ); 39 | const res = await postMyData(params); 40 | console.log(`> Registered id: ${res.id}`); 41 | }); 42 | 43 | /// for get 44 | pgm 45 | .command("get ", "") 46 | .description("Get data from json-server") 47 | .option("-p, --password ", "String password for decryption") 48 | .option("-m, --masterSecret ", "Base64 encoded master secret binary") 49 | .option("-r, --remote", "Retrieve from remote server (zettant.com)") 50 | .action(async (id, options) => { 51 | if (!options.password && !options.masterSecret) { 52 | console.error("Either string password or Base64-encoded master secret is required for decryption"); 53 | process.exit(1); 54 | } 55 | console.log(`> Retrieve encrypted data to ${options.remote ? "remote" : "local"} server`); 56 | console.log(`> Id: ${id}`); 57 | if (options.password) console.log(`> Password: ${options.password}`); 58 | else console.log(`> Master secret: ${options.masterSecret}`); 59 | 60 | // Derive key then decrypt 61 | const params = Object.assign( 62 | { id, remote: options.remote }, 63 | options.password ? { password: options.password } : { masterSecret: options.masterSecret } 64 | ); 65 | const res = await getMyData(params); 66 | console.log(`> Decrypted data: ${res.data}`); 67 | }); 68 | 69 | /// for generate master secret encoded in Base64 70 | pgm 71 | .command("gen-secret ", "") 72 | .description("Generate master secret") 73 | .action((len) => { 74 | if (!len) { 75 | console.error("length of master secret in bytes is required"); 76 | process.exit(1); 77 | } 78 | const res = generateBase64MasterSecret(parseInt(len)); 79 | console.log(`> Generated master secret in Base64: ${res}`); 80 | }); 81 | 82 | /// weak ecb mode simulation 83 | pgm 84 | .command("aes-mode-compare ", "") 85 | .description("AES-ECB/CBC simulation, check the encrypted binary occurrence.") 86 | .action(async (data) => { 87 | const jscu = getJscu(); 88 | 89 | const ecbKey = jscu.random.getRandomBytes(32); 90 | console.log(`random key (Base64): ${jseu.encoder.encodeBase64(ecbKey)}`); 91 | console.log(`data (Hex): ${jseu.encoder.arrayBufferToHexString(jseu.encoder.stringToArrayBuffer(data))}`); 92 | const ecb = await encryptECB(data, ecbKey); 93 | console.log(`AES-ECB (Hex): ${jseu.encoder.arrayBufferToHexString(jseu.encoder.decodeBase64(ecb.data))}`); 94 | const cbc = await encrypt(data, ecbKey); 95 | console.log(`AES-CBC (Hex): ${jseu.encoder.arrayBufferToHexString(jseu.encoder.decodeBase64(cbc.data))}`); 96 | }); 97 | 98 | pgm.parse(process.argv); 99 | -------------------------------------------------------------------------------- /sample-04/src/post-get.js: -------------------------------------------------------------------------------- 1 | import {makeApiCall} from './util/comm'; 2 | import {mockDataUrl, remoteDataUrl} from './util/params'; 3 | import {deriveKeyFromPassword, deriveKeyFromMasterSecret} from './derive-key'; 4 | import jseu from 'js-encoding-utils'; 5 | 6 | import * as aes from './encrypt' 7 | 8 | /** 9 | * Post data 10 | * @param data {string} - plaintext data to be encrypted 11 | * @param password {string|undefined} - string password 12 | * @param masterSecret {string|undefined} - binary master secret in Base64 13 | * @param remote {boolean} - fetch remote json-server if true 14 | * @param hash {'SHA-256'|'SHA-384'|'SHA-512'} - hash algorithm for pbkdf2 15 | * @param iterationCount {number} - iteration count for pbkdf2 16 | * @return {Promise<*>} 17 | */ 18 | export const postMyData = async ({data, password, masterSecret, remote=false, hash='SHA-256', iterationCount=2048}) => { 19 | if((password && masterSecret) || (!password && !masterSecret)) throw new Error('Either one of password or masterSecret must be specified'); 20 | //////////////////////// 21 | const keyObj = (password) 22 | ? await deriveKeyFromPassword(password, 32, null, hash, iterationCount) // Derive key from password 23 | : await deriveKeyFromMasterSecret(masterSecret, 32, null, hash); // Derive key from master secret binary 24 | 25 | const kdf = (password) ? 'PBKDF2' : 'HKDF'; 26 | let msg = '> Derived key and its related params:\n' 27 | + `\t Derived key in Base64: ${jseu.encoder.encodeBase64(keyObj.key)}\n` 28 | + `\t ${kdf} Param - Salt in Base64: ${keyObj.kdfParams.salt}\n` 29 | + `\t ${kdf} Param - Hash: ${keyObj.kdfParams.hash}`; 30 | msg += (password) ? `\n\t ${kdf} Param - Iteration: ${keyObj.kdfParams.iterationCount}` : ''; 31 | console.log( msg ); 32 | 33 | const payload = {}; 34 | //////////////////////// 35 | // universal api 36 | const encryptedObj = await aes.encrypt(data, keyObj.key); 37 | //////////////////////// 38 | payload.data = encryptedObj.data; 39 | payload.iv = encryptedObj.iv; 40 | payload.kdfParams = keyObj.kdfParams; 41 | //////////////////////// 42 | 43 | const response = await makeApiCall({ 44 | method: 'POST', 45 | requestUrl: (remote) ? remoteDataUrl : mockDataUrl, 46 | payload, 47 | headers: {'Content-Type': 'application/json'}, 48 | mode: 'cors' 49 | }); 50 | 51 | return {id: response.id}; 52 | }; 53 | 54 | 55 | /** 56 | * Get data 57 | * @param dataId - id registered in the json server 58 | * @param password {string|undefined} - the string password 59 | * @param masterSecret {string|undefined} - binary master secret in Base64 60 | * @param remote {boolean} - fetch remote json-server if true 61 | * @return {Promise<*>} 62 | */ 63 | export const getMyData = async ({id, password, masterSecret, remote=false}) => { 64 | const data = await makeApiCall({ 65 | method: 'GET', 66 | requestUrl: `${(remote) ? remoteDataUrl : mockDataUrl}/${id}`, 67 | headers: {'Content-Type': 'application/json'}, 68 | mode: 'cors' 69 | }); 70 | 71 | //////////////////////// 72 | if((password && masterSecret) || (!password && !masterSecret)) throw new Error('Either one of password or masterSecret must be specified'); 73 | if(!data.data || !data.iv || !data.kdfParams) throw new Error(`Maybe Unencrypted data => contents: ${data}`); 74 | const keyObj = (password) 75 | ? await deriveKeyFromPassword(password, 32, data.kdfParams.salt, data.kdfParams.hash, data.kdfParams.iterationCount) // Derive key from password 76 | : await deriveKeyFromMasterSecret(masterSecret, 32, data.kdfParams.salt, data.kdfParams.hash); // Derive key from master secret binary 77 | 78 | const kdf = (password) ? 'PBKDF2' : 'HKDF'; 79 | let msg = '> Derived key and its related params:\n' 80 | + `\t Derived key in Base64: ${jseu.encoder.encodeBase64(keyObj.key)}\n` 81 | + `\t ${kdf} Param - Salt in Base64: ${keyObj.kdfParams.salt}\n` 82 | + `\t ${kdf} Param - Hash: ${keyObj.kdfParams.hash}`; 83 | msg += (password) ? `\n\t ${kdf} Param - Iteration: ${keyObj.kdfParams.iterationCount}` : ''; 84 | console.log( msg ); 85 | 86 | //////////////////////// 87 | // universal api 88 | const decrypted = await aes.decrypt(data.data, keyObj.key, data.iv); 89 | //////////////////////// 90 | return {data: decrypted}; 91 | //////////////////////// 92 | }; 93 | -------------------------------------------------------------------------------- /sample-04/src/util/comm.js: -------------------------------------------------------------------------------- 1 | import {getFetch} from './env.js'; 2 | 3 | /** 4 | * make RESTful API call to the security hub api. 5 | * @param method {String} - 'GET' or 'POST' 6 | * @param requestUrl {String} - url connected with the endpoint defined above. 7 | * @param payload {Object} - request body. 8 | * @param headers - header params. 9 | * @param mode 10 | * @return {Promise<*>} 11 | */ 12 | export const makeApiCall = async ({method, requestUrl, payload=null, headers=null, mode='cors'}) => { 13 | //logger.debug('make API call to AWS API Gateway'); 14 | const fetch = getFetch(); 15 | const body = (payload)? JSON.stringify(payload) : null; 16 | const response = await fetch(requestUrl, { 17 | method, 18 | body, 19 | headers, 20 | mode 21 | }); 22 | 23 | let success = false; 24 | if (response.status >= 200 && response.status < 300) { 25 | success = true; 26 | } 27 | 28 | const responseJson = await response.json(); 29 | if(success) { 30 | return responseJson; 31 | } 32 | else { 33 | const err = Object.assign({status: response.status, statusText: response.statusText}, responseJson); 34 | throw new Error(JSON.stringify(err)); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /sample-04/src/util/env.js: -------------------------------------------------------------------------------- 1 | import jscu from 'js-crypto-utils'; 2 | import fetch from 'cross-fetch'; 3 | 4 | /** 5 | * Get jscu 6 | */ 7 | export const getJscu = () => { 8 | const global = Function('return this;')(); 9 | if (typeof window !== 'undefined'){ 10 | return window.jscu; 11 | } 12 | else{ 13 | global.jscu = jscu; 14 | return jscu; 15 | } 16 | }; 17 | 18 | /** 19 | * Get fetch 20 | */ 21 | export const getFetch = () => { 22 | // node-fetch in aws sdk 23 | const global = Function('return this;')(); 24 | if (typeof window === 'undefined'){ 25 | global.fetch = fetch; 26 | return fetch; 27 | } 28 | else { 29 | return window.fetch; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /sample-04/src/util/params.js: -------------------------------------------------------------------------------- 1 | export const mockDataUrl = 'http://localhost:3000/data'; // for HTTP local mock server 2 | export const remoteDataUrl = 'https://e2e.secarchlab.net/data'; // for HTTPS remote server 3 | -------------------------------------------------------------------------------- /sample-04/src/util/pkcs7.js: -------------------------------------------------------------------------------- 1 | 2 | // PKCS #7 Padding, RFC 5652 3 | export const pkcs7Padding = (paddingLength, blockLength) => { 4 | const pad = (paddingLength) ? new Uint8Array(paddingLength) : new Uint8Array(blockLength); 5 | const filling = (paddingLength) ? paddingLength : blockLength; 6 | return pad.map( () => filling); 7 | }; 8 | -------------------------------------------------------------------------------- /sample-04/webpack.baseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "libName": "e2eTest" 3 | } 4 | -------------------------------------------------------------------------------- /sample-04/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const webpack = require('webpack'); 3 | const base = require('./webpack.baseconfig.json'); 4 | 5 | const config = { 6 | entry: ['./src/index.js'], 7 | 8 | output: { 9 | filename: `${base.libName}.bundle.js`, 10 | chunkFilename: '[name].js', 11 | path: path.resolve(__dirname, './dist'), 12 | publicPath: path.resolve(__dirname, './dist'), 13 | library: base.libName, 14 | libraryTarget: 'umd', 15 | globalObject: 'this' // for node js import 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx', '.mjs'], 19 | modules: ['node_modules'] 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(m|)js$/, 25 | use: [{ 26 | loader: 'babel-loader' 27 | }], 28 | exclude: path.join(__dirname, 'node_modules') // exclude: /node_modules/ 29 | }, 30 | ], 31 | }, 32 | externals: { 33 | crypto: true, 34 | 'cross-fetch': true 35 | } 36 | }; 37 | 38 | module.exports = (env, argv) => { 39 | config.mode = (typeof argv.mode !== 'undefined' && argv.mode === 'production') ? argv.mode : 'development'; 40 | 41 | if (config.mode === 'production') console.log('Webpack for production'); 42 | else{ 43 | console.log('Webpack for development'); 44 | config.devtool = 'inline-source-map'; // add inline source map 45 | } 46 | 47 | return config; 48 | }; 49 | -------------------------------------------------------------------------------- /sample-05-rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /sample-05-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli05" 3 | authors = ["Jun Kurihara"] 4 | description = "Rust version of sample-05" 5 | repository = "https://github.com/junkurihara/lecture-security_engineering" 6 | version = "0.1.0" 7 | edition = "2021" 8 | publish = false 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | aes = "0.8.4" 14 | anyhow = "1.0.91" 15 | cbc = { version = "0.1.2", features = ["alloc"] } 16 | clap = { version = "4.5.20", features = ["std", "cargo", "wrap_help", "derive"] } 17 | hkdf = "0.12.4" 18 | rand = "0.8.5" 19 | rsa = { version = "0.9.6" } 20 | serde = { version = "1.0.213", features = ["derive"] } 21 | sha2 = "0.10.8" 22 | hex = "0.4.3" 23 | p256 = "0.13.2" 24 | elliptic-curve = { version = "0.13.8", features = ["sec1", "pkcs8", "ecdh"] } 25 | p384 = "0.13.0" 26 | rmp-serde = "1.3.0" 27 | 28 | [dev-dependencies] 29 | hex-literal = "0.4.1" 30 | base64 = "0.22.1" 31 | -------------------------------------------------------------------------------- /sample-05-rs/README.md: -------------------------------------------------------------------------------- 1 | # sample-05-rs 2 | 3 | Rust implementation of [`sample-05`](../sample-05/), which is fully compatible with the original version. 4 | 5 | ## Build 6 | 7 | ```shell: 8 | $ cargo build --release 9 | ``` 10 | 11 | Then you have an executable binary `./target/release/cli05`. 12 | 13 | ## Usage 14 | 15 | ```shell: 16 | $ ./target/release/cli05 -h 17 | Rust version of sample-05 18 | 19 | Usage: cli05 20 | 21 | Commands: 22 | rsa-oaep-demo Execute RSAES-OAEP encryption and decryption demo with RSA key generation 23 | rsa-keygen Generate RSA Key 24 | rsa-oaep-encrypt RSA-OAEP Encryption 25 | rsa-oaep-decrypt RSA-OAEP Decryption 26 | check-ecdh Generate ECC key pair and check the consistency of ECDH derived bits 27 | ecc-keygen Generate ECC Key 28 | ecdh-aes-encrypt ECDH with AES Encryption 29 | ecdh-aes-decrypt ECDH with AES Decryption 30 | help Print this message or the help of the given subcommand(s) 31 | 32 | Options: 33 | -h, --help Print help 34 | -V, --version Print version 35 | ``` 36 | 37 | ```shell: 38 | $ ../target/debug/cli05 rsa-oaep-demo -h 39 | Execute RSAES-OAEP encryption and decryption demo with RSA key generation 40 | 41 | Usage: cli05 rsa-oaep-demo 42 | 43 | Arguments: 44 | plaintext data string 45 | 46 | Options: 47 | -h, --help Print help 48 | ``` 49 | 50 | ```shell: 51 | $ ../target/debug/cli05 rsa-keygen -h 52 | Generate RSA Key 53 | 54 | Usage: cli05 rsa-keygen [BITS] 55 | 56 | Arguments: 57 | [BITS] Modulus length like 2048 [default: 2048] 58 | 59 | Options: 60 | -h, --help Print help 61 | ``` 62 | 63 | ```shell: 64 | $ ../target/debug/cli05 rsa-oaep-encrypt -h 65 | RSA-OAEP Encryption 66 | 67 | Usage: cli05 rsa-oaep-encrypt --publicKey 68 | 69 | Arguments: 70 | plaintext data string 71 | 72 | Options: 73 | -p, --publicKey hex DER-formatted public key 74 | -h, --help Print help 75 | ``` 76 | 77 | ```shell: 78 | $ ../target/debug/cli05 rsa-oaep-decrypt -h 79 | RSA-OAEP Decryption 80 | 81 | Usage: cli05 rsa-oaep-decrypt --privateKey 82 | 83 | Arguments: 84 | encrypted data string 85 | 86 | Options: 87 | -s, --privateKey hex DER-formatted private key 88 | -h, --help Print help 89 | ``` 90 | 91 | ```shell: 92 | $ ../target/debug/cli05 check-ecdh -h 93 | Generate ECC key pair and check the consistency of ECDH derived bits 94 | 95 | Usage: cli05 check-ecdh 96 | 97 | Options: 98 | -h, --help Print help 99 | ``` 100 | 101 | ```shell: 102 | $ ../target/debug/cli05 ecc-keygen -h 103 | Generate ECC Key 104 | 105 | Usage: cli05 ecc-keygen [CURVE] 106 | 107 | Arguments: 108 | [CURVE] Curve name like P-256 [default: P-256] 109 | 110 | Options: 111 | -h, --help Print help 112 | ``` 113 | 114 | ```shell: 115 | $ ../target/debug/cli05 ecdh-aes-encrypt -h 116 | ECDH with AES Encryption 117 | 118 | Usage: cli05 ecdh-aes-encrypt --publicKey --privateKey 119 | 120 | Arguments: 121 | plaintext data string 122 | 123 | Options: 124 | -p, --publicKey hex DER-formatted public key 125 | -s, --privateKey hex DER-formatted private key 126 | -h, --help Print help 127 | ``` 128 | 129 | ```shell: 130 | $ ../target/debug/cli05 ecdh-aes-decrypt -h 131 | ECDH with AES Decryption 132 | 133 | Usage: cli05 ecdh-aes-decrypt --publicKey --privateKey 134 | 135 | Arguments: 136 | encrypted and msgpacked data string in hex 137 | 138 | Options: 139 | -p, --publicKey hex DER-formatted public key 140 | -s, --privateKey hex DER-formatted private key 141 | -h, --help Print help 142 | ``` 143 | -------------------------------------------------------------------------------- /sample-05-rs/src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct ClapArgs { 6 | #[clap(subcommand)] 7 | pub subcommand: SubCommands, 8 | } 9 | 10 | #[allow(non_snake_case, non_camel_case_types)] 11 | #[derive(Debug, Subcommand)] 12 | pub enum SubCommands { 13 | /// Execute RSAES-OAEP encryption and decryption demo with RSA key generation 14 | Rsa_Oaep_Demo { 15 | /// plaintext data string 16 | data: String, 17 | }, 18 | /// Generate RSA Key 19 | Rsa_Keygen { 20 | /// Modulus length like 2048 21 | #[arg(default_value = "2048")] 22 | bits: usize, 23 | }, 24 | /// RSA-OAEP Encryption 25 | Rsa_Oaep_Encrypt { 26 | /// hex DER-formatted public key 27 | #[arg(short, long = "publicKey")] 28 | public_key: String, 29 | 30 | /// plaintext data string 31 | data: String, 32 | }, 33 | /// RSA-OAEP Decryption 34 | Rsa_Oaep_Decrypt { 35 | /// hex DER-formatted private key 36 | #[arg(short = 's', long = "privateKey")] 37 | private_key: String, 38 | 39 | /// encrypted data string 40 | data: String, 41 | }, 42 | /// Generate ECC key pair and check the consistency of ECDH derived bits 43 | Check_Ecdh, 44 | /// Generate ECC Key 45 | Ecc_Keygen { 46 | /// Curve name like P-256 47 | #[arg(default_value = "P-256")] 48 | curve: String, 49 | }, 50 | /// ECDH with AES Encryption 51 | Ecdh_Aes_Encrypt { 52 | /// hex DER-formatted public key 53 | #[arg(short, long = "publicKey")] 54 | public_key: String, 55 | 56 | /// hex DER-formatted private key 57 | #[arg(short = 's', long = "privateKey")] 58 | private_key: String, 59 | 60 | /// plaintext data string 61 | data: String, 62 | }, 63 | /// ECDH with AES Decryption 64 | Ecdh_Aes_Decrypt { 65 | /// hex DER-formatted public key 66 | #[arg(short, long = "publicKey")] 67 | public_key: String, 68 | 69 | /// hex DER-formatted private key 70 | #[arg(short = 's', long = "privateKey")] 71 | private_key: String, 72 | 73 | /// encrypted and msgpacked data string in hex 74 | data: String, 75 | }, 76 | } 77 | -------------------------------------------------------------------------------- /sample-05-rs/src/crypto.rs: -------------------------------------------------------------------------------- 1 | use crate::{error::*, key::BinaryKey}; 2 | use aes::cipher::{ 3 | block_padding::Pkcs7, 4 | generic_array::{ 5 | typenum::{U16, U32}, 6 | GenericArray, 7 | }, 8 | BlockDecryptMut, BlockEncryptMut, KeyIvInit, 9 | }; 10 | use rand::RngCore; 11 | type Aes256CbcEnc = cbc::Encryptor; 12 | type Aes256CbcDec = cbc::Decryptor; 13 | 14 | pub struct Encrypted { 15 | pub data: Vec, 16 | pub iv: Vec, 17 | } 18 | 19 | pub fn encrypt(data: &[u8], key: &BinaryKey, iv: Option<&[u8]>) -> Result { 20 | let key_array: &GenericArray = GenericArray::from_slice(&key.key); 21 | let iv = match iv { 22 | None => { 23 | let mut iv = [0u8; 16]; 24 | rand::thread_rng().fill_bytes(&mut iv); 25 | iv.to_vec() 26 | } 27 | Some(v) => v.to_vec(), 28 | }; 29 | let iv: &GenericArray = GenericArray::from_slice(&iv); 30 | 31 | let encrypted = Aes256CbcEnc::new(key_array, iv).encrypt_padded_vec_mut::(data); 32 | 33 | Ok(Encrypted { 34 | data: encrypted, 35 | iv: iv.to_vec(), 36 | }) 37 | } 38 | 39 | pub fn decrypt(encrypted: &Encrypted, key: &BinaryKey) -> Result> { 40 | let key_array: &GenericArray = GenericArray::from_slice(&key.key); 41 | let iv: &GenericArray = GenericArray::from_slice(&encrypted.iv); 42 | Aes256CbcDec::new(key_array, iv) 43 | .decrypt_padded_vec_mut::(&encrypted.data) 44 | .map_err(|e| anyhow!(e)) 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn aes_cbc_works() -> Result<()> { 53 | let data = b"secret".as_slice(); 54 | let key = BinaryKey::try_new("password".as_bytes(), 32, None)?; 55 | let encrypted = encrypt(data, &key, None)?; 56 | let decrypted = decrypt(&encrypted, &key)?; 57 | 58 | assert_eq!(&decrypted, data); 59 | Ok(()) 60 | } 61 | 62 | #[test] 63 | fn aes_cbc_test_vector() -> Result<()> { 64 | let data = b"hello my super secret world!!!"; 65 | let salt = hex::decode("8db7cbd35eb24bd4546fc49ffba9be3e6d8bd48a3bbb54a9a87b2bf91e914eee")?; 66 | let key = BinaryKey::try_new(b"my secret key", 32, Some(&salt))?; 67 | 68 | let iv = hex::decode("ceec133d6ee7ad6a27ea7121cabcf3c4")?; 69 | let encrypted_data = hex::decode("ea581e08b09f990ea4b68cb5fc119e773fb4103399cb15c6f5991b50daafe6e0")?; 70 | 71 | let encrypted = encrypt(data, &key, Some(&iv))?; 72 | 73 | assert_eq!(encrypted.data, encrypted_data); 74 | assert_eq!(iv, encrypted.iv); 75 | 76 | let dec = decrypt( 77 | &Encrypted { 78 | data: encrypted_data, 79 | iv, 80 | }, 81 | &key, 82 | )?; 83 | 84 | assert_eq!(data, dec.as_slice()); 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sample-05-rs/src/error.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub use anyhow::{anyhow, bail, ensure, Context, Result}; 3 | -------------------------------------------------------------------------------- /sample-05-rs/src/key.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use hkdf::Hkdf; 3 | use rand::RngCore; 4 | use sha2::Sha256; 5 | 6 | const SALT_LEN: usize = 32; 7 | 8 | pub struct BinaryKey { 9 | pub key: Vec, 10 | pub salt: Vec, 11 | } 12 | 13 | impl BinaryKey { 14 | pub fn try_new(master: &[u8], len: usize, salt: Option<&[u8]>) -> Result { 15 | let salt_bin = match salt { 16 | Some(v) => v.to_vec(), 17 | None => { 18 | let mut buf = [0u8; SALT_LEN]; 19 | rand::thread_rng().fill_bytes(&mut buf); 20 | buf.to_vec() 21 | } 22 | }; 23 | let info = b""; 24 | let hkdf = Hkdf::::new(Some(&salt_bin), master); 25 | let mut okm = vec![Default::default(); len]; 26 | hkdf.expand(info, &mut okm).map_err(|e| anyhow!(e))?; 27 | 28 | Ok(Self { 29 | key: okm.to_vec(), 30 | salt: salt_bin.to_vec(), 31 | }) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use base64::{engine::general_purpose, Engine as _}; 39 | 40 | #[test] 41 | fn gen_binary_key_with_salt() -> Result<()> { 42 | // hkdf 43 | let mut ikm = Vec::with_capacity(32); 44 | for i in 0..32 { 45 | ikm.push(i as u8) 46 | } 47 | 48 | let binary_key = BinaryKey::try_new(&ikm, 144, Some(&ikm))?; 49 | 50 | let test_vector = "fJHB6pVraz09Ognk2NRFR/DKdsK0cnFQORjocdWbv6YaAV7m9LmrZhT2O8v1yBEZXBbEaqiRfV59VGWVd5L685jh6IHoZWoTN50i8JLMogXrnB/mvCSwLEMjY4dTxbHspz88XS+94aKvl/Hql9+IGfnOWNAcb6brgCEoD1rb7pmYT2FzIVk3qLWNTO2QtTl1"; 51 | assert_eq!( 52 | binary_key.key.as_slice(), 53 | general_purpose::STANDARD.decode(test_vector)? 54 | ); 55 | Ok(()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sample-05-rs/src/rsa.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use rsa::{ 3 | pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}, 4 | Oaep, RsaPrivateKey, RsaPublicKey, 5 | }; 6 | 7 | #[derive(Debug)] 8 | pub struct RsaKeyPair { 9 | pub public: RsaPublicKey, 10 | pub private: Option, 11 | } 12 | 13 | impl RsaKeyPair { 14 | pub fn new(bits: &usize) -> Result { 15 | let mut rng = rand::thread_rng(); 16 | let private_key = RsaPrivateKey::new(&mut rng, *bits)?; 17 | let public_key = private_key.to_public_key(); 18 | 19 | Ok(Self { 20 | public: public_key, 21 | private: Some(private_key), 22 | }) 23 | } 24 | 25 | pub fn to_spki_public_der(&self) -> Result> { 26 | let der = self.public.to_public_key_der()?.as_bytes().to_vec(); 27 | Ok(der) 28 | } 29 | pub fn to_pkcs8_private_der(&self) -> Result> { 30 | if self.private.is_none() { 31 | bail!("No private key"); 32 | } 33 | let der = self.private.as_ref().unwrap().to_pkcs8_der()?.as_bytes().to_vec(); 34 | Ok(der) 35 | } 36 | 37 | pub fn from_spki_public_der(der: &[u8]) -> Result { 38 | let public_key = RsaPublicKey::from_public_key_der(der)?; 39 | Ok(RsaKeyPair { 40 | public: public_key, 41 | private: None, 42 | }) 43 | } 44 | 45 | pub fn from_pkcs8_private_der(der: &[u8]) -> Result { 46 | let private_key = RsaPrivateKey::from_pkcs8_der(der)?; 47 | let public_key = private_key.to_public_key(); 48 | Ok(RsaKeyPair { 49 | public: public_key, 50 | private: Some(private_key), 51 | }) 52 | } 53 | 54 | pub fn oaep_encrypt(&self, data: &[u8]) -> Result> { 55 | let mut rng = rand::thread_rng(); 56 | let padding = Oaep::new::(); 57 | let enc_data = self.public.encrypt(&mut rng, padding, data)?; 58 | Ok(enc_data) 59 | } 60 | 61 | pub fn oaep_decrypt(&self, data: &[u8]) -> Result> { 62 | if self.private.is_none() { 63 | bail!("No private key"); 64 | } 65 | let padding = Oaep::new::(); 66 | let dec_data = self.private.as_ref().unwrap().decrypt(padding, data)?; 67 | Ok(dec_data) 68 | } 69 | } 70 | 71 | #[cfg(test)] 72 | mod tests { 73 | use super::*; 74 | use crate::util::*; 75 | 76 | #[test] 77 | fn test_keygen() { 78 | let keypair = RsaKeyPair::new(&2048).unwrap(); 79 | 80 | let public_der = keypair.to_spki_public_der().unwrap(); 81 | let private_der = keypair.to_pkcs8_private_der().unwrap(); 82 | 83 | let keypair_from_public_der = RsaKeyPair::from_spki_public_der(&public_der).unwrap(); 84 | let keypair_from_private_der = RsaKeyPair::from_pkcs8_private_der(&private_der).unwrap(); 85 | 86 | let public_der2 = keypair_from_public_der.to_spki_public_der().unwrap(); 87 | let public_der3 = keypair_from_private_der.to_spki_public_der().unwrap(); 88 | assert_eq!(public_der, public_der2); 89 | assert_eq!(public_der, public_der3); 90 | 91 | let private_der2 = keypair_from_private_der.to_pkcs8_private_der().unwrap(); 92 | assert_eq!(private_der, private_der2); 93 | } 94 | 95 | #[test] 96 | fn test_keygen_encrypt_decrypt() { 97 | let keypair = RsaKeyPair::new(&2048).unwrap(); 98 | 99 | let plaintext = "hello"; 100 | 101 | let ciphertext = keypair.oaep_encrypt(plaintext.as_bytes()).unwrap(); 102 | 103 | let plaintext2 = String::from_utf8(keypair.oaep_decrypt(&ciphertext).unwrap()).unwrap(); 104 | 105 | assert_eq!(plaintext.to_string(), plaintext2); 106 | } 107 | 108 | #[test] 109 | fn test_hex() { 110 | let v: [u8; 3] = [0x01, 0x02, 0x03]; 111 | let s = v.to_vec().to_hex_string(); 112 | assert_eq!(s, "010203") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /sample-05-rs/src/util.rs: -------------------------------------------------------------------------------- 1 | pub trait ToHexString { 2 | fn to_hex_string(&self) -> String; 3 | } 4 | 5 | impl ToHexString for Vec { 6 | fn to_hex_string(&self) -> String { 7 | self.iter().fold("".to_string(), |acc, n| format!("{}{:02x}", acc, n)) 8 | } 9 | } 10 | 11 | impl ToHexString for &[u8] { 12 | fn to_hex_string(&self) -> String { 13 | self.iter().fold("".to_string(), |acc, n| format!("{}{:02x}", acc, n)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample-05/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "@babel/preset-env", { 4 | "targets": { 5 | "browsers": [ 6 | "last 2 chrome versions", 7 | "last 2 firefox versions", 8 | "IE 11", 9 | "last 2 Edge versions" 10 | ] 11 | }, 12 | "useBuiltIns": false 13 | } ] 14 | ], 15 | "ignore": [ "node_modules" ], 16 | "only": [ "src", "test" ], 17 | "plugins": [ 18 | [ 19 | "@babel/plugin-transform-runtime", 20 | { 21 | "@babel/polyfill": false, 22 | "regenerator": true 23 | } 24 | ] 25 | ], 26 | "env": { 27 | "production": { 28 | }, 29 | "development": { 30 | }, 31 | "test": { 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample-05/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 2 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "always" 28 | ], 29 | "arrow-body-style": "error", 30 | "arrow-parens": "error", 31 | "arrow-spacing": "error", 32 | "generator-star-spacing": "error", 33 | "no-duplicate-imports": "error", 34 | "no-useless-computed-key": "error", 35 | "no-useless-constructor": "error", 36 | "no-useless-rename": "error", 37 | "no-var": "error", 38 | "object-shorthand": "error", 39 | "prefer-arrow-callback": "error", 40 | "prefer-const": "error", 41 | "prefer-rest-params": "error", 42 | "prefer-spread": "error", 43 | "prefer-template": "error", 44 | "rest-spread-spacing": "error", 45 | "template-curly-spacing": "error", 46 | "yield-star-spacing": "error" 47 | }, 48 | "globals": { 49 | "window": false, 50 | "_window": false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sample-05/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | obsolete/ 4 | thumbs.db 5 | yarn-error.log 6 | .idea/ 7 | .vscode/ 8 | .coverage/ 9 | .nyc_output/ 10 | dist/ 11 | coverage/ 12 | data/db.json 13 | -------------------------------------------------------------------------------- /sample-05/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jun Kurihara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sample-05/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e-security-class-sample", 3 | "version": "0.1.0", 4 | "description": "sample for the class", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "preinstall": "npx only-allow pnpm", 9 | "execute": "node -r @babel/register src/commands-node.js", 10 | "build": "./node_modules/.bin/webpack --mode development --config webpack.config.js", 11 | "cleanup": "rm -rf ./dist coverage .nyc_output; rm -rf ./test/html/*.bundle.js; rm -rf ./test/html/test.html; rm -rf ./node_modules" 12 | }, 13 | "author": "Jun Kurihara", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@babel/cli": "7.26.4", 17 | "@babel/core": "7.26.0", 18 | "@babel/plugin-transform-regenerator": "7.25.9", 19 | "@babel/plugin-transform-runtime": "7.25.9", 20 | "@babel/preset-env": "7.26.0", 21 | "@babel/register": "7.25.9", 22 | "@babel/eslint-parser": "7.25.9", 23 | "babel-loader": "9.2.1", 24 | "cross-env": "7.0.3", 25 | "eslint": "9.17.0", 26 | "webpack": "5.97.1", 27 | "webpack-cli": "6.0.1" 28 | }, 29 | "dependencies": { 30 | "@babel/runtime": "~7.26.0", 31 | "commander": "~12.1.0", 32 | "js-crypto-ec": "1.0.7", 33 | "js-crypto-utils": "1.0.7", 34 | "js-encoding-utils": "0.7.3", 35 | "msgpack-lite": "~0.1.26" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample-05/src/commands-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | E2E Encryption Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /sample-05/src/derive-key.js: -------------------------------------------------------------------------------- 1 | import {getJscu} from './util/env'; 2 | import jseu from 'js-encoding-utils'; 3 | 4 | /** 5 | * HKDF for perfect forward secrecy 6 | * @param masterSecret {string} - master secret (binary seed) in hex string 7 | * @param len {number} - output key length in byte 8 | * @param salt {string|null} - Uint8Array salt in hex string 9 | * @param hash {string} - Hash used in PBKDF2 algorithm, like 'SHA-256' 10 | * @return {Promise<{kdfParams: {salt: *, hash: *}, key: *}>} 11 | */ 12 | export const deriveKeyFromMasterSecret = async (masterSecret, len, salt=null, hash='SHA-256') => { 13 | const jscu = getJscu(); 14 | 15 | // derive key from master secret binary 16 | // following params (salt, iterationCount, aesKeyLen, hash) must be shared with receiver. 17 | if(!salt){ 18 | salt = jscu.random.getRandomBytes(32); // Uint8Array -> must be shared with receiver 19 | } 20 | else { 21 | salt = jseu.encoder.hexStringToArrayBuffer(salt); 22 | } 23 | 24 | const keyObj = await jscu.hkdf.compute( 25 | jseu.encoder.hexStringToArrayBuffer(masterSecret), 26 | hash, 27 | len, 28 | '', // 'info' field for RFC5869. This could be always blank. 29 | salt 30 | ).catch( (e) => { 31 | throw new Error(`failed to derive binary key from master secret binary: ${e.message}`); 32 | }); 33 | return { 34 | key: keyObj.key, 35 | kdfParams: { // pbkdf2 parameters that must be shared with receiver 36 | salt: jseu.encoder.arrayBufferToHexString(keyObj.salt), 37 | hash 38 | } 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /sample-05/src/encryptAES.js: -------------------------------------------------------------------------------- 1 | // Works both in Node.js and Browsers by using "jscu" 2 | 3 | import jseu from 'js-encoding-utils'; 4 | import {getJscu} from './util/env'; 5 | 6 | /** 7 | * Encrypt data here 8 | * @param data {string} - plaintext data to be encrypted 9 | * @param key {Uint8Array} - 256bit key 10 | * @return {Promise<{data: *, iv: *}>} 11 | */ 12 | export const encryptAES = async (data, key) => { 13 | const jscu = getJscu(); 14 | 15 | const iv = jscu.random.getRandomBytes(16); 16 | const encrypted = await jscu.aes.encrypt( 17 | jseu.encoder.stringToArrayBuffer(data), 18 | key, 19 | {name: 'AES-CBC', iv} 20 | ); 21 | 22 | return { 23 | data: jseu.encoder.arrayBufferToHexString(encrypted), 24 | iv: jseu.encoder.arrayBufferToHexString(iv) 25 | }; 26 | }; 27 | 28 | /** 29 | * Decrypt data 30 | * @param data {string} - encrypted data in hex string 31 | * @param key {Uint8Array} - 256bit key 32 | * @param iv {string} - iv in hex string 33 | * @return {Promise<*|void|Promise|IDBRequest|[]>} 34 | */ 35 | export const decryptAES = async (data, key, iv) => { 36 | const jscu = getJscu(); 37 | 38 | const decrypted = await jscu.aes.decrypt( 39 | jseu.encoder.hexStringToArrayBuffer(data), 40 | key, 41 | {name: 'AES-CBC', iv: jseu.encoder.hexStringToArrayBuffer(iv)} 42 | ); 43 | 44 | return jseu.encoder.arrayBufferToString(decrypted); 45 | }; 46 | -------------------------------------------------------------------------------- /sample-05/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | */ 4 | import {ecdh, ecKeyGen, rsaKeyGen, rsaOaepDecrypt, rsaOaepEncrypt} from './test-apis'; 5 | import {deriveKeyFromMasterSecret} from './derive-key'; 6 | import {encryptAES, decryptAES} from './encryptAES'; 7 | 8 | export {ecdh, encryptAES, decryptAES, ecKeyGen, rsaKeyGen, rsaOaepDecrypt, rsaOaepEncrypt, deriveKeyFromMasterSecret}; 9 | export default {ecdh, encryptAES, decryptAES, ecKeyGen, rsaKeyGen, rsaOaepDecrypt, rsaOaepEncrypt, deriveKeyFromMasterSecret}; 10 | -------------------------------------------------------------------------------- /sample-05/src/test-apis.js: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | 3 | import {getJscu, getJscec} from './util/env'; 4 | 5 | 6 | export const ecdh = async (publicDer, privateDer) => { 7 | const jscu = getJscu(); 8 | const jscec = getJscec(); 9 | 10 | const publicKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(publicDer)); 11 | const privateKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(privateDer)); 12 | const publicJwk = await publicKey.export('jwk'); 13 | const privateJwk = await privateKey.export('jwk'); 14 | 15 | // Derive shared bits at each end. 16 | const derived = await jscec.deriveSecret(publicJwk, privateJwk); // JWK formatted key is required 17 | return jseu.encoder.arrayBufferToHexString(derived); 18 | }; 19 | 20 | export const rsaOaepEncrypt = async (stringData, publicDer, hash = 'SHA-256') => { 21 | const jscu = getJscu(); 22 | 23 | const publicKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(publicDer)); 24 | 25 | const encrypted = await jscu.pkc.encrypt( 26 | jseu.encoder.stringToArrayBuffer(stringData), 27 | publicKey, 28 | {hash} // for OAEP 29 | ); 30 | return jseu.encoder.arrayBufferToHexString(encrypted.data); 31 | }; 32 | 33 | export const rsaOaepDecrypt = async (encryptedString, privateDer, hash = 'SHA-256') => { 34 | const jscu = getJscu(); 35 | 36 | const privateKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(privateDer)); 37 | 38 | const decrypted = await jscu.pkc.decrypt( 39 | jseu.encoder.hexStringToArrayBuffer(encryptedString), 40 | privateKey, 41 | {hash} // for OAEP 42 | ); 43 | return jseu.encoder.arrayBufferToString(decrypted); 44 | }; 45 | 46 | 47 | export const rsaKeyGen = async (bits = 2048) => { 48 | const jscu = getJscu(); 49 | const keyPair = await jscu.pkc.generateKey('RSA', {modulusLength: bits}); 50 | return { 51 | publicKey: jseu.encoder.arrayBufferToHexString(await keyPair.publicKey.export('der')), 52 | privateKey: jseu.encoder.arrayBufferToHexString(await keyPair.privateKey.export('der')) 53 | }; 54 | }; 55 | 56 | export const ecKeyGen = async (namedCurve = 'P-256') => { 57 | const jscu = getJscu(); 58 | const keyPair = await jscu.pkc.generateKey('EC', {namedCurve}); 59 | return { 60 | publicKey: jseu.encoder.arrayBufferToHexString(await keyPair.publicKey.export('der')), 61 | privateKey: jseu.encoder.arrayBufferToHexString(await keyPair.privateKey.export('der')) 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /sample-05/src/util/env.js: -------------------------------------------------------------------------------- 1 | import jscu from 'js-crypto-utils'; 2 | import jscec from 'js-crypto-ec'; 3 | 4 | /** 5 | * Get jscu 6 | */ 7 | export const getJscu = () => { 8 | const global = Function('return this;')(); 9 | if (typeof window !== 'undefined'){ 10 | return window.jscu; 11 | } 12 | else{ 13 | global.jscu = jscu; 14 | return jscu; 15 | } 16 | }; 17 | 18 | /** 19 | * Get ec 20 | */ 21 | export const getJscec = () => { 22 | const global = Function('return this;')(); 23 | if (typeof window !== 'undefined'){ 24 | return window.jscec; 25 | } 26 | else{ 27 | global.jscec = jscec; 28 | return jscec; 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /sample-05/src/util/format.js: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | import {getJscu} from './env'; 3 | 4 | export const ecPemToHexString = async (pemKey) => { 5 | const jscu = getJscu(); 6 | const keyObj = new jscu.Key('pem', pemKey); 7 | const bin = await keyObj.export('oct', {compact: true}); 8 | return jseu.encoder.arrayBufferToHexString(bin); 9 | }; 10 | -------------------------------------------------------------------------------- /sample-05/webpack.baseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "libName": "e2eTest" 3 | } 4 | -------------------------------------------------------------------------------- /sample-05/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const webpack = require('webpack'); 3 | const base = require('./webpack.baseconfig.json'); 4 | 5 | const config = { 6 | entry: ['./src/index.js'], 7 | 8 | output: { 9 | filename: `${base.libName}.bundle.js`, 10 | chunkFilename: '[name].js', 11 | path: path.resolve(__dirname, './dist'), 12 | publicPath: path.resolve(__dirname, './dist'), 13 | library: base.libName, 14 | libraryTarget: 'umd', 15 | globalObject: 'this' // for node js import 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx', '.mjs'], 19 | modules: ['node_modules'] 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(m|)js$/, 25 | use: [{ 26 | loader: 'babel-loader' 27 | }], 28 | exclude: path.join(__dirname, 'node_modules') // exclude: /node_modules/ 29 | }, 30 | ], 31 | }, 32 | externals: { 33 | crypto: true 34 | } 35 | }; 36 | 37 | module.exports = (env, argv) => { 38 | config.mode = (typeof argv.mode !== 'undefined' && argv.mode === 'production') ? argv.mode : 'development'; 39 | 40 | if (config.mode === 'production') console.log('Webpack for production'); 41 | else{ 42 | console.log('Webpack for development'); 43 | config.devtool = 'inline-source-map'; // add inline source map 44 | } 45 | 46 | return config; 47 | }; 48 | -------------------------------------------------------------------------------- /sample-06-rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /sample-06-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli06" 3 | authors = ["Jun Kurihara"] 4 | description = "Rust version of sample-06" 5 | repository = "https://github.com/junkurihara/lecture-security_engineering" 6 | version = "0.1.0" 7 | edition = "2021" 8 | publish = false 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | anyhow = "1.0.91" 14 | clap = { version = "4.5.20", features = [ 15 | "std", 16 | "cargo", 17 | "wrap_help", 18 | "derive", 19 | ] } 20 | rand = "0.8.5" 21 | rsa = { version = "0.9.6" } 22 | sha2 = "0.10.8" 23 | hex = "0.4.3" 24 | p256 = { version = "0.13.2", features = ["ecdsa"] } 25 | elliptic-curve = { version = "0.13.8", features = ["sec1", "pkcs8", "ecdh"] } 26 | p384 = { version = "0.13.0", features = ["ecdsa"] } 27 | sha3 = "0.10.8" 28 | digest = "0.10.7" 29 | hmac = "0.12.1" 30 | crypto-common = "0.1.6" 31 | typenum = "1.17.0" 32 | ecdsa = { version = "0.16.9", features = ["signing", "verifying", "pkcs8"] } 33 | 34 | [dev-dependencies] 35 | hex-literal = "0.4.1" 36 | base64 = "0.22.1" 37 | -------------------------------------------------------------------------------- /sample-06-rs/README.md: -------------------------------------------------------------------------------- 1 | # sample-06-rs 2 | 3 | Rust implementation of [`sample-06`](../sample-06/), which is fully compatible with the original version. 4 | 5 | ## Build 6 | 7 | ```shell: 8 | $ cargo build --release 9 | ``` 10 | 11 | Then you have an executable binary `./target/release/cli06`. 12 | 13 | ## Usage 14 | 15 | ```shell: 16 | $ ./target/release/cli06 -h 17 | Rust version of sample-06 18 | 19 | Usage: cli06 20 | 21 | Commands: 22 | gen-hash Generate Hash 23 | gen-hex-key Generate hex key for HMAC generation 24 | gen-hmac Generate HMAC (key length must be equal to that of hash.) 25 | verify-hmac Verify HMAC 26 | gen-rsa-key Generate RSA key pair 27 | sign-rsa-pss Sign with RSASSA PSS 28 | verify-rsa-pss Verify with RSASSA PSS 29 | gen-ecc-key Generate ECC key pair 30 | sign-ecdsa Sign with ECDSA 31 | verify-ecdsa Verify with ECDSA 32 | help Print this message or the help of the given subcommand(s) 33 | 34 | Options: 35 | -h, --help Print help 36 | -V, --version Print version 37 | ``` 38 | 39 | ```shell: 40 | $ ../target/debug/cli06 gen-hash -h 41 | Generate Hash 42 | 43 | Usage: cli06 gen-hash [OPTIONS] 44 | 45 | Arguments: 46 | Data string to be hashed 47 | 48 | Options: 49 | -a, --algorithm Name of hash function like 'SHA-256' [default: SHA-256] 50 | -h, --help Print help 51 | ``` 52 | 53 | ```shell: 54 | $ ../target/debug/cli06 gen-hex-key -h 55 | Generate hex key for HMAC generation 56 | 57 | Usage: cli06 gen-hex-key 58 | 59 | Arguments: 60 | key size in bytes 61 | 62 | Options: 63 | -h, --help Print help 64 | ``` 65 | 66 | ```shell: 67 | $ ../target/debug/cli06 gen-hmac -h 68 | Generate HMAC (key length must be equal to that of hash.) 69 | 70 | Usage: cli06 gen-hmac [OPTIONS] --key 71 | 72 | Arguments: 73 | Data string to be keyed-hashed 74 | 75 | Options: 76 | -k, --key Hex key of length equal to the hash size 77 | -a, --algorithm Name of hash function like 'SHA-256' [default: SHA-256] 78 | -h, --help Print help 79 | ``` 80 | 81 | ```shell: 82 | $ ../target/debug/cli06 verify-hmac -h 83 | Verify HMAC 84 | 85 | Usage: cli06 verify-hmac [OPTIONS] --key --mac 86 | 87 | Arguments: 88 | Data string to be keyed-hashed 89 | 90 | Options: 91 | -k, --key Hex key of length equal to the hash size 92 | -m, --mac Hex HMAC 93 | -a, --algorithm Name of hash function like 'SHA-256' [default: SHA-256] 94 | -h, --help Print help 95 | ``` 96 | 97 | ```shell: 98 | $ ../target/debug/cli06 gen-rsa-key -h 99 | Generate RSA key pair 100 | 101 | Usage: cli06 gen-rsa-key [OPTIONS] 102 | 103 | Options: 104 | -b, --bits Modulus length like 2048 [default: 2048] 105 | -h, --help Print help 106 | ``` 107 | 108 | ```shell: 109 | $ ../target/debug/cli06 sign-rsa-pss -h 110 | Sign with RSASSA PSS 111 | 112 | Usage: cli06 sign-rsa-pss --privateKey 113 | 114 | Arguments: 115 | message data to be signed 116 | 117 | Options: 118 | -s, --privateKey hex DER-formatted private key 119 | -h, --help Print help 120 | ``` 121 | 122 | ```shell: 123 | $ ../target/debug/cli06 verify-rsa-pss -h 124 | Verify with RSASSA PSS 125 | 126 | Usage: cli06 verify-rsa-pss --publicKey --signature 127 | 128 | Arguments: 129 | message data 130 | 131 | Options: 132 | -p, --publicKey hex DER-formatted public key 133 | -t, --signature hex signature 134 | -h, --help Print help 135 | ``` 136 | 137 | ```shell: 138 | $ ../target/debug/cli06 gen-ecc-key -h 139 | Generate ECC key pair 140 | 141 | Usage: cli06 gen-ecc-key [CURVE] 142 | 143 | Arguments: 144 | [CURVE] Curve name like P-256 [default: P-256] 145 | 146 | Options: 147 | -h, --help Print help 148 | ``` 149 | 150 | ```shell: 151 | $ ../target/debug/cli06 sign-ecdsa -h 152 | Sign with ECDSA 153 | 154 | Usage: cli06 sign-ecdsa --privateKey 155 | 156 | Arguments: 157 | message data to be signed 158 | 159 | Options: 160 | -s, --privateKey hex DER-formatted private key 161 | -h, --help Print help 162 | ``` 163 | 164 | ```shell: 165 | $ ../target/debug/cli06 verify-ecdsa -h 166 | Verify with ECDSA 167 | 168 | Usage: cli06 verify-ecdsa --publicKey --signature 169 | 170 | Arguments: 171 | message data 172 | 173 | Options: 174 | -p, --publicKey hex DER-formatted public key 175 | -t, --signature hex signature 176 | -h, --help Print help 177 | ``` 178 | -------------------------------------------------------------------------------- /sample-06-rs/src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | 3 | #[derive(Parser, Debug)] 4 | #[command(author, version, about, long_about = None)] 5 | pub struct ClapArgs { 6 | #[clap(subcommand)] 7 | pub subcommand: SubCommands, 8 | } 9 | 10 | #[allow(non_snake_case, non_camel_case_types)] 11 | #[derive(Debug, Subcommand)] 12 | pub enum SubCommands { 13 | /// Generate Hash 14 | Gen_Hash { 15 | /// Name of hash function like 'SHA-256' 16 | #[arg(short, long, default_value = "SHA-256")] 17 | algorithm: String, 18 | 19 | /// Data string to be hashed 20 | data: String, 21 | }, 22 | /// Generate hex key for HMAC generation 23 | Gen_Hex_Key { 24 | /// key size in bytes 25 | len: usize, 26 | }, 27 | /// Generate HMAC (key length must be equal to that of hash.) 28 | Gen_Hmac { 29 | /// Hex key of length equal to the hash size 30 | #[arg(short, long)] 31 | key: String, 32 | 33 | /// Name of hash function like 'SHA-256' 34 | #[arg(short, long, default_value = "SHA-256")] 35 | algorithm: String, 36 | 37 | /// Data string to be keyed-hashed 38 | data: String, 39 | }, 40 | /// Verify HMAC 41 | Verify_Hmac { 42 | /// Hex key of length equal to the hash size 43 | #[arg(short, long)] 44 | key: String, 45 | 46 | /// Hex HMAC 47 | #[arg(short, long)] 48 | mac: String, 49 | 50 | /// Name of hash function like 'SHA-256' 51 | #[arg(short, long, default_value = "SHA-256")] 52 | algorithm: String, 53 | 54 | /// Data string to be keyed-hashed 55 | data: String, 56 | }, 57 | /// Generate RSA key pair 58 | Gen_Rsa_key { 59 | /// Modulus length like 2048 60 | #[arg(short, long, default_value = "2048")] 61 | bits: usize, 62 | }, 63 | /// Sign with RSASSA PSS 64 | Sign_Rsa_Pss { 65 | /// hex DER-formatted private key 66 | #[arg(short = 's', long = "privateKey")] 67 | private_key: String, 68 | 69 | /// message data to be signed 70 | data: String, 71 | }, 72 | /// Verify with RSASSA PSS 73 | Verify_Rsa_Pss { 74 | /// hex DER-formatted public key 75 | #[arg(short, long = "publicKey")] 76 | public_key: String, 77 | 78 | /// hex signature 79 | #[arg(short = 't', long)] 80 | signature: String, 81 | 82 | /// message data 83 | data: String, 84 | }, 85 | /// Generate ECC key pair 86 | Gen_Ecc_key { 87 | /// Curve name like P-256 88 | #[arg(default_value = "P-256")] 89 | curve: String, 90 | }, 91 | /// Sign with ECDSA 92 | Sign_Ecdsa { 93 | /// hex DER-formatted private key 94 | #[arg(short = 's', long = "privateKey")] 95 | private_key: String, 96 | 97 | /// message data to be signed 98 | data: String, 99 | }, 100 | /// Verify with ECDSA 101 | Verify_Ecdsa { 102 | /// hex DER-formatted public key 103 | #[arg(short, long = "publicKey")] 104 | public_key: String, 105 | 106 | /// hex signature 107 | #[arg(short = 't', long)] 108 | signature: String, 109 | 110 | /// message data 111 | data: String, 112 | }, 113 | } 114 | -------------------------------------------------------------------------------- /sample-06-rs/src/error.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub use anyhow::{anyhow, bail, ensure, Context, Result}; 3 | -------------------------------------------------------------------------------- /sample-06-rs/src/hash.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crypto_common::{typenum::IsLess, BlockSizeUser}; 3 | use digest::{ 4 | block_buffer::Eager, 5 | core_api::{BufferKindUser, CoreProxy, FixedOutputCore, UpdateCore}, 6 | Digest, HashMarker, Mac, 7 | }; 8 | use hmac::Hmac; 9 | use typenum::{Le, NonZero, U256}; 10 | 11 | pub fn generate_hash(data: &[u8]) -> Vec 12 | where 13 | D: Digest, 14 | { 15 | let mut hasher = D::new(); 16 | hasher.update(data); 17 | hasher.finalize().to_vec() 18 | } 19 | 20 | pub fn generate_hmac(data: &[u8], key: &[u8]) -> Result> 21 | where 22 | D: CoreProxy, 23 | D::Core: HashMarker + UpdateCore + FixedOutputCore + BufferKindUser + Default + Clone, 24 | ::BlockSize: IsLess, 25 | Le<::BlockSize, U256>: NonZero, 26 | { 27 | let mut mac = Hmac::::new_from_slice(key)?; 28 | mac.update(data); 29 | 30 | // `result` has type `CtOutput` which is a thin wrapper around array of 31 | // bytes for providing constant time equality check 32 | Ok(mac.finalize().into_bytes().to_vec()) 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use hex_literal::hex; 39 | use sha2::{Sha256, Sha384, Sha512}; 40 | use sha3::{Sha3_256, Sha3_384, Sha3_512}; 41 | 42 | #[test] 43 | fn test_generate_hash() { 44 | let data = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq".as_bytes(); 45 | 46 | assert_eq!( 47 | generate_hash::(data), 48 | hex!("248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1") 49 | ); 50 | assert_eq!( 51 | generate_hash::(data), 52 | hex!("3391fdddfc8dc7393707a65b1b4709397cf8b1d162af05abfe8f450de5f36bc6b0455a8520bc4e6f5fe95b1fe3c8452b") 53 | ); 54 | assert_eq!( 55 | generate_hash::(data), 56 | hex!("204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445") 57 | ); 58 | assert_eq!( 59 | generate_hash::(data), 60 | hex!("41c0dba2a9d6240849100376a8235e2c82e1b9998a999e21db32dd97496d3376") 61 | ); 62 | assert_eq!( 63 | generate_hash::(data), 64 | hex!("991c665755eb3a4b6bbdfb75c78a492e8c56a22c5c4d7e429bfdbc32b9d4ad5aa04a1f076e62fea19eef51acd0657c22") 65 | ); 66 | assert_eq!( 67 | generate_hash::(data), 68 | hex!("04a371e84ecfb5b8b77cb48610fca8182dd457ce6f326a0fd3d7ec2f1e91636dee691fbe0c985302ba1b0d8dc78c086346b533b49c030d99a27daf1139d6e75e") 69 | ); 70 | } 71 | 72 | #[test] 73 | fn test_generate_hmac() { 74 | let key = "luchse sind halt tolle katzen".as_bytes(); 75 | let data = "luchse luchsen luchsig in luxemburg umher".as_bytes(); 76 | 77 | assert_eq!( 78 | generate_hmac::(data, key).unwrap(), 79 | hex!("2ce4f6d7e9ac3abc656a8db6ed66df72d6beed9b310f6fc2cffe57db7631c88f") 80 | ); 81 | assert_eq!( 82 | generate_hmac::(data, key).unwrap(), 83 | hex!("9fe725ff6a9b0f898028cc5232e35b0370974087fcef3e3c733721bf2d0eb7f99b12437458c6b5a77af74db886c744ab") 84 | ); 85 | assert_eq!(generate_hmac::(data, key).unwrap(), hex!("dfaccb94cb57c9c48a22b7a72931e581ba9ef0c3b9fad37abe80a3091ea8d9bf0b37236e6be9e53ef27ad57f10c335d28e3ffdcfb92fd23a7f5e409993b97887")); 86 | assert_eq!( 87 | generate_hmac::(data, key).unwrap(), 88 | hex!("3f8c691e77be447d4ecdcf0d61f28b9c8c0067f6fdd822464b9da369f3c2852b") 89 | ); 90 | assert_eq!( 91 | generate_hmac::(data, key).unwrap(), 92 | hex!("152d19cf3538989b1cd1685d94c6f4705fa975c20d2cefca541291c5a401fb5cf977640aa421b92621f53664789355a7") 93 | ); 94 | assert_eq!(generate_hmac::(data, key).unwrap(), hex!("6379a3fdebee97d298ba4a1ac63379e81e90b70277ec2770c48f841777789bee5c1f49c33812af4ac5d478413e5c0ffe89dabbea5f46c9f3acdb8952992b9202")); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /sample-06-rs/src/rsa.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use rsa::{ 3 | pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}, 4 | pss::{BlindedSigningKey, Signature, VerifyingKey}, 5 | signature::{RandomizedSigner, SignatureEncoding, Verifier}, 6 | Oaep, RsaPrivateKey, RsaPublicKey, 7 | }; 8 | use sha2::Sha256; 9 | 10 | #[derive(Debug)] 11 | pub struct RsaKeyPair { 12 | pub public: RsaPublicKey, 13 | pub private: Option, 14 | } 15 | 16 | #[allow(dead_code)] 17 | impl RsaKeyPair { 18 | pub fn new(bits: &usize) -> Result { 19 | let mut rng = rand::thread_rng(); 20 | let private_key = RsaPrivateKey::new(&mut rng, *bits)?; 21 | let public_key = private_key.to_public_key(); 22 | 23 | Ok(Self { 24 | public: public_key, 25 | private: Some(private_key), 26 | }) 27 | } 28 | 29 | pub fn to_spki_public_der(&self) -> Result> { 30 | let der = self.public.to_public_key_der()?.as_bytes().to_vec(); 31 | Ok(der) 32 | } 33 | pub fn to_pkcs8_private_der(&self) -> Result> { 34 | if self.private.is_none() { 35 | bail!("No private key"); 36 | } 37 | let der = self.private.as_ref().unwrap().to_pkcs8_der()?.as_bytes().to_vec(); 38 | Ok(der) 39 | } 40 | 41 | pub fn from_spki_public_der(der: &[u8]) -> Result { 42 | let public_key = RsaPublicKey::from_public_key_der(der)?; 43 | Ok(RsaKeyPair { 44 | public: public_key, 45 | private: None, 46 | }) 47 | } 48 | 49 | pub fn from_pkcs8_private_der(der: &[u8]) -> Result { 50 | let private_key = RsaPrivateKey::from_pkcs8_der(der)?; 51 | let public_key = private_key.to_public_key(); 52 | Ok(RsaKeyPair { 53 | public: public_key, 54 | private: Some(private_key), 55 | }) 56 | } 57 | 58 | pub fn oaep_encrypt(&self, data: &[u8]) -> Result> { 59 | let mut rng = rand::thread_rng(); 60 | let padding = Oaep::new::(); 61 | let enc_data = self.public.encrypt(&mut rng, padding, data)?; 62 | Ok(enc_data) 63 | } 64 | 65 | pub fn oaep_decrypt(&self, data: &[u8]) -> Result> { 66 | if self.private.is_none() { 67 | bail!("No private key"); 68 | } 69 | let padding = Oaep::new::(); 70 | let dec_data = self.private.as_ref().unwrap().decrypt(padding, data)?; 71 | Ok(dec_data) 72 | } 73 | 74 | pub fn pss_sign(&self, data: &[u8]) -> Result> { 75 | if self.private.is_none() { 76 | bail!("No private key"); 77 | } 78 | let mut rng = rand::thread_rng(); 79 | let signing_key: BlindedSigningKey<_> = BlindedSigningKey::::new(self.private.as_ref().unwrap().to_owned()); 80 | Ok(signing_key.sign_with_rng(&mut rng, data).to_bytes().to_vec()) 81 | } 82 | 83 | pub fn pss_verify(&self, data: &[u8], signature: &[u8]) -> Result<()> { 84 | let verifying_key: VerifyingKey<_> = VerifyingKey::::new(self.public.to_owned()); 85 | let signature = Signature::try_from(signature)?; 86 | verifying_key.verify(data, &signature).map_err(|e| anyhow!(e)) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use crate::util::*; 94 | 95 | #[test] 96 | fn test_keygen() { 97 | let keypair = RsaKeyPair::new(&2048).unwrap(); 98 | 99 | let public_der = keypair.to_spki_public_der().unwrap(); 100 | let private_der = keypair.to_pkcs8_private_der().unwrap(); 101 | 102 | let keypair_from_public_der = RsaKeyPair::from_spki_public_der(&public_der).unwrap(); 103 | let keypair_from_private_der = RsaKeyPair::from_pkcs8_private_der(&private_der).unwrap(); 104 | 105 | let public_der2 = keypair_from_public_der.to_spki_public_der().unwrap(); 106 | let public_der3 = keypair_from_private_der.to_spki_public_der().unwrap(); 107 | assert_eq!(public_der, public_der2); 108 | assert_eq!(public_der, public_der3); 109 | 110 | let private_der2 = keypair_from_private_der.to_pkcs8_private_der().unwrap(); 111 | assert_eq!(private_der, private_der2); 112 | } 113 | 114 | #[test] 115 | fn test_keygen_sign_verify() { 116 | let keypair = RsaKeyPair::new(&2048).unwrap(); 117 | 118 | let message = "hello"; 119 | 120 | let signature = keypair.pss_sign(message.as_bytes()).unwrap(); 121 | 122 | let result = keypair.pss_verify(message.as_bytes(), signature.as_slice()); 123 | 124 | assert!(result.is_ok()); 125 | } 126 | 127 | #[test] 128 | fn test_keygen_encrypt_decrypt() { 129 | let keypair = RsaKeyPair::new(&2048).unwrap(); 130 | 131 | let plaintext = "hello"; 132 | 133 | let ciphertext = keypair.oaep_encrypt(plaintext.as_bytes()).unwrap(); 134 | 135 | let plaintext2 = String::from_utf8(keypair.oaep_decrypt(&ciphertext).unwrap()).unwrap(); 136 | 137 | assert_eq!(plaintext.to_string(), plaintext2); 138 | } 139 | 140 | #[test] 141 | fn test_hex() { 142 | let v: [u8; 3] = [0x01, 0x02, 0x03]; 143 | let s = v.to_vec().to_hex_string(); 144 | assert_eq!(s, "010203") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /sample-06-rs/src/util.rs: -------------------------------------------------------------------------------- 1 | pub trait ToHexString { 2 | fn to_hex_string(&self) -> String; 3 | } 4 | 5 | impl ToHexString for Vec { 6 | fn to_hex_string(&self) -> String { 7 | hex::encode(self) 8 | } 9 | } 10 | 11 | impl ToHexString for &[u8] { 12 | fn to_hex_string(&self) -> String { 13 | hex::encode(self) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sample-06/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "@babel/preset-env", { 4 | "targets": { 5 | "browsers": [ 6 | "last 2 chrome versions", 7 | "last 2 firefox versions", 8 | "IE 11", 9 | "last 2 Edge versions" 10 | ] 11 | }, 12 | "useBuiltIns": false 13 | } ] 14 | ], 15 | "ignore": [ "node_modules" ], 16 | "only": [ "src", "test" ], 17 | "plugins": [ 18 | [ 19 | "@babel/plugin-transform-runtime", 20 | { 21 | "@babel/polyfill": false, 22 | "regenerator": true 23 | } 24 | ] 25 | ], 26 | "env": { 27 | "production": { 28 | }, 29 | "development": { 30 | }, 31 | "test": { 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample-06/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "indent": [ 14 | "error", 15 | 2 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "always" 28 | ], 29 | "arrow-body-style": "error", 30 | "arrow-parens": "error", 31 | "arrow-spacing": "error", 32 | "generator-star-spacing": "error", 33 | "no-duplicate-imports": "error", 34 | "no-useless-computed-key": "error", 35 | "no-useless-constructor": "error", 36 | "no-useless-rename": "error", 37 | "no-var": "error", 38 | "object-shorthand": "error", 39 | "prefer-arrow-callback": "error", 40 | "prefer-const": "error", 41 | "prefer-rest-params": "error", 42 | "prefer-spread": "error", 43 | "prefer-template": "error", 44 | "rest-spread-spacing": "error", 45 | "template-curly-spacing": "error", 46 | "yield-star-spacing": "error" 47 | }, 48 | "globals": { 49 | "window": false, 50 | "_window": false 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sample-06/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | obsolete/ 4 | thumbs.db 5 | yarn-error.log 6 | .idea/ 7 | .vscode/ 8 | .coverage/ 9 | .nyc_output/ 10 | dist/ 11 | coverage/ 12 | data/db.json 13 | -------------------------------------------------------------------------------- /sample-06/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jun Kurihara 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sample-06/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e-security-class-sample", 3 | "version": "0.1.0", 4 | "description": "sample for the class", 5 | "main": "dist/index.js", 6 | "private": true, 7 | "scripts": { 8 | "preinstall": "npx only-allow pnpm", 9 | "execute": "node -r @babel/register src/commands-node.js", 10 | "build": "./node_modules/.bin/webpack --mode development --config webpack.config.js", 11 | "cleanup": "rm -rf ./dist coverage .nyc_output; rm -rf ./test/html/*.bundle.js; rm -rf ./test/html/test.html; rm -rf ./node_modules" 12 | }, 13 | "author": "Jun Kurihara", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@babel/cli": "7.26.4", 17 | "@babel/core": "7.26.0", 18 | "@babel/plugin-transform-regenerator": "7.25.9", 19 | "@babel/plugin-transform-runtime": "7.25.9", 20 | "@babel/preset-env": "7.26.0", 21 | "@babel/register": "7.25.9", 22 | "@babel/eslint-parser": "7.25.9", 23 | "babel-loader": "9.2.1", 24 | "cross-env": "7.0.3", 25 | "eslint": "9.17.0", 26 | "webpack": "5.97.1", 27 | "webpack-cli": "6.0.1" 28 | }, 29 | "dependencies": { 30 | "@babel/runtime": "~7.26.0", 31 | "commander": "~12.1.0", 32 | "js-crypto-ec": "1.0.7", 33 | "js-crypto-utils": "1.0.7", 34 | "js-encoding-utils": "0.7.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sample-06/src/commands-browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | E2E Encryption Test 6 | 7 | 8 | 9 | 10 | 11 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /sample-06/src/commands-node.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { 4 | genHash, 5 | genHmac, 6 | verifyHmac, 7 | genRsaKey, 8 | signRsaPss, 9 | verifyRsaPss, 10 | genEccKey, 11 | signEcdsa, 12 | verifyEcdsa, 13 | } from "./test-apis"; 14 | import { Command } from "commander"; 15 | import jseu from "js-encoding-utils"; 16 | import { getJscu } from "./util/env"; 17 | 18 | const pgm = new Command(); 19 | pgm.version("0.0.1"); 20 | 21 | pgm 22 | .command("gen-hash ", "") 23 | .description("Generate hash") 24 | .option("-h, --hash ", "Name of hash function like 'SHA-256'", "SHA-256") 25 | .action(async (data, options) => { 26 | // get Hash 27 | const hashedData = await genHash(data, options.hash); 28 | console.log(`\n${jseu.encoder.arrayBufferToHexString(hashedData)}\n=======\n`); 29 | }); 30 | 31 | pgm 32 | .command("gen-hex-key ", "") 33 | .description("Generate hex key for HMAC generation") 34 | .action((len) => { 35 | const jscu = getJscu(); 36 | const key = jscu.random.getRandomBytes(parseInt(len)); 37 | console.log(`\n${jseu.encoder.arrayBufferToHexString(key)}\n=======\n`); 38 | }); 39 | 40 | pgm 41 | .command("gen-hmac ", "") 42 | .description("Generate HMAC (key length must be equal to that of hash.)") 43 | .option("-k, --key ", "Hex key of length equal to the hash size") 44 | .option("-h, --hash ", "Name of hash function like 'SHA-256'", "SHA-256") 45 | .action(async (data, options) => { 46 | // get hmac 47 | const hashedData = await genHmac(data, options.key, options.hash); 48 | console.log(`\n${jseu.encoder.arrayBufferToHexString(hashedData)}\n=======\n`); 49 | }); 50 | 51 | pgm 52 | .command("verify-hmac ", "") 53 | .description("Verify HMAC") 54 | .option("-k, --key ", "Hex key of length equal to the hash size") 55 | .option("-m, --mac ", "Hex HMAC") 56 | .option("-h, --hash ", "Name of hash function like 'SHA-256'", "SHA-256") 57 | .action(async (data, options) => { 58 | // verify 59 | const result = await verifyHmac(data, options.key, options.mac, options.hash); 60 | console.log(`\n${result}\n=======\n`); 61 | }); 62 | 63 | pgm 64 | .command("gen-rsa-key", "") 65 | .description("Generate RSA key pair") 66 | .option("-b, --bits ", "Key length in bits", "2048") 67 | .action(async (options) => { 68 | // verify 69 | const result = await genRsaKey(parseInt(options.bits)); 70 | console.log(`\n${result.publicKey}\n`); 71 | console.log(`\n${result.privateKey}\n=======\n`); 72 | }); 73 | 74 | pgm 75 | .command("sign-rsa-pss ", "") 76 | .description("Sign with RSASSA PSS") 77 | .option("-h, --hash ", "Name of hash function like 'SHA-256'", "SHA-256") 78 | .option("-s, --privateKey ", "Private key in Hex") 79 | .action(async (data, options) => { 80 | const sig = await signRsaPss(data, options.privateKey, options.hash, 32); 81 | console.log(`\n${jseu.encoder.arrayBufferToHexString(sig)}\n=======\n`); 82 | }); 83 | 84 | pgm 85 | .command("verify-rsa-pss ", "") 86 | .description("Verify with RSASSA PSS") 87 | .option("-h, --hash ", "Name of hash function like 'SHA-256'", "SHA-256") 88 | .option("-t, --signature ", "Signature in Hex") 89 | .option("-p, --publicKey ", "Public key in Hex") 90 | .action(async (data, options) => { 91 | const result = await verifyRsaPss(data, options.signature, options.publicKey, options.hash, 32); 92 | console.log(`\n${result}\n=======\n`); 93 | }); 94 | 95 | pgm 96 | .command("gen-ecc-key", "") 97 | .description("Generate ECC key pair") 98 | .option("-c, --curve ", "Curve name like 'P-256'", "P-256") 99 | .action(async (options) => { 100 | // verify 101 | const result = await genEccKey(options.curve); 102 | console.log(`\n${result.publicKey}\n`); 103 | console.log(`\n${result.privateKey}\n=======\n`); 104 | }); 105 | 106 | pgm 107 | .command("sign-ecdsa ", "") 108 | .description("Sign with ECDSA") 109 | .option("-h, --hash ", "Name of hash function like 'SHA-256'", "SHA-256") 110 | .option("-s, --privateKey ", "Private key in Hex") 111 | .action(async (data, options) => { 112 | const sig = await signEcdsa(data, options.privateKey, options.hash); 113 | console.log(`\n${jseu.encoder.arrayBufferToHexString(sig)}\n=======\n`); 114 | }); 115 | 116 | pgm 117 | .command("verify-ecdsa ", "") 118 | .description("Verify with ECDSA") 119 | .option("-h, --hash ", "Name of hash function like 'SHA-256'", "SHA-256") 120 | .option("-t, --signature ", "Signature in Hex") 121 | .option("-p, --publicKey ", "Public key in Hex") 122 | .action(async (data, options) => { 123 | const result = await verifyEcdsa(data, options.signature, options.publicKey, options.hash); 124 | console.log(`\n${result}\n=======\n`); 125 | }); 126 | 127 | pgm.parse(process.argv); 128 | -------------------------------------------------------------------------------- /sample-06/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * index.js 3 | */ 4 | import {genHash, genHmac, verifyHmac, genRsaKey, signRsaPss, verifyRsaPss, genEccKey, signEcdsa, verifyEcdsa} from './test-apis'; 5 | import * as util from './util/format'; 6 | 7 | export {util, genHash, genHmac, verifyHmac, genRsaKey, signRsaPss, verifyRsaPss, genEccKey, signEcdsa, verifyEcdsa}; 8 | export default {util, genHash, genHmac, verifyHmac, genRsaKey, signRsaPss, verifyRsaPss, genEccKey, signEcdsa, verifyEcdsa}; 9 | -------------------------------------------------------------------------------- /sample-06/src/test-apis.js: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | 3 | import {getJscu} from './util/env'; 4 | 5 | /** 6 | * Get Hash 7 | * @param data {string} 8 | * @param hash {'SHA-256'|'SHA-384'|'SHA-512'|'SHA3-256'|'SHA3-384'|'SHA3-512'} 9 | * @return {Promise} 10 | */ 11 | export const genHash = (data, hash = 'SHA-256') => { 12 | const jscu = getJscu(); 13 | const binary = jseu.encoder.stringToArrayBuffer(data); 14 | 15 | return jscu.hash.compute(binary, hash); 16 | }; 17 | 18 | 19 | export const genHmac = (data, key, hash = 'SHA-256') => { 20 | const jscu = getJscu(); 21 | const binaryData = jseu.encoder.stringToArrayBuffer(data); 22 | const binaryKey = jseu.encoder.hexStringToArrayBuffer(key); 23 | 24 | return jscu.hmac.compute(binaryKey, binaryData, hash); 25 | }; 26 | 27 | export const verifyHmac = (data, key, mac, hash = 'SHA-256') => { 28 | const jscu = getJscu(); 29 | const binaryData = jseu.encoder.stringToArrayBuffer(data); 30 | const binaryKey = jseu.encoder.hexStringToArrayBuffer(key); 31 | const binaryMac = jseu.encoder.hexStringToArrayBuffer(mac); 32 | 33 | return jscu.hmac.verify(binaryKey, binaryData, binaryMac, hash); 34 | }; 35 | 36 | export const genRsaKey = async (bits = 2048) => { 37 | const jscu = getJscu(); 38 | const kp = await jscu.pkc.generateKey('RSA', {modulusLength: bits}); 39 | const publicKey = jseu.encoder.arrayBufferToHexString(await kp.publicKey.export('der')); 40 | const privateKey = jseu.encoder.arrayBufferToHexString(await kp.privateKey.export('der')); 41 | 42 | return {publicKey, privateKey}; 43 | }; 44 | 45 | export const signRsaPss = async(data, privateKeyHex, hash = 'SHA-256', saltLength = 32) => { 46 | const jscu = getJscu(); 47 | const binaryData = jseu.encoder.stringToArrayBuffer(data); 48 | const privateKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(privateKeyHex)); 49 | 50 | return jscu.pkc.sign(binaryData, privateKey, hash, {name: 'RSA-PSS', saltLength}); 51 | }; 52 | 53 | 54 | export const verifyRsaPss = (data, signatureHex, publicKeyHex, hash = 'SHA-256', saltLength = 32) => { 55 | const jscu = getJscu(); 56 | const binaryData = jseu.encoder.stringToArrayBuffer(data); 57 | const publicKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(publicKeyHex)); 58 | const signature = jseu.encoder.hexStringToArrayBuffer(signatureHex); 59 | 60 | return jscu.pkc.verify(binaryData, signature, publicKey, hash, {name: 'RSA-PSS', saltLength}); 61 | }; 62 | 63 | export const genEccKey = async (curve = 'P-256') => { 64 | const jscu = getJscu(); 65 | const kp = await jscu.pkc.generateKey('EC', {namedCurve: curve}); 66 | const publicKey = jseu.encoder.arrayBufferToHexString(await kp.publicKey.export('der')); 67 | const privateKey = jseu.encoder.arrayBufferToHexString(await kp.privateKey.export('der')); 68 | 69 | return {publicKey, privateKey}; 70 | }; 71 | 72 | export const signEcdsa = async (data, privateKeyHex, hash = 'SHA-256') => { 73 | const jscu = getJscu(); 74 | const binaryData = jseu.encoder.stringToArrayBuffer(data); 75 | const privateKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(privateKeyHex)); 76 | 77 | return jscu.pkc.sign(binaryData, privateKey, hash); 78 | }; 79 | 80 | export const verifyEcdsa = async(data, signatureHex, publicKeyHex, hash = 'SHA-256') => { 81 | const jscu = getJscu(); 82 | const binaryData = jseu.encoder.stringToArrayBuffer(data); 83 | const publicKey = new jscu.Key('der', jseu.encoder.hexStringToArrayBuffer(publicKeyHex)); 84 | const signature = jseu.encoder.hexStringToArrayBuffer(signatureHex); 85 | 86 | return jscu.pkc.verify(binaryData, signature, publicKey, hash); 87 | }; 88 | -------------------------------------------------------------------------------- /sample-06/src/util/env.js: -------------------------------------------------------------------------------- 1 | import jscu from 'js-crypto-utils'; 2 | 3 | /** 4 | * Get jscu 5 | */ 6 | export const getJscu = () => { 7 | const global = Function('return this;')(); 8 | if (typeof window !== 'undefined'){ 9 | return window.jscu; 10 | } 11 | else{ 12 | global.jscu = jscu; 13 | return jscu; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /sample-06/src/util/format.js: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | 3 | export const arrayBufferToHexString = (d) => jseu.encoder.arrayBufferToHexString(d); 4 | -------------------------------------------------------------------------------- /sample-06/webpack.baseconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "libName": "e2eTest" 3 | } 4 | -------------------------------------------------------------------------------- /sample-06/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const webpack = require('webpack'); 3 | const base = require('./webpack.baseconfig.json'); 4 | 5 | const config = { 6 | entry: ['./src/index.js'], 7 | 8 | output: { 9 | filename: `${base.libName}.bundle.js`, 10 | chunkFilename: '[name].js', 11 | path: path.resolve(__dirname, './dist'), 12 | publicPath: path.resolve(__dirname, './dist'), 13 | library: base.libName, 14 | libraryTarget: 'umd', 15 | globalObject: 'this' // for node js import 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx', '.mjs'], 19 | modules: ['node_modules'] 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(m|)js$/, 25 | use: [{ 26 | loader: 'babel-loader' 27 | }], 28 | exclude: path.join(__dirname, 'node_modules') // exclude: /node_modules/ 29 | }, 30 | ], 31 | }, 32 | externals: { 33 | crypto: true 34 | } 35 | }; 36 | 37 | module.exports = (env, argv) => { 38 | config.mode = (typeof argv.mode !== 'undefined' && argv.mode === 'production') ? argv.mode : 'development'; 39 | 40 | if (config.mode === 'production') console.log('Webpack for production'); 41 | else{ 42 | console.log('Webpack for development'); 43 | config.devtool = 'inline-source-map'; // add inline source map 44 | } 45 | 46 | return config; 47 | }; 48 | -------------------------------------------------------------------------------- /sample-07-09-rs/.dockerignore: -------------------------------------------------------------------------------- 1 | README.md 2 | target/ 3 | -------------------------------------------------------------------------------- /sample-07-09-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webauthn_sample" 3 | authors = ["Jun Kurihara"] 4 | description = "Rust version of sample-07-09-rs" 5 | repository = "https://github.com/junkurihara/lecture-security_engineering" 6 | version = "0.1.0" 7 | edition = "2021" 8 | publish = false 9 | 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | axum = { version = "0.8.1" } 15 | anyhow = "1.0.91" 16 | clap = { version = "4.5.20", features = [ 17 | "std", 18 | "cargo", 19 | "wrap_help", 20 | "derive", 21 | ] } 22 | rand = "0.8.5" 23 | rustc-hash = "2.0.0" 24 | serde = { version = "1.0.213", features = ["derive"] } 25 | tokio = { version = "1.41.0", features = ["full"] } 26 | tower = { version = "0.5.1", features = ["util", "timeout"] } 27 | tower-http = { version = "0.6.1", features = ["trace", "fs"] } 28 | tower-sessions = "0.14.0" 29 | tracing = "0.1.40" 30 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 31 | url = "2.5.2" 32 | uuid = { version = "1.11.0", features = ["v4"] } 33 | webauthn-rs = { version = "0.5.0", features = [ 34 | "resident-key-support", 35 | "danger-allow-state-serialisation", 36 | "preview-features", 37 | ] } 38 | axum-macros = "0.5.0" 39 | -------------------------------------------------------------------------------- /sample-07-09-rs/README.md: -------------------------------------------------------------------------------- 1 | # A sample server/client of FIDO2 WebAuthn/Passkeys 2 | 3 | **NOTE: This example is not capable with untrusted CA-certified passkeys**. We think only Yubico keys are available with this example. 4 | 5 | ## How to run 6 | 7 | You need Rust/Cargo environment in addition to Node.js v20+ at first. You can serve a sample WebAuthn registration/authentication server listening on `127.0.0.1:8080` as follows. Note that `start.sh` builds our TypeScript library located in `../sample-07-09/` to call from the frontend along with the rust server. 8 | 9 | ```shell: 10 | % cd sample-07-09-rs 11 | % bash ./start.sh 12 | ``` 13 | 14 | ## Usage of Rust-based server 15 | 16 | ```shell: 17 | % ../target/release/webauthn_sample --help 18 | Rust version of sample-07-09-rs 19 | 20 | Usage: webauthn_sample [OPTIONS] 21 | 22 | Options: 23 | -l, --listen-addr Listen socket [default: 127.0.0.1:8080] 24 | -a, --asset-dir Asset directory [default: ./assets] 25 | -h, --help Print help 26 | -V, --version Print version 27 | ``` 28 | -------------------------------------------------------------------------------- /sample-07-09-rs/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebAuthn-rs Tutorial 7 | 12 | 13 | 14 | 15 | 16 |

Welcome to the WebAuthn Server!

17 | 18 |
19 | 20 | 21 | 22 |
23 | 24 |
25 |

26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sample-07-09-rs/docker/.env.example: -------------------------------------------------------------------------------- 1 | ## All values are optional 2 | # Listen socket 3 | LISTEN_ADDRESS="0.0.0.0:8080" 4 | 5 | # RP ID 6 | RP_ID="webauthn.secarchlab.net" 7 | 8 | # RP origin which must be a valid URL 9 | RP_ORIGIN="https://webauthn.secarchlab.net/" 10 | 11 | # RP name 12 | RP_NAME="WebAuthnExample" 13 | 14 | # Cookie name 15 | COOKIE_NAME="webauthn" 16 | -------------------------------------------------------------------------------- /sample-07-09-rs/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 AS base 2 | 3 | SHELL ["/bin/sh", "-x", "-c"] 4 | ENV SERIAL 2 5 | 6 | ######################################## 7 | FROM base as builder 8 | 9 | ENV CFLAGS=-Ofast 10 | ENV BUILD_DEPS curl make ca-certificates build-essential pkg-config libssl-dev 11 | ENV NODE_MAJOR 20 12 | 13 | WORKDIR /tmp 14 | 15 | COPY . /tmp/ 16 | 17 | ENV RUSTFLAGS "-C link-arg=-s" 18 | 19 | RUN update-ca-certificates 2> /dev/null || true 20 | 21 | RUN apt-get update && apt-get install -qy --no-install-recommends $BUILD_DEPS && \ 22 | curl -sSf https://sh.rustup.rs | bash -s -- -y --default-toolchain stable && \ 23 | export PATH="$HOME/.cargo/bin:$PATH" && \ 24 | echo "Building WebAuthn Sample from source" && \ 25 | cargo build --release --no-default-features && \ 26 | strip --strip-all /tmp/target/release/webauthn_sample 27 | 28 | ######################################## 29 | FROM base AS runner 30 | LABEL maintainer="Jun Kurihara" 31 | 32 | ENV RUNTIME_DEPS logrotate ca-certificates gosu 33 | 34 | RUN apt-get update && \ 35 | apt-get install -qy --no-install-recommends $RUNTIME_DEPS && \ 36 | apt-get -qy clean && \ 37 | rm -fr /tmp/* /var/tmp/* /var/cache/apt/* /var/lib/apt/lists/* /var/log/apt/* /var/log/*.log &&\ 38 | find / -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; && \ 39 | find / -type d -path /proc -prune -o -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \; && \ 40 | mkdir -p /webauthn/bin &&\ 41 | mkdir -p /webauthn/log 42 | 43 | COPY --from=builder /tmp/target/release/webauthn_sample /webauthn/bin/webauthn_sample 44 | COPY --from=builder /tmp/assets /webauthn/assets 45 | COPY ./docker/run.sh /webauthn 46 | COPY ./docker/entrypoint.sh /webauthn 47 | 48 | RUN chmod +x /webauthn/run.sh && \ 49 | chmod +x /webauthn/entrypoint.sh 50 | 51 | EXPOSE 53/udp 53/tcp 52 | 53 | CMD ["/usr/bin/bash", "/webauthn/entrypoint.sh"] 54 | 55 | ENTRYPOINT ["/usr/bin/bash", "/webauthn/entrypoint.sh"] 56 | -------------------------------------------------------------------------------- /sample-07-09-rs/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | webauthn: 4 | image: jqtype/webauthn-sample:latest 5 | container_name: webauthn-sample 6 | ## Uncomment if you build by yourself 7 | build: 8 | context: ../ 9 | dockerfile: ./docker/Dockerfile 10 | init: true 11 | restart: unless-stopped 12 | ports: 13 | - 8080:8080 14 | logging: 15 | options: 16 | max-size: "10m" 17 | max-file: "3" 18 | env_file: .env 19 | environment: 20 | - LOG_LEVEL=debug # debug|info|warn|error 21 | - LOG_TO_FILE=true 22 | - HOST_USER=jun 23 | - HOST_UID=501 24 | - HOST_GID=501 25 | volumes: 26 | # Log Directory for the case of LOG_TO_FILE=true 27 | - ./log:/webauthn/log 28 | -------------------------------------------------------------------------------- /sample-07-09-rs/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | LOG_DIR=/webauthn/log 3 | LOG_FILE=${LOG_DIR}/webauthn.log 4 | LOG_SIZE=10M 5 | LOG_NUM=10 6 | 7 | LOGGING=${LOG_TO_FILE:-false} 8 | USER=${HOST_USER:-webauthn} 9 | USER_ID=${HOST_UID:-900} 10 | GROUP_ID=${HOST_GID:-900} 11 | 12 | ####################################### 13 | # Setup logrotate 14 | function setup_logrotate () { 15 | if [ $LOGROTATE_NUM ]; then 16 | LOG_NUM=${LOGROTATE_NUM} 17 | fi 18 | if [ $LOGROTATE_SIZE ]; then 19 | LOG_SIZE=${LOGROTATE_SIZE} 20 | fi 21 | 22 | cat > /etc/logrotate.conf << EOF 23 | # see "man logrotate" for details 24 | # rotate log files weekly 25 | weekly 26 | # use the adm group by default, since this is the owning group 27 | # of /var/log/syslog. 28 | # su root adm 29 | # keep 4 weeks worth of backlogs 30 | rotate 4 31 | # create new (empty) log files after rotating old ones 32 | create 33 | # use date as a suffix of the rotated file 34 | #dateext 35 | # uncomment this if you want your log files compressed 36 | #compress 37 | # packages drop log rotation information into this directory 38 | include /etc/logrotate.d 39 | # system-specific logs may be also be configured here. 40 | EOF 41 | 42 | cat > /etc/logrotate.d/webauthn.conf << EOF 43 | ${LOG_FILE} { 44 | dateext 45 | daily 46 | missingok 47 | rotate ${LOG_NUM} 48 | notifempty 49 | compress 50 | delaycompress 51 | dateformat -%Y-%m-%d-%s 52 | size ${LOG_SIZE} 53 | copytruncate 54 | su ${USER} ${USER} 55 | } 56 | EOF 57 | } 58 | 59 | ####################################### 60 | function setup_ubuntu () { 61 | # Check the existence of the user, if not exist, create it. 62 | if [ ! $(id ${USER}) ]; then 63 | echo "webauthn: Create user ${USER} with ${USER_ID}:${GROUP_ID}" 64 | groupadd -g ${GROUP_ID} ${USER} 65 | useradd -u ${USER_ID} -g ${GROUP_ID} ${USER} 66 | fi 67 | 68 | # for crontab when logging 69 | if "${LOGGING}"; then 70 | # Set up logrotate 71 | setup_logrotate 72 | 73 | # Setup cron 74 | mkdir -p /etc/cron.15min/ 75 | cp -p /etc/cron.daily/logrotate /etc/cron.15min/ 76 | echo "*/15 * * * * root cd / && run-parts --report /etc/cron.15min" >> /etc/crontab 77 | service cron start 78 | fi 79 | } 80 | 81 | ####################################### 82 | 83 | if [ $(whoami) != "root" -o $(id -u) -ne 0 -a $(id -g) -ne 0 ]; then 84 | echo "Do not execute 'docker run' or 'docker-compose up' with a specific user through '-u'." 85 | echo "If you want to run 'webauthn-sample' with a specific user, use HOST_USER, HOST_UID and HOST_GID environment variables." 86 | exit 1 87 | fi 88 | 89 | # set up user and cron for ubuntu base image 90 | setup_ubuntu 91 | 92 | # Check the given user and its uid:gid 93 | if [ $(id -u ${USER}) -ne ${USER_ID} -a $(id -g ${USER}) -ne ${GROUP_ID} ]; then 94 | echo "${USER} exists or was previously created. However, its uid and gid are inconsistent. Please recreate your container." 95 | exit 1 96 | fi 97 | 98 | # Change permission according to the given user 99 | chown -R ${USER_ID}:${USER_ID} /webauthn 100 | 101 | # run webauthn sample server 102 | echo "Start with user: ${USER} (${USER_ID}:${GROUP_ID})" 103 | if "${LOGGING}"; then 104 | echo "Start with writing log file" 105 | gosu ${USER} sh -c "/webauthn/run.sh 2>&1 | tee ${LOG_FILE}" 106 | else 107 | echo "Start without writing log file" 108 | gosu ${USER} sh -c "/webauthn/run.sh 2>&1" 109 | fi 110 | -------------------------------------------------------------------------------- /sample-07-09-rs/docker/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | DEFAULT_LOG_LEVEL="info" 3 | 4 | CONFIG_STRING="--asset-dir=/webauthn/assets " 5 | 6 | if [ -n "${LISTEN_ADDRESS}" ]; then 7 | CONFIG_STRING="${CONFIG_STRING} --listen-address=${LISTEN_ADDRESS}" 8 | fi 9 | 10 | if [ -n "${RP_ID}" ]; then 11 | CONFIG_STRING="${CONFIG_STRING} --rp-id=${RP_ID}" 12 | fi 13 | 14 | if [ -n "${RP_ORIGIN}" ]; then 15 | CONFIG_STRING="${CONFIG_STRING} --rp-origin=${RP_ORIGIN}" 16 | fi 17 | 18 | if [ -n "${RP_NAME}" ]; then 19 | CONFIG_STRING="${CONFIG_STRING} --rp-name=${RP_NAME}" 20 | fi 21 | 22 | if [ -n "${COOKIE_NAME}" ]; then 23 | CONFIG_STRING="${CONFIG_STRING} --cookie-name=${COOKIE_NAME}" 24 | fi 25 | 26 | 27 | ########################## 28 | # start 29 | echo "Start with logg level ${LOG_LEVEL:-${DEFAULT_LOG_LEVEL}}" 30 | RUST_LOG=${LOG_LEVEL:-${DEFAULT_LOG_LEVEL}} /webauthn/bin/webauthn_sample ${CONFIG_STRING} 31 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "mocha": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "plugins": [ 13 | "@typescript-eslint" 14 | ], 15 | "parserOptions": { 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 2 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "unix" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "single" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ], 35 | "arrow-body-style": "error", 36 | "arrow-parens": "error", 37 | "arrow-spacing": "error", 38 | "generator-star-spacing": "error", 39 | "no-duplicate-imports": "error", 40 | "no-useless-computed-key": "error", 41 | "no-useless-constructor": "error", 42 | "no-useless-rename": "error", 43 | "no-var": "error", 44 | "object-shorthand": "error", 45 | "prefer-arrow-callback": "error", 46 | "prefer-const": "error", 47 | "prefer-rest-params": "error", 48 | "prefer-spread": "error", 49 | "prefer-template": "error", 50 | "rest-spread-spacing": "error", 51 | "template-curly-spacing": "error", 52 | "yield-star-spacing": "error", 53 | 54 | // for ts 55 | "no-unused-vars": "off", 56 | "@typescript-eslint/no-unused-vars": "error", 57 | "no-array-constructor": "off", 58 | "@typescript-eslint/no-array-constructor": "error" 59 | }, 60 | "settings": { 61 | "node": { 62 | "tryExtensions": [".ts", ".tsx", ".js", ".jsx", ".json", ".node"] 63 | } 64 | }, 65 | "globals": { 66 | "window": false, 67 | "_window": false 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | obsolete/ 4 | thumbs.db 5 | yarn-error.log 6 | .idea/ 7 | .vscode/ 8 | .coverage/ 9 | .nyc_output/ 10 | dist/ 11 | coverage/ 12 | data/db.json 13 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/README.md: -------------------------------------------------------------------------------- 1 | # FIDO2 WebAuthn TypeScript Lib 2 | 3 | Formally, this sample was a demonstration code to show how the FIDO2 WebAuthn works in you browser via `karma` test codes invoked by `yarn`. 4 | 5 | Recent changes of package managers, `yarn` is now a bit obsolete and `pnpm` becomes more popular. However, `karma` doesn't work with `pnpm`, and hence we decided to gradually migrate our demonstration style from this Node.js version to another one. 6 | 7 | In 2023 lecture class, we implemented a rust-version of FIDO2 WebAuthn server for demonstration, and **this TypeScrypt codes is currently used as a library called from the frontend of the server**. This means the code is not directly used for demonstration. 8 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Oct 13 2020 22:00:08 GMT+0900 (Japan Standard Time) 3 | 4 | module.exports = (config) => { 5 | config.set({ 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: "", 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ["jasmine", "karma-typescript"], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | "./node_modules/js-crypto-utils/dist/jscu.bundle.js", 16 | { pattern: "dist/**/*.bundle.js" }, 17 | { pattern: "src/**/*.ts" }, 18 | { pattern: "test/**/*.ts" }, 19 | ], 20 | 21 | // list of files / patterns to exclude 22 | exclude: [], 23 | 24 | // preprocess matching files before serving them to the browser 25 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 26 | preprocessors: { 27 | "**/*.ts": ["karma-typescript"], 28 | }, 29 | 30 | // test results reporter to use 31 | // possible values: 'dots', 'progress' 32 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 33 | reporters: ["progress", "karma-typescript"], 34 | karmaTypescriptConfig: { 35 | bundlerOptions: { 36 | constants: { 37 | "process.env": typeof process.env.TEST_ENV !== "undefined" ? { TEST_ENV: process.env.TEST_ENV } : {}, 38 | }, 39 | transforms: [require("karma-typescript-es6-transform")()], 40 | }, 41 | coverageOptions: { 42 | exclude: /(test\/.*|\.(d|spec|test)\.ts)/i, 43 | }, 44 | reports: { 45 | html: { 46 | directory: "coverage", 47 | subdirectory: "karma/html", 48 | }, 49 | text: "", 50 | lcovonly: { 51 | directory: "coverage", 52 | subdirectory: "karma", 53 | }, 54 | }, 55 | }, 56 | 57 | client: { 58 | jasmine: { 59 | random: false, 60 | }, 61 | }, 62 | 63 | // web server port 64 | port: 9876, 65 | 66 | // enable / disable colors in the output (reporters and logs) 67 | colors: true, 68 | 69 | // level of logging 70 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 71 | logLevel: config.LOG_INFO, 72 | 73 | // enable / disable watching file and executing tests whenever any file changes 74 | // autoWatch: true, 75 | 76 | // start these browsers 77 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 78 | // browsers: ['ChromeHeadless'], 79 | browsers: ["Chrome"], 80 | // browsers: ['Chrome-headless'], 81 | // customLaunchers: { 82 | // 'Chrome-headless': { 83 | // base: 'Chrome', 84 | // flags: ['--headless', '--remote-debugging-port=9222', '--no-sandbox'] 85 | // } 86 | // }, 87 | 88 | // Continuous Integration mode 89 | // if true, Karma captures browsers, runs the tests and exits 90 | singleRun: true, 91 | 92 | // Concurrency level 93 | // how many browser should be started simultaneous 94 | concurrency: Infinity, 95 | }); 96 | }; 97 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "sample-fido2", 4 | "version": "0.0.1", 5 | "description": "Sample Frontend for FIDO 2", 6 | "main": "dist/index.js", 7 | "scripts": { 8 | "preinstall": "npx only-allow pnpm", 9 | "tsc": "tsc --build ./tsconfig.json", 10 | "webpack": "webpack --mode development --config webpack.config.js", 11 | "webpack:prod": "webpack --mode production --config webpack.config.js", 12 | "build": "rm -rf ./dist && yarn tsc && yarn webpack:prod", 13 | "cleanup": "rm -rf ./dist coverage ./node_modules" 14 | }, 15 | "author": "Jun Kurihara", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@types/jest": "29.5.11", 19 | "@types/node": "20.11.5", 20 | "@types/webappsec-credential-management": "0.6.8", 21 | "@typescript-eslint/eslint-plugin": "6.19.1", 22 | "@typescript-eslint/parser": "6.19.1", 23 | "can-npm-publish": "1.3.6", 24 | "cross-env": "7.0.3", 25 | "eslint": "8.56.0", 26 | "jasmine-core": "5.1.1", 27 | "jest": "29.7.0", 28 | "ts-jest": "29.1.2", 29 | "ts-loader": "9.5.1", 30 | "typescript": "5.3.3", 31 | "webpack": "5.89.0", 32 | "webpack-cli": "5.1.4" 33 | }, 34 | "dependencies": { 35 | "@peculiar/x509": "1.9.6", 36 | "buffer": "~6.0.3", 37 | "cbor-x": "~1.5.8", 38 | "js-crypto-utils": "1.0.7", 39 | "js-encoding-utils": "0.7.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/sample.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICvDCCAaSgAwIBAgIEBMX+/DANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ 3 | dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw 4 | MDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1 5 | YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQG 6 | A1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgODAwODQ3MzIwWTATBgcqhkjOPQIB 7 | BggqhkjOPQMBBwNCAAQc2Np2EaP17x+IXpULpl2A4zSFU5FYS9R/W3GcUyNcJCHk 8 | 45m9tXNngkGQk1dmYUk8kUwuZyTfk5T8+n3qixgEo2wwajAiBgkrBgEEAYLECgIE 9 | FTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsr 10 | BgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwGA1UdEwEB/wQCMAAwDQYJ 11 | KoZIhvcNAQELBQADggEBAHcYTO91LRoF8wpThdwthvj6wGNxcLAiYqUZXPX+0Db+ 12 | AGVODSkVvEVSmj+JXmrBzNQel3FW4AupOgbgrJmmcWWEBZyXSpRQtYcl2LTNU0+I 13 | z9WbyHNN1wQJ9ybFwj608xBuoNRC0rG8wgYbMC4usyRadt3dYOVdQi0cfaksVB2V 14 | NKnw+ttQUWKoZsPHtuzFx8NlazLQBep1W2T0FCONFEG7x/l+ZcfNhT13azAbaurJ 15 | 2J0/ff6H0PXJP6h+Obne4xfz0+8ujftWDUSh9oaiVRYf+tgam/tzOKyEU38V2liV 16 | 11zMyHKWrXiK0AfyDgb58ky2HSrn/AgE5MW/oXg/CXc= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 72 | 73 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/src/assertion.ts: -------------------------------------------------------------------------------- 1 | import {getJscu} from './env'; 2 | import {checkCredentialId, checkResponse, parseAuthenticatorResponse} from './credential'; 3 | 4 | export const verifyAssertion = async ( 5 | assertion: PublicKeyCredential, 6 | challenge: Uint8Array, 7 | publicKeyPem: string 8 | ): Promise<{ valid: boolean, msg: string }> => { 9 | const jscu = getJscu(); 10 | 11 | // Verifying the assertion for user authentication 12 | // https://www.w3.org/TR/webauthn/#verifying-assertion 13 | if (!checkCredentialId(assertion)) return {valid: false, msg: 'InvalidId'}; 14 | 15 | // AuthenticatorAssertionResponse 16 | const response = ((assertion).response); 17 | const clientDataHash = await jscu.hash.compute(new Uint8Array(response.clientDataJSON), 'SHA-256'); 18 | const decodedResponse = parseAuthenticatorResponse(response); 19 | const authenticatorData = decodedResponse.authenticatorData; 20 | const signature = decodedResponse.signature; 21 | 22 | // check clientDataJson and authData 23 | const r = await checkResponse(decodedResponse.clientDataJSON, authenticatorData, 'webauthn.get', challenge); 24 | if (!r.valid) { 25 | console.log(r.msg); 26 | return {valid: false, msg: r.msg}; 27 | } 28 | 29 | const verificationData = new Uint8Array(authenticatorData.length + clientDataHash.length); 30 | verificationData.set(authenticatorData, 0); 31 | verificationData.set(clientDataHash, authenticatorData.length); 32 | // console.log(authenticatorData.toString()); 33 | // console.log(clientDataHash.toString()); 34 | // console.log(verificationData.toString()); 35 | 36 | const jscuKeyObj = new jscu.Key('pem', publicKeyPem); 37 | const res = await jscu.pkc.verify(verificationData, signature, jscuKeyObj, 'SHA-256', {format: 'der'}); 38 | // console.log(x); 39 | return (res) ? {valid: true, msg: 'OK'} : {valid: false, msg: 'InvalidSignature'}; 40 | }; 41 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/src/attestation.ts: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | import {getJscu} from './env'; 3 | import * as x509 from '@peculiar/x509'; 4 | import {coseToJwk} from './util'; 5 | import {checkCredentialId, checkResponse, parseAuthenticatorResponse} from './credential'; 6 | 7 | const parseAttestedCredentialData = async ( 8 | authData: Uint8Array 9 | ): Promise<{ aaguid: Uint8Array, credentialId: string, publicKeyPem: string }> => { 10 | const tailer = authData.slice(37, authData.length); 11 | const aaguid = tailer.slice(0, 16); 12 | const credentialIdLength = (tailer[16] << 8) + tailer[17]; 13 | const credentialId = tailer.slice(18, 18 + credentialIdLength); 14 | const attestedCredentialsBuf = tailer.slice(18 + credentialIdLength, tailer.length); 15 | const jwk = coseToJwk(attestedCredentialsBuf); 16 | const pemKey = await (new (getJscu()).Key('jwk', jwk)).export('pem'); 17 | 18 | return {aaguid, credentialId: jseu.encoder.encodeBase64Url(credentialId), publicKeyPem: pemKey}; 19 | }; 20 | 21 | 22 | export const verifyAttestation = async ( 23 | credential: PublicKeyCredential, 24 | challenge: Uint8Array 25 | ): Promise<{ valid: boolean, credentialPublicKey?: string, attestationCertificate?: string }> => { 26 | const jscu = getJscu(); 27 | // https://www.w3.org/TR/webauthn/#registering-a-new-credential 28 | //check Id 29 | if (!checkCredentialId(credential)) { 30 | console.log('Credential ID is not valid'); 31 | return {valid: false}; 32 | } 33 | 34 | // AuthenticatorAttestationResponse 35 | const response = (credential.response); 36 | const decodedResponse = parseAuthenticatorResponse(response); 37 | const authData = decodedResponse.attestationObject.authData; 38 | 39 | // check clientDataJson and authData 40 | const r = await checkResponse(decodedResponse.clientDataJSON, authData, 'webauthn.create', challenge); 41 | if (!r.valid) { 42 | console.log(r.msg); 43 | return {valid: false}; 44 | } 45 | const clientDataHash = await jscu.hash.compute(new Uint8Array(response.clientDataJSON), 'SHA-256'); 46 | 47 | // TODO: Adapted only to Security Key By Yubico 48 | const fmt = decodedResponse.attestationObject.fmt; 49 | const attStmt = decodedResponse.attestationObject.attStmt; // attestation statement 50 | if (fmt === 'packed') { 51 | // to be verified for 'packed' 52 | const verificationData = new Uint8Array(authData.length + clientDataHash.length); 53 | verificationData.set(authData, 0); 54 | verificationData.set(clientDataHash, authData.length); 55 | 56 | // extract public key cert generated at Security Key By Yubico 57 | const pemCert = jseu.formatter.binToPem(new Uint8Array(attStmt.x5c[0]), 'certificate'); 58 | const crt = new x509.X509Certificate(pemCert); 59 | const jscuKeyObj = new jscu.Key('der', new Uint8Array(crt.publicKey.rawData)); 60 | 61 | // signature to be verified 62 | const signature = new Uint8Array(attStmt.sig); 63 | 64 | // @ts-ignore 65 | const validateSig = await jscu.pkc.verify(new Uint8Array(verificationData), signature, jscuKeyObj, 'SHA-256', {format: 'der'}); 66 | if (!validateSig) { 67 | console.log('Signature is not valid'); 68 | return {valid: false}; 69 | } 70 | 71 | // Get Public Key From AuthData 72 | const parsed = await parseAttestedCredentialData(new Uint8Array(authData)); 73 | return {valid: true, credentialPublicKey: parsed.publicKeyPem, attestationCertificate: pemCert}; 74 | } else { 75 | console.log(`This format is not supported: ${fmt}`); 76 | return {valid: false}; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/src/credential.ts: -------------------------------------------------------------------------------- 1 | import { decode } from 'cbor-x/decode'; 2 | import jseu from 'js-encoding-utils'; 3 | import {getJscu} from './env'; 4 | import { Buffer } from 'buffer'; 5 | window.Buffer = window.Buffer || Buffer; 6 | 7 | export const checkCredentialId = ( 8 | credential: PublicKeyCredential 9 | ): boolean => credential.id === jseu.encoder.encodeBase64Url(new Uint8Array(credential.rawId)); 10 | 11 | export const checkResponse = async ( 12 | clientDataJson: any, 13 | authData: Uint8Array, 14 | type: 'webauthn.create'|'webauthn.get', 15 | challenge: Uint8Array 16 | ): Promise<{valid: boolean, msg: string}> => { 17 | const jscu = getJscu(); 18 | 19 | if(clientDataJson.type !== type) return {valid: false, msg: 'InvalidType'}; 20 | if(jseu.encoder.encodeBase64Url(challenge) !== clientDataJson.challenge) return {valid: false, msg: 'InvalidChallenge'}; 21 | 22 | // check and parse authData https://www.w3.org/TR/webauthn/#authenticator-data 23 | const rpId = (new URL(clientDataJson.origin)).hostname; 24 | const rpIdHash = await jscu.hash.compute(jseu.encoder.stringToArrayBuffer(rpId), 'SHA-256'); 25 | // check rpIdHash 26 | if (jseu.encoder.encodeBase64(authData.slice(0, 32)) !== jseu.encoder.encodeBase64(rpIdHash)) return {valid: false, msg: 'InvalidRpIdHash'}; 27 | 28 | // check flag: TODO: Adapted only to Security Key By Yubico 29 | const flag = new Uint8Array([authData[32]]); 30 | if ((flag[0] & 0x01) !== 0x01) return {valid: false, msg: 'InvalidFlag'}; // check user present flag 31 | if (type === 'webauthn.create' && (flag[0] & 0x40) !== 0x40) return {valid: false, msg: 'InvalidFlag'}; // attestedCredentialData flag 32 | // TODO check clientExtensionResults and others from step 12... 33 | 34 | return {valid: true, msg: 'ok'}; 35 | }; 36 | 37 | // Parser 38 | export const parseAuthenticatorResponse = ( 39 | res: AuthenticatorAttestationResponse|AuthenticatorAssertionResponse 40 | ): {clientDataJSON: any, attestationObject?: any, authenticatorData?: any, signature?: any, userHandle?: any} => { 41 | const typeName = Object.prototype.toString.call(res).slice(8,-1); 42 | 43 | const clientDataJSON = JSON.parse( 44 | jseu.encoder.arrayBufferToString(new Uint8Array(res.clientDataJSON)) 45 | ); 46 | 47 | if(typeName === 'AuthenticatorAttestationResponse'){ 48 | const attestationObject = decode( 49 | Buffer.from( 50 | (res).attestationObject 51 | )); 52 | return {clientDataJSON, attestationObject}; 53 | } 54 | else { 55 | const authenticatorData = new Uint8Array((res).authenticatorData); 56 | const signature = new Uint8Array((res).signature); 57 | const userHandle = (res).userHandle; 58 | return {clientDataJSON, authenticatorData, signature, userHandle}; 59 | } 60 | }; 61 | 62 | export const getPublicKeyIdFromAssertion = (assertion: PublicKeyCredential): Uint8Array => new Uint8Array(assertion.rawId); 63 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/src/env.ts: -------------------------------------------------------------------------------- 1 | export const getJscu = () => { 2 | if(typeof window !== 'undefined' && typeof (window).jscu !== 'undefined'){ 3 | return (window).jscu; 4 | } 5 | else return require('js-crypto-utils'); 6 | }; 7 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * index.ts 3 | */ 4 | 5 | import * as x5c from './credential'; 6 | import {verifyAssertion as verifyAssertion1} from './assertion'; 7 | import {verifyAttestation as verifyAttestation1} from './attestation'; 8 | 9 | export const parseAuthenticatorResponse = x5c.parseAuthenticatorResponse; 10 | export const verifyAttestation = verifyAttestation1; 11 | export const verifyAssertion = verifyAssertion1; 12 | export const getPublicKeyIdFromAssertion = x5c.getPublicKeyIdFromAssertion; 13 | export default {parseAuthenticatorResponse, verifyAttestation, verifyAssertion, getPublicKeyIdFromAssertion}; 14 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/src/util.ts: -------------------------------------------------------------------------------- 1 | import jseu from 'js-encoding-utils'; 2 | import { decode } from 'cbor-x/decode'; 3 | import { Buffer } from 'buffer'; 4 | window.Buffer = window.Buffer || Buffer; 5 | 6 | export const coseToJwk = (cose: Uint8Array) => { 7 | const attestedCredentials = decode(Buffer.from(cose)); 8 | // https://tools.ietf.org/html/rfc8152#section-7 9 | const jwk: JsonWebKey = {}; 10 | Object.keys(attestedCredentials).forEach( (key: any) => { 11 | switch(parseInt(key)){ 12 | case 1: 13 | if (attestedCredentials[key] === 2) jwk.kty = 'EC'; 14 | break; 15 | case 3: 16 | if (attestedCredentials[key] === -7) jwk.alg = 'ES256'; 17 | break; 18 | case -1: 19 | if(attestedCredentials[key] === 1) jwk.crv = 'P-256'; 20 | break; 21 | case -2: 22 | jwk.x = jseu.encoder.encodeBase64Url(new Uint8Array(attestedCredentials[key])); 23 | break; 24 | case -3: 25 | jwk.y = jseu.encoder.encodeBase64Url(new Uint8Array(attestedCredentials[key])); 26 | break; 27 | } 28 | }); 29 | return jwk; 30 | }; 31 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/test/credential-params.ts: -------------------------------------------------------------------------------- 1 | //////// 2 | // Parameters for FIDO2 WebAuthn Credential Creation 3 | //////// 4 | 5 | // Parameters for Creation of Credential Key Pair/Certificate 6 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions 7 | export const createCredentialDefaultArgs: CredentialCreationOptions = { 8 | publicKey: { 9 | // Challenge 10 | // 本当はサーバーで生成した暗号学的に安全な乱数をセット (16bytes以上) 11 | challenge: new Uint8Array([ 12 | 0x8C, 0x0A, 0x26, 0xFF, 0x22, 0x91, 0xC1, 0xE9, 0xB9, 0x4E, 0x2E, 0x17, 0x1A, 0x98, 0x6A, 0x73, 13 | 0x71, 0x9D, 0x43, 0x48, 0xD5, 0xA7, 0x6A, 0x15, 0x7E, 0x38, 0x94, 0x52, 0x77, 0x97, 0x0F, 0xEF 14 | ]).buffer, 15 | 16 | // Relying Party Info (a.k.a. - Service): 17 | rp: { 18 | icon: 'https://login.example.com/login.ico', // optional 19 | id: 'localhost', 20 | name: 'Example RP' 21 | }, 22 | 23 | // User Info: 24 | user: { 25 | icon: 'https://login.example.com/login.ico', // optional 26 | id: new Uint8Array(16), 27 | name: 'john.p.smith@example.com', 28 | displayName: 'John P. Smith', 29 | }, 30 | 31 | // Public Key Credential Parameters 32 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions/pubKeyCredParams 33 | pubKeyCredParams: [{ 34 | type: 'public-key', // As of March 2019, only 'public-key' is accepted. 35 | alg: -7 // Signature Algorithm (ECDSA with SHA-256) // https://www.iana.org/assignments/cose/cose.xhtml#algorithms 36 | }], 37 | 38 | // Attestation Type (optional) 39 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions/attestation 40 | attestation: 'direct', 41 | 42 | // Time out (optional) 43 | timeout: 60000, 44 | 45 | // List of Credentials that are already registered. (Optional) 46 | // Use to avoid existing users from re-creating credential. 47 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions/excludeCredentials 48 | excludeCredentials: [], 49 | 50 | // (Optional) an optional property of the PublicKeyCredentialCreationOptions dictionary. 51 | // An object giving criteria to filter out the authenticators to be used for the creation operation. 52 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions/authenticatorSelection 53 | authenticatorSelection:{ 54 | authenticatorAttachment: 'cross-platform', // for USB key-like pluggable one 55 | residentKey: 'preferred', 56 | userVerification: 'required' 57 | }, 58 | 59 | // Extensions (Optional) 60 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions/extensions 61 | extensions: {} 62 | } 63 | }; 64 | 65 | // Parameters for Authentication (Assertion) 66 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions 67 | export const getCredentialDefaultArgs: CredentialRequestOptions = { 68 | publicKey: { 69 | // Challenge 70 | // 本当はサーバーで生成した暗号学的に安全な乱数をセット (16bytes以上) 71 | challenge: new Uint8Array([ 72 | 0x79, 0x50, 0x68, 0x71, 0xDA, 0xEE, 0xEE, 0xB9, 0x94, 0xC3, 0xC2, 0x15, 0x67, 0x65, 0x26, 0x22, 73 | 0xE3, 0xF3, 0xAB, 0x3B, 0x78, 0x2E, 0xD5, 0x6F, 0x81, 0x26, 0xE2, 0xA6, 0x01, 0x7D, 0x74, 0x50 74 | ]).buffer, 75 | 76 | // Info of credential public keys allowed to use authentication (Optional) 77 | // 認証器次第ではここが空、RPが指定しなくても問題ない。つまりユーザIDに応じてCredential IDを探してきてそれをユーザへ通知しなくていい。 78 | // (RP IDに応じてユーザが鍵を選べる, Client-side discoverable CredentialやresidentKeyと呼ぶ) 79 | allowCredentials: [{ 80 | id: (new Uint8Array()).buffer, 81 | transports: ['usb', 'nfc', 'ble'], 82 | type: 'public-key' 83 | }], 84 | 85 | // rpId indicating Relying Party ID (default = current domain) 86 | rpId: 'localhost', 87 | 88 | // User verification (biometrics authentication, optional, default = 'preferred') 89 | // PINが未指定の場合などは、'required'にすると検証不可として認証エラー 90 | userVerification: 'required', 91 | 92 | // Time out (optional) 93 | timeout: 60000, 94 | 95 | // Extensions (Optional) 96 | // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions/extensions 97 | extensions: {} 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/test/misc.spec.ts: -------------------------------------------------------------------------------- 1 | import {getTestEnv} from './prepare'; 2 | const env = getTestEnv(); 3 | // const library = env.library; 4 | const envName = env.envName; 5 | import jseu from 'js-encoding-utils'; 6 | // import * as x509 from '@fidm/x509'; 7 | import * as x509 from '@peculiar/x509' 8 | // import * as x509 from '@peculiar/x509' 9 | import * as jscu from 'js-crypto-utils'; 10 | 11 | describe(`${envName}: Misc tests for small utilities`, () => { 12 | it ('x509', async () => { 13 | const pemCert = '-----BEGIN CERTIFICATE-----\n' + 14 | 'MIICvDCCAaSgAwIBAgIEBMX+/DANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ\n' + 15 | 'dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw\n' + 16 | 'MDBaGA8yMDUwMDkwNDAwMDAwMFowbTELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1\n' + 17 | 'YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEmMCQG\n' + 18 | 'A1UEAwwdWXViaWNvIFUyRiBFRSBTZXJpYWwgODAwODQ3MzIwWTATBgcqhkjOPQIB\n' + 19 | 'BggqhkjOPQMBBwNCAAQc2Np2EaP17x+IXpULpl2A4zSFU5FYS9R/W3GcUyNcJCHk\n' + 20 | '45m9tXNngkGQk1dmYUk8kUwuZyTfk5T8+n3qixgEo2wwajAiBgkrBgEEAYLECgIE\n' + 21 | 'FTEuMy42LjEuNC4xLjQxNDgyLjEuMTATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsr\n' + 22 | 'BgEEAYLlHAEBBAQSBBD4oBHzjApNFYAGFxEfntx9MAwGA1UdEwEB/wQCMAAwDQYJ\n' + 23 | 'KoZIhvcNAQELBQADggEBAHcYTO91LRoF8wpThdwthvj6wGNxcLAiYqUZXPX+0Db+\n' + 24 | 'AGVODSkVvEVSmj+JXmrBzNQel3FW4AupOgbgrJmmcWWEBZyXSpRQtYcl2LTNU0+I\n' + 25 | 'z9WbyHNN1wQJ9ybFwj608xBuoNRC0rG8wgYbMC4usyRadt3dYOVdQi0cfaksVB2V\n' + 26 | 'NKnw+ttQUWKoZsPHtuzFx8NlazLQBep1W2T0FCONFEG7x/l+ZcfNhT13azAbaurJ\n' + 27 | '2J0/ff6H0PXJP6h+Obne4xfz0+8ujftWDUSh9oaiVRYf+tgam/tzOKyEU38V2liV\n' + 28 | '11zMyHKWrXiK0AfyDgb58ky2HSrn/AgE5MW/oXg/CXc=\n' + 29 | '-----END CERTIFICATE-----'; 30 | const crt = new x509.X509Certificate(pemCert); 31 | const key = new jscu.Key('der', new Uint8Array(crt.publicKey.rawData)); 32 | const jwk = await key.export('jwk'); 33 | expect((jwk).kty).toBe('EC'); 34 | expect((jwk).crv).toBe('P-256'); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/test/prepare.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * prepare.ts 3 | */ 4 | 5 | const base = require('../webpack.baseconfig'); 6 | 7 | 8 | export const getTestEnv = () => { 9 | let envName; 10 | let message; 11 | let library; 12 | //console.log(process.env.TEST_ENV); 13 | 14 | if (process.env.TEST_ENV === 'window'){ 15 | if(typeof window !== 'undefined' && typeof (window)[base.libName] !== 'undefined'){ 16 | envName = 'Window'; 17 | library = (window)[base.libName]; 18 | message = '**This is a test with a library imported from window.**'; 19 | } 20 | else throw new Error('The library is not loaded in window object.'); 21 | } 22 | else { 23 | envName = 'Source'; 24 | library = require('../src/index'); 25 | message = '**This is a test with source codes in src.**'; 26 | } 27 | 28 | return {library, envName, message}; 29 | }; 30 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | "lib": [ 7 | "dom", 8 | "esnext", 9 | "esnext.asynciterable" 10 | ], /* Specify library files to be included in the compilation. */ 11 | // "allowJs": true, /* Allow javascript files to be compiled. */ 12 | // "checkJs": true, /* Report errors in .js files. */ 13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 14 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./dist", /* Redirect output structure to the directory. */ 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "composite": true, /* Enable project compilation */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | "strictNullChecks": true, /* Enable strict null checks. */ 31 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | "paths": { 47 | "*": [ 48 | "./typings/*" 49 | ] 50 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | "typeRoots": [ 53 | "node_modules/@types" 54 | ], /* List of folders to include type definitions from. */ 55 | "types": [ 56 | "node", 57 | "jest" 58 | ], /* Type declaration files to be included in compilation. */ 59 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 60 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 61 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 62 | 63 | /* Source Map Options */ 64 | // "sourceRoot": "./src" /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 67 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 68 | 69 | /* Experimental Options */ 70 | "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 71 | "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */ 72 | }, 73 | "include": [ 74 | "src/**/*" 75 | ], 76 | "exclude": [ 77 | "node_modules", 78 | "**/*.spec.ts" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/webpack.baseconfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /*-- Change according to your project! --*/ 3 | libName: 'fido2testlib' 4 | }; 5 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const webpack = require('webpack'); 3 | const base = require('./webpack.baseconfig'); 4 | 5 | const config = { 6 | entry: ['./src/index.ts'], 7 | 8 | output: { 9 | filename: `${base.libName}.bundle.js`, 10 | chunkFilename: '[name].js', 11 | path: path.resolve(__dirname, './dist'), 12 | publicPath: path.resolve(__dirname, './dist'), 13 | library: base.libName, 14 | libraryTarget: 'umd', 15 | globalObject: 'this' // for node js import 16 | }, 17 | resolve: { 18 | extensions: ['.ts', '.tsx', '.js', '.jsx' ], 19 | modules: ['node_modules'] 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.ts$/, 25 | use: { 26 | loader: 'ts-loader', 27 | options: { 28 | transpileOnly: true, 29 | configFile: 'webpack.tsconfig.json', // for tree shaking https://mizchi.dev/202006101314-switch-tsconfig-on-webpack 30 | }, 31 | }, 32 | exclude: path.join(__dirname, 'node_modules') // exclude: /node_modules/ 33 | }, 34 | ], 35 | }, 36 | externals:{ 37 | crypto: true 38 | } 39 | }; 40 | 41 | module.exports = (env, argv) => { 42 | config.mode = (typeof argv.mode !== 'undefined' && argv.mode === 'production') ? argv.mode : 'development'; 43 | 44 | if (config.mode === 'production') console.log('Webpack for production'); 45 | else{ 46 | console.log('Webpack for development'); 47 | config.devtool = 'inline-source-map'; // add inline source map 48 | } 49 | 50 | return config; 51 | }; 52 | -------------------------------------------------------------------------------- /sample-07-09-rs/frontend-lib/webpack.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /sample-07-09-rs/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:8080"; 2 | pub const DEFAULT_ASSET_DIR: &str = "./assets"; 3 | 4 | pub const DEFAULT_RP_ID: &str = "localhost"; 5 | pub const DEFAULT_RP_ORIGIN: &str = "http://localhost:8080"; 6 | pub const DEFAULT_RP_NAME: &str = "Webauthn Sample"; 7 | 8 | pub const COOKIE_NAME: &str = "webauthn_sample"; 9 | 10 | pub const COOKIE_REGISTRATION_STATE: &str = "reg_state"; 11 | pub const COOKIE_AUTHENTICATION_STATE: &str = "auth_state"; 12 | 13 | pub const DEFAULT_TIMEOUT_SEC: u64 = 30; 14 | -------------------------------------------------------------------------------- /sample-07-09-rs/src/error.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub use anyhow::{anyhow, bail, ensure, Error, Result}; 3 | -------------------------------------------------------------------------------- /sample-07-09-rs/src/log.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused)] 2 | pub use tracing::{debug, error, info, warn}; 3 | use tracing_subscriber::{fmt, prelude::*, EnvFilter}; 4 | 5 | pub fn init_logger() { 6 | let format_layer = fmt::layer() 7 | .with_line_number(false) 8 | .with_thread_ids(false) 9 | .with_target(false) 10 | .with_thread_names(true) 11 | .with_target(true) 12 | .with_level(true) 13 | .compact(); 14 | 15 | // This limits the logger to emits only this crate 16 | let level_string = std::env::var(EnvFilter::DEFAULT_ENV).unwrap_or_else(|_| "info".to_string()); 17 | let filter_layer = EnvFilter::new(format!("{}={}", env!("CARGO_PKG_NAME"), level_string)); 18 | 19 | tracing_subscriber::registry() 20 | .with(format_layer) 21 | .with(filter_layer) 22 | .init(); 23 | } 24 | -------------------------------------------------------------------------------- /sample-07-09-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | mod constants; 2 | mod error; 3 | mod log; 4 | mod startup; 5 | mod webauthn; 6 | 7 | use crate::{error::*, log::*, startup::*, webauthn::*}; 8 | use axum::{error_handling::HandleErrorLayer, extract::Extension, http::StatusCode, routing::post, BoxError, Router}; 9 | use constants::DEFAULT_TIMEOUT_SEC; 10 | use std::sync::Arc; 11 | use tokio::runtime::Builder; 12 | use tower::ServiceBuilder; 13 | use tower_http::services::ServeDir; 14 | use tower_sessions::{cookie::SameSite, MemoryStore, SessionManagerLayer}; 15 | 16 | fn main() -> Result<()> { 17 | init_logger(); 18 | 19 | let mut runtime_builder = Builder::new_multi_thread(); 20 | runtime_builder.enable_all(); 21 | runtime_builder.thread_name("webauthn_sample"); 22 | let runtime = runtime_builder.build().unwrap(); 23 | 24 | runtime.block_on(async { 25 | match parse_opts().await { 26 | Ok(shared_state) => { 27 | define_route(Arc::new(shared_state)).await; 28 | } 29 | Err(e) => { 30 | error!("{e}"); 31 | } 32 | }; 33 | }); 34 | 35 | Ok(()) 36 | } 37 | 38 | async fn define_route(shared_state: Arc) { 39 | let addr = shared_state.listen_socket; 40 | let asset_dir = shared_state.asset_dir.clone(); 41 | let cookie_name = shared_state.cookie_name.clone(); 42 | let cookie_secure_flag = shared_state.cookie_secure_flag; 43 | 44 | // session 45 | let session_store = MemoryStore::default(); 46 | let error_layer = HandleErrorLayer::new(|_: BoxError| async { StatusCode::BAD_REQUEST }); 47 | let session_manager_layer = SessionManagerLayer::new(session_store) 48 | .with_secure(cookie_secure_flag) // This should be true in production (https environment) 49 | .with_name(cookie_name) 50 | .with_same_site(SameSite::Lax); 51 | let session_service = ServiceBuilder::new() 52 | .layer(error_layer) 53 | .layer(session_manager_layer) 54 | .timeout(std::time::Duration::from_secs(DEFAULT_TIMEOUT_SEC)); 55 | 56 | // routes 57 | let api = Router::<_>::new() 58 | .route("/register_start/:username", post(start_register)) 59 | .route("/register_finish", post(finish_register)) 60 | .route("/login_start/:username", post(start_auth)) 61 | .route("/login_finish", post(finish_auth)) 62 | .layer(Extension(shared_state)); 63 | let static_files = Router::new().nest_service("/", ServeDir::new(asset_dir).append_index_html_on_directories(true)); 64 | 65 | // build router with session 66 | let router = Router::new().merge(api).merge(static_files).layer(session_service); 67 | 68 | // build server 69 | let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 70 | let server = axum::serve(listener, router.into_make_service()); 71 | 72 | if let Err(e) = server.await { 73 | error!("Server is down!: {e}"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /sample-07-09-rs/src/startup.rs: -------------------------------------------------------------------------------- 1 | use crate::{constants::*, error::*, log::*}; 2 | use clap::Parser; 3 | use rustc_hash::FxHashMap as HashMap; 4 | use std::{ 5 | net::SocketAddr, 6 | sync::{Arc, Mutex}, 7 | }; 8 | use url::Url; 9 | use uuid::Uuid; 10 | use webauthn_rs::{prelude::Passkey, Webauthn, WebauthnBuilder}; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(author, version, about, long_about = None)] 14 | pub struct ClapArgs { 15 | /// Listen socket 16 | #[clap(short, long, default_value = DEFAULT_LISTEN_ADDR)] 17 | listen_address: String, 18 | 19 | /// Asset directory 20 | #[clap(short, long, default_value = DEFAULT_ASSET_DIR)] 21 | asset_dir: String, 22 | 23 | /// RP ID 24 | #[clap(long, default_value = DEFAULT_RP_ID)] 25 | rp_id: String, 26 | 27 | /// RP origin which must be a valid URL 28 | #[clap(long, default_value = DEFAULT_RP_ORIGIN)] 29 | rp_origin: String, 30 | 31 | /// RP name 32 | #[clap(long, default_value = DEFAULT_RP_NAME)] 33 | rp_name: String, 34 | 35 | /// Cookie name 36 | #[clap(long, default_value = COOKIE_NAME)] 37 | cookie_name: String, 38 | } 39 | 40 | #[derive(Debug)] 41 | pub struct AppState { 42 | pub listen_socket: SocketAddr, 43 | pub asset_dir: String, 44 | pub cookie_name: String, 45 | pub cookie_secure_flag: bool, 46 | 47 | pub webauthn: Arc, 48 | 49 | pub users: Arc>, 50 | } 51 | 52 | #[derive(Debug)] 53 | pub struct UserData { 54 | pub username_id_map: HashMap, 55 | pub id_passkey_map: HashMap>, 56 | } 57 | 58 | pub async fn parse_opts() -> Result { 59 | let _ = include_str!("../Cargo.toml"); 60 | let args = ClapArgs::parse(); 61 | 62 | let listen_socket = args.listen_address.parse::()?; 63 | info!("Listening on {}", &listen_socket); 64 | 65 | let asset_dir = args.asset_dir; 66 | info!("Serving static files from {}", &asset_dir); 67 | 68 | // webauthn 69 | info!("RP ID: {}", &args.rp_id); 70 | let rp_origin = Url::parse(&args.rp_origin).expect("Invalid URL"); 71 | info!("RP origin: {}", rp_origin); 72 | info!("RP name: {}", &args.rp_name); 73 | let builder = WebauthnBuilder::new(&args.rp_id, &rp_origin)?; 74 | let builder = builder.rp_name(&args.rp_name); 75 | let webauthn = Arc::new(builder.build()?); 76 | 77 | // cookie 78 | let cookie_secure_flag = rp_origin.scheme() == "https"; 79 | let cookie_name = args.cookie_name; 80 | info!("Cookie name: {} (secure={})", &cookie_name, cookie_secure_flag); 81 | 82 | // user db 83 | let users = Arc::new(Mutex::new(UserData { 84 | username_id_map: HashMap::default(), 85 | id_passkey_map: HashMap::default(), 86 | })); 87 | 88 | let app_state = AppState { 89 | listen_socket, 90 | asset_dir, 91 | cookie_secure_flag, 92 | cookie_name, 93 | webauthn, 94 | users, 95 | }; 96 | Ok(app_state) 97 | } 98 | -------------------------------------------------------------------------------- /sample-07-09-rs/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Build fido2 client lib from TypeScript codes" 4 | cd ./frontend-lib 5 | pnpm cleanup && pnpm i && pnpm build && cp dist/*.bundle.js ../assets/ 6 | 7 | echo "Build fido2 server from Rust codes" 8 | cd .. 9 | cargo build --release 10 | 11 | echo "Run fido2 server" 12 | ../target/release/webauthn_sample 13 | -------------------------------------------------------------------------------- /slides2023-10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2023-10.pdf -------------------------------------------------------------------------------- /slides2023-11.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2023-11.pdf -------------------------------------------------------------------------------- /slides2024-01.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-01.pdf -------------------------------------------------------------------------------- /slides2024-02.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-02.pdf -------------------------------------------------------------------------------- /slides2024-03.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-03.pdf -------------------------------------------------------------------------------- /slides2024-04.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-04.pdf -------------------------------------------------------------------------------- /slides2024-05.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-05.pdf -------------------------------------------------------------------------------- /slides2024-06.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-06.pdf -------------------------------------------------------------------------------- /slides2024-07-09.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-07-09.pdf -------------------------------------------------------------------------------- /slides2024-10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junkurihara/lecture-security_engineering/7cc505b8eacdf1851933894c177f14f4c171ee0d/slides2024-10.pdf --------------------------------------------------------------------------------