├── .gitignore ├── .github ├── CODEOWNERS └── workflows │ ├── build.yaml │ └── release.yaml ├── README.md ├── Cargo.toml ├── src ├── bin │ └── nk │ │ └── main.rs ├── crc.rs ├── error.rs ├── xkeys.rs └── lib.rs └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea 5 | .vscode/ -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # wasmcloud nkeys maintainers 2 | * @wasmCloud/nkeys-maintainers 3 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.config.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | config: 19 | - { os: "ubuntu-latest" } 20 | - { os: "macos-latest" } 21 | - { os: "windows-latest" } 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Install latest Rust stable toolchain 26 | uses: dtolnay/rust-toolchain@v1 27 | with: 28 | toolchain: stable 29 | default: true 30 | components: clippy, rustfmt 31 | - name: Format check 32 | run: cargo fmt --all -- --check 33 | - name: Test 34 | run: cargo test --all-features 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | github_release: 13 | if: startswith(github.ref, 'refs/tags/') # Only run on tag push 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Create Release 17 | id: create_release 18 | uses: softprops/action-gh-release@v1 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | tag_name: ${{ github.ref }} 22 | name: Release ${{ github.ref }} 23 | draft: false 24 | prerelease: true 25 | 26 | crates_release: 27 | if: startswith(github.ref, 'refs/tags/') # Only run on tag push 28 | needs: github_release 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | - id: crates-release-action 33 | uses: wasmcloud/common-actions/crates-release@main 34 | with: 35 | crates-token: ${{ secrets.CRATES_PUBLISH_TOKEN }} 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/wasmCloud/nkeys/actions/workflows/build.yaml/badge.svg?branch=main)](https://github.com/wasmCloud/nkeys/actions/workflows/build.yaml) 2 | 3 | # NKeys 4 | 5 | Rust implementation of the [NATS nkeys](https://github.com/nats-io/nkeys) Go library. 6 | 7 | The NATS ecosystem as of v2.x+ moved to Ed25519 keys for identity, authentication and authorization for entities such as Accounts, Users, Servers and Clusters. 8 | 9 | Ed25519 is fast and resistant to side channel attacks. Generation of a seed key is all that is needed to be stored and kept safe, as the seed can generate both the public and private keys. 10 | 11 | The NATS system will utilize Ed25519 keys, meaning that NATS systems will never store or even have access to any private keys. Authentication will utilize a random challenge response mechanism. 12 | 13 | Dealing with 32 byte and 64 byte raw keys can be challenging. NKEYS is designed to formulate keys in a much friendlier fashion and references work done in cryptocurrencies, specifically Stellar. Bitcoin and others used a form of Base58 (or Base58Check) to endode raw keys. Stellar utilized a more traditonal Base32 with a CRC16 and a version or prefix byte. NKEYS utilizes a similar format where the prefix will be 1 byte for public and private keys and will be 2 bytes for seeds. The base32 encoding of these prefixes will yield friendly human readbable prefixes. 14 | 15 | The following is a list of available prefixes and their keypair types: 16 | 17 | * **N** - Server 18 | * **C** - Cluster 19 | * **O** - Operator 20 | * **A** - Account 21 | * **U** - User 22 | * **M** - Module 23 | * **V** - Service / Service Provider 24 | * **P** - Private Key 25 | * **X** - Curve Key (X25519) 26 | 27 | For seeds, the first encoded prefix is **S**, and the second character will be the type for the public key, e.g. `SU` is a seed for a user key pair, `SA` is a seed for an account key pair. 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nkeys" 3 | version = "0.4.5" 4 | authors = ["wasmCloud Team"] 5 | edition = "2021" 6 | description = "Rust implementation of the NATS nkeys library" 7 | license = "Apache-2.0" 8 | homepage = "https://github.com/wasmcloud/nkeys" 9 | documentation = "https://docs.rs/nkeys" 10 | repository = "https://github.com/wasmcloud/nkeys" 11 | readme = "README.md" 12 | keywords = ["crypto", "nats", "ed25519", "cryptography"] 13 | categories = ["cryptography", "authentication"] 14 | 15 | [features] 16 | cli = [ 17 | "quicli", 18 | "structopt", 19 | "term-table", 20 | "exitfailure", 21 | "env_logger", 22 | "serde_json", 23 | ] 24 | 25 | xkeys = ["dep:crypto_box"] 26 | 27 | [package.metadata.docs.rs] 28 | all_features = true 29 | 30 | [[bin]] 31 | name = "nk" 32 | required-features = ["cli"] 33 | 34 | [dependencies] 35 | signatory = "0.27" 36 | ed25519 = { version = "2.2.3", default-features = false } 37 | ed25519-dalek = { version = "2.0.0", default-features = false, features = [ 38 | "digest", 39 | ] } 40 | rand = "0.8" 41 | data-encoding = "2.3.0" 42 | log = "0.4.11" 43 | crypto_box = { version = "0.9.1", optional = true } # For xKeys support 44 | 45 | # CLI Dependencies 46 | quicli = { version = "0.4", optional = true } 47 | structopt = { version = "0.3.17", optional = true } 48 | term-table = { version = "1.3.0", optional = true } 49 | exitfailure = { version = "0.5.1", optional = true } 50 | env_logger = { version = "0.9", optional = true } 51 | serde_json = { version = "1.0", optional = true } 52 | 53 | [target.'cfg(target_arch = "wasm32")'.dependencies] 54 | # NOTE: We need this due to an underlying dependency being pulled in by 55 | # `ed25519-dalek`. Even if we exclude `rand`, that crate pulls it in. `rand` pulls in the low level 56 | # `getrandom` library, which explicitly doesn't support wasm32-unknown-unknown. This is a hack to 57 | # get around that by enabling the `custom` feature of getrandom (even though we don't actually use 58 | # the library). This makes `rand` compile in the dalek library even though we aren't actually using 59 | # the `rand` part of it. 60 | getrandom = { version = "0.2", default-features = false, features = ["custom"] } 61 | -------------------------------------------------------------------------------- /src/bin/nk/main.rs: -------------------------------------------------------------------------------- 1 | extern crate serde_json; 2 | 3 | use nkeys::{self, KeyPair, KeyPairType}; 4 | use serde_json::json; 5 | use std::error::Error; 6 | use std::fmt; 7 | use std::str::FromStr; 8 | use structopt::clap::AppSettings; 9 | use structopt::StructOpt; 10 | 11 | #[derive(Debug, StructOpt, Clone)] 12 | #[structopt( 13 | global_settings(&[AppSettings::ColoredHelp, AppSettings::VersionlessSubcommands]), 14 | name = "nk", 15 | about = "A tool for manipulating nkeys" 16 | )] 17 | struct Cli { 18 | #[structopt(subcommand)] 19 | cmd: Command, 20 | } 21 | 22 | #[derive(StructOpt, Debug, Clone)] 23 | enum Command { 24 | #[structopt(name = "gen", about = "Generates a key pair")] 25 | Gen { 26 | /// The type of key pair to generate. May be Account, User, Module, Service, Server, Operator, Cluster, Curve (xkey) 27 | #[structopt(case_insensitive = true)] 28 | keytype: KeyPairType, 29 | #[structopt( 30 | short = "o", 31 | long = "output", 32 | default_value = "text", 33 | help = "Specify output format (text or json)" 34 | )] 35 | output: Output, 36 | }, 37 | } 38 | 39 | #[derive(StructOpt, Debug, Clone)] 40 | enum Output { 41 | Text, 42 | Json, 43 | } 44 | 45 | impl FromStr for Output { 46 | type Err = OutputParseErr; 47 | 48 | fn from_str(s: &str) -> Result { 49 | match s { 50 | "json" => Ok(Output::Json), 51 | "text" => Ok(Output::Text), 52 | _ => Err(OutputParseErr), 53 | } 54 | } 55 | } 56 | 57 | #[derive(Debug, Clone, PartialEq, Eq)] 58 | struct OutputParseErr; 59 | 60 | impl Error for OutputParseErr {} 61 | 62 | impl fmt::Display for OutputParseErr { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | write!( 65 | f, 66 | "error parsing output type, see help for the list of accepted outputs" 67 | ) 68 | } 69 | } 70 | 71 | fn main() { 72 | let args = Cli::from_args(); 73 | let cmd = &args.cmd; 74 | env_logger::init(); 75 | 76 | match cmd { 77 | Command::Gen { keytype, output } => { 78 | generate(keytype, output); 79 | } 80 | } 81 | } 82 | 83 | fn generate(kt: &KeyPairType, output_type: &Output) { 84 | let kp = KeyPair::new(kt.clone()); 85 | match output_type { 86 | Output::Text => { 87 | println!( 88 | "Public Key: {}\nSeed: {}\n\nRemember that the seed is private, treat it as a secret.", 89 | kp.public_key(), 90 | kp.seed().unwrap() 91 | ); 92 | } 93 | Output::Json => { 94 | let output = json!({ 95 | "public_key": kp.public_key(), 96 | "seed": kp.seed().unwrap(), 97 | }); 98 | 99 | println!("{}", output); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/crc.rs: -------------------------------------------------------------------------------- 1 | use super::error::{Error, ErrorKind}; 2 | use super::Result; 3 | 4 | const CRC_TABLE: [u16; 256] = [ 5 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 6 | 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 7 | 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 8 | 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 9 | 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 10 | 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 11 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 12 | 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 13 | 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 14 | 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 15 | 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 16 | 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 17 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 18 | 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 19 | 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 20 | 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 21 | 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 22 | 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 23 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 24 | 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 25 | 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 26 | 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 27 | ]; 28 | 29 | pub(crate) fn crc16(data: &[u8]) -> u16 { 30 | let mut crc = 0_u16; 31 | for b in data { 32 | let idx = ((crc >> 8) ^ *b as u16) & 0x00FF; 33 | crc = (crc << 8) ^ CRC_TABLE[idx as usize]; 34 | } 35 | crc 36 | } 37 | 38 | pub(crate) fn valid_checksum(data: &[u8], expected: u16) -> bool { 39 | crc16(data) == expected 40 | } 41 | 42 | pub(crate) fn push_crc(data: &mut Vec) { 43 | let crc = crc16(data); 44 | data.extend(u16::to_le_bytes(crc)); 45 | } 46 | 47 | pub(crate) fn extract_crc(data: &mut Vec) -> Result { 48 | let data_len = data.len().checked_sub(2).ok_or_else(|| { 49 | Error::new( 50 | ErrorKind::ChecksumFailure, 51 | Some("CRC data vector contains less than two characters"), 52 | ) 53 | })?; 54 | 55 | let crc = u16::from_le_bytes(data[data_len..].try_into().unwrap()); 56 | data.truncate(data_len); 57 | Ok(crc) 58 | } 59 | 60 | #[cfg(test)] 61 | mod tests { 62 | use super::{crc16, extract_crc, push_crc, valid_checksum}; 63 | 64 | #[test] 65 | fn e2e() { 66 | let mut raw_data = rand::random::<[u8; 32]>(); 67 | let data_crc = crc16(&raw_data); 68 | 69 | let mut data = raw_data.to_vec(); 70 | push_crc(&mut data); 71 | 72 | let crc = extract_crc(&mut data).unwrap(); 73 | assert_eq!(raw_data, data.as_slice()); 74 | assert_eq!(data_crc, crc); 75 | assert!(valid_checksum(&raw_data, data_crc)); 76 | 77 | raw_data[17] = raw_data[17].wrapping_sub(1); 78 | assert!(!valid_checksum(&raw_data, data_crc)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! # Error wrappers and boilerplate 2 | //! 3 | //! This module provides some basic boilerplate for errors. As a consumer of this 4 | //! library, you should expect that all public functions return a `Result` type 5 | //! using this local `Error`, which implements the standard Error trait. 6 | //! As a general rule, errors that come from dependent crates are wrapped by 7 | //! this crate's error type. 8 | #![allow(unused_macros)] 9 | 10 | use core::fmt; 11 | use signatory::signature; 12 | 13 | use std::{ 14 | error::Error as StdError, 15 | string::{String, ToString}, 16 | }; 17 | 18 | /// Provides an error type specific to the nkeys library 19 | #[derive(Debug)] 20 | pub struct Error { 21 | kind: ErrorKind, 22 | 23 | description: Option, 24 | } 25 | 26 | /// Provides context as to how a particular nkeys error might have occurred 27 | #[derive(Debug, Copy, Clone, PartialEq)] 28 | pub enum ErrorKind { 29 | /// Indicates an inappropriate byte prefix was used for an encoded key string 30 | InvalidPrefix, 31 | /// Indicates a key string was used with the wrong length 32 | InvalidKeyLength, 33 | /// Indicates a signature verification mismatch. Use this to check for invalid signatures or messages 34 | VerifyError, 35 | /// Indicates an unexpected underlying error occurred while trying to perform routine signature tasks. 36 | SignatureError, 37 | /// Indicates a checksum mismatch occurred while validating a crc-encoded string 38 | ChecksumFailure, 39 | /// Indicates a miscellaneous error occurred during encoding or decoding the nkey-specific formats 40 | CodecFailure, 41 | /// Indicates a key type mismatch, e.g. attempting to sign with only a public key 42 | IncorrectKeyType, 43 | /// Payload not valid (or failed to be decrypted) 44 | InvalidPayload, 45 | /// Signature did not match the expected length (64 bytes) 46 | InvalidSignatureLength, 47 | } 48 | 49 | /// A handy macro borrowed from the `signatory` crate that lets library-internal code generate 50 | /// more readable exception handling flows 51 | #[macro_export] 52 | macro_rules! err { 53 | ($variant:ident, $msg:expr) => { 54 | $crate::error::Error::new( 55 | $crate::error::ErrorKind::$variant, 56 | Some($msg) 57 | ) 58 | }; 59 | ($variant:ident, $fmt:expr, $($arg:tt)+) => { 60 | err!($variant, &format!($fmt, $($arg)+)) 61 | }; 62 | } 63 | 64 | impl ErrorKind { 65 | pub fn as_str(self) -> &'static str { 66 | match self { 67 | ErrorKind::InvalidPrefix => "Invalid byte prefix", 68 | ErrorKind::InvalidKeyLength => "Invalid key length", 69 | ErrorKind::InvalidSignatureLength => "Invalid signature length", 70 | ErrorKind::VerifyError => "Signature verification failure", 71 | ErrorKind::ChecksumFailure => "Checksum match failure", 72 | ErrorKind::CodecFailure => "Codec failure", 73 | ErrorKind::SignatureError => "Signature failure", 74 | ErrorKind::IncorrectKeyType => "Incorrect key type", 75 | ErrorKind::InvalidPayload => "Invalid payload", 76 | } 77 | } 78 | } 79 | 80 | impl fmt::Display for ErrorKind { 81 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | write!(f, "{}", self.as_str()) 83 | } 84 | } 85 | 86 | impl Error { 87 | /// Creates a new nkeys error wrapper 88 | pub fn new(kind: ErrorKind, description: Option<&str>) -> Self { 89 | Error { 90 | kind, 91 | description: description.map(|desc| desc.to_string()), 92 | } 93 | } 94 | 95 | /// An accessor exposing the error kind enum. Crate consumers should have little to no 96 | /// need to access this directly and it's mostly used to assert that internal functions 97 | /// are creating appropriate error wrappers. 98 | pub fn kind(&self) -> ErrorKind { 99 | self.kind 100 | } 101 | } 102 | 103 | /// Creates an nkeys error derived from an error that came from the `signatory` crate 104 | impl From for Error { 105 | fn from(source: signature::Error) -> Error { 106 | err!(SignatureError, &format!("Signature error: {}", source)) 107 | } 108 | } 109 | 110 | /// Creates an nkeys error derived from a decoding failure in the `data_encoding` crate 111 | impl From for Error { 112 | fn from(source: data_encoding::DecodeError) -> Error { 113 | err!(CodecFailure, "Data encoding failure: {}", source) 114 | } 115 | } 116 | 117 | impl StdError for Error { 118 | fn description(&self) -> &str { 119 | if let Some(ref desc) = self.description { 120 | desc 121 | } else { 122 | self.kind.as_str() 123 | } 124 | } 125 | } 126 | 127 | impl fmt::Display for Error { 128 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 129 | match self.description { 130 | Some(ref desc) => write!(f, "{}: {}", self.kind.as_str(), desc), 131 | None => write!(f, "{}", self.kind.as_str()), 132 | } 133 | } 134 | } 135 | 136 | #[cfg(test)] 137 | mod tests { 138 | #[test] 139 | fn test_error_to_string() { 140 | assert_eq!( 141 | err!(InvalidKeyLength, "Testing").to_string(), 142 | "Invalid key length: Testing" 143 | ); 144 | assert_eq!( 145 | err!(InvalidKeyLength, "Testing {}", 1).to_string(), 146 | "Invalid key length: Testing 1" 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/xkeys.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | decode_raw, decode_seed, encode, encode_prefix, encode_seed, err, KeyPairType, 3 | PREFIX_BYTE_CURVE, PREFIX_BYTE_PRIVATE, 4 | }; 5 | 6 | use super::Result; 7 | use crypto_box::{ 8 | aead::{Aead, AeadCore}, 9 | Nonce, SalsaBox, 10 | }; 11 | use ed25519::signature::digest::typenum::Unsigned; 12 | use std::fmt::{self, Debug}; 13 | 14 | const XKEY_VERSION_V1: &[u8] = b"xkv1"; 15 | 16 | use crypto_box::{PublicKey, SecretKey}; 17 | use rand::{CryptoRng, Rng, RngCore}; 18 | 19 | /// The main interface used for reading and writing _nkey-encoded_ curve key 20 | /// pairs. 21 | #[derive(Clone)] 22 | pub struct XKey { 23 | public: PublicKey, 24 | secret: Option, 25 | } 26 | 27 | impl Debug for XKey { 28 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 29 | write!(f, "XKey") 30 | } 31 | } 32 | 33 | impl XKey { 34 | /// Creates a new xkey. 35 | /// 36 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 37 | /// rand support. Use [`new_from_raw`](XKey::new_from_raw) instead 38 | #[cfg(not(target_arch = "wasm32"))] 39 | pub fn new() -> Self { 40 | Self::new_with_rand(&mut rand::rngs::OsRng) 41 | } 42 | 43 | /// Create a new xkey pair from a random generator 44 | /// 45 | /// NOTE: These generator should be a cryptographically secure random source. 46 | /// 47 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 48 | /// rand support. Use [`new_from_raw`](XKey::new_from_raw) instead 49 | #[cfg(not(target_arch = "wasm32"))] 50 | pub fn new_with_rand(rand: &mut (impl CryptoRng + RngCore)) -> Self { 51 | Self::new_from_raw(rand.gen()) 52 | } 53 | 54 | /// Create a new xkey pair using a pre-existing set of random bytes. 55 | /// 56 | /// NOTE: These bytes should be generated from a cryptographically secure random source. 57 | pub fn new_from_raw(random_bytes: [u8; 32]) -> Self { 58 | let private = SecretKey::from_bytes(random_bytes); 59 | Self { 60 | public: private.public_key(), 61 | secret: Some(private), 62 | } 63 | } 64 | 65 | /// Attempts to produce a public-only xkey from the given encoded public key string 66 | pub fn from_public_key(source: &str) -> Result { 67 | let source_bytes = source.as_bytes(); 68 | let raw = decode_raw(source_bytes)?; 69 | 70 | let (prefix, rest) = raw.split_first().ok_or(err!(VerifyError, "Empty key"))?; 71 | if *prefix != PREFIX_BYTE_CURVE { 72 | Err(err!( 73 | InvalidPrefix, 74 | "Not a valid public key prefix: {}", 75 | raw[0] 76 | )) 77 | } else { 78 | let public = PublicKey::try_from(rest) 79 | .map_err(|_| err!(VerifyError, "Could not read public key"))?; 80 | 81 | Ok(Self { 82 | public, 83 | secret: None, 84 | }) 85 | } 86 | } 87 | 88 | /// Attempts to produce a full xkey pair from the given encoded seed string 89 | pub fn from_seed(source: &str) -> Result { 90 | let (ty, seed) = decode_seed(source)?; 91 | 92 | if ty != PREFIX_BYTE_CURVE { 93 | return Err(err!( 94 | InvalidPrefix, 95 | "Expected a curve, got {:?}", 96 | KeyPairType::from(ty) 97 | )); 98 | } 99 | 100 | let secret = SecretKey::from_bytes(seed); 101 | Ok(Self { 102 | public: secret.public_key(), 103 | secret: Some(secret), 104 | }) 105 | } 106 | 107 | /// Attempts to return the encoded, human-readable string for this key pair's seed. 108 | /// Remember that this value should be treated as a secret. Do not store it for 109 | /// any longer than necessary 110 | pub fn seed(&self) -> Result { 111 | let Some(secret) = &self.secret else { 112 | return Err(err!(IncorrectKeyType, "This keypair has no seed")); 113 | }; 114 | 115 | Ok(encode_seed(&KeyPairType::Curve, &secret.to_bytes())) 116 | } 117 | 118 | /// Returns the encoded, human-readable public key of this key pair 119 | pub fn public_key(&self) -> String { 120 | encode(&KeyPairType::Curve, self.public.as_bytes()) 121 | } 122 | 123 | pub fn private_key(&self) -> Result { 124 | let Some(secret) = &self.secret else { 125 | return Err(err!(IncorrectKeyType, "This keypair has no seed")); 126 | }; 127 | 128 | Ok(encode_prefix(&[PREFIX_BYTE_PRIVATE], &secret.to_bytes())) 129 | } 130 | 131 | /// Returns the type of this key pair. 132 | pub fn key_pair_type(&self) -> KeyPairType { 133 | KeyPairType::Curve 134 | } 135 | 136 | pub fn open(&self, input: &[u8], sender: &Self) -> Result> { 137 | let nonce_size = ::NonceSize::to_usize(); 138 | 139 | let Some(secret_key) = &self.secret else { 140 | return Err(err!(SignatureError, "Cannot open without a private key")); 141 | }; 142 | 143 | if input.len() <= XKEY_VERSION_V1.len() + nonce_size { 144 | return Err(err!(InvalidPayload, "Payload too short")); 145 | } 146 | 147 | let Some(input) = input.strip_prefix(XKEY_VERSION_V1) else { 148 | return Err(err!(InvalidPrefix, "Cannot open message, wrong version")); 149 | }; 150 | 151 | let (nonce, input) = input.split_at(nonce_size); 152 | 153 | let b = SalsaBox::new(&sender.public, secret_key); 154 | b.decrypt(nonce.into(), input) 155 | .map_err(|_| err!(InvalidPayload, "Cannot decrypt payload")) 156 | } 157 | 158 | /// Seal is compatible with nacl.Box.Seal() and can be used in similar situations for small 159 | /// messages. We generate the nonce from crypto rand by default. 160 | /// 161 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 162 | /// rand support. Use [`seal_with_nonce`](XKey::seal_with_nonce) instead 163 | #[cfg(not(target_arch = "wasm32"))] 164 | pub fn seal(&self, input: &[u8], recipient: &Self) -> Result> { 165 | self.seal_with_rand(input, recipient, &mut rand::rngs::OsRng) 166 | } 167 | 168 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 169 | /// rand support. Use [`seal_with_nonce`](XKey::seal_with_nonce) instead 170 | #[cfg(not(target_arch = "wasm32"))] 171 | pub fn seal_with_rand( 172 | &self, 173 | input: &[u8], 174 | recipient: &Self, 175 | rand: impl CryptoRng + RngCore, 176 | ) -> Result> { 177 | let nonce = SalsaBox::generate_nonce(rand); 178 | self.seal_with_nonce(input, recipient, nonce) 179 | } 180 | 181 | /// NOTE: Nonce bytes should be generated from a cryptographically secure random source, and 182 | /// only be used once. 183 | pub fn seal_with_nonce(&self, input: &[u8], recipient: &Self, nonce: Nonce) -> Result> { 184 | let Some(private_key) = &self.secret else { 185 | return Err(err!(SignatureError, "Cannot seal without a private key")); 186 | }; 187 | 188 | let b = SalsaBox::new(&recipient.public, private_key); 189 | let crypted = b 190 | .encrypt(&nonce, input) 191 | .map_err(|_| err!(SignatureError, "Cannot seal payload"))?; // Can't fail when used with SalsaBox 192 | 193 | let mut out = Vec::with_capacity( 194 | XKEY_VERSION_V1.len() 195 | + ::NonceSize::to_usize() 196 | + input.len() 197 | + ::TagSize::to_usize(), 198 | ); 199 | out.extend_from_slice(XKEY_VERSION_V1); 200 | out.extend_from_slice(nonce.as_slice()); 201 | out.extend_from_slice(&crypted); 202 | 203 | Ok(out) 204 | } 205 | } 206 | 207 | #[cfg(not(target_arch = "wasm32"))] 208 | impl Default for XKey { 209 | fn default() -> Self { 210 | Self::new() 211 | } 212 | } 213 | 214 | #[cfg(test)] 215 | mod tests { 216 | use super::*; 217 | use crate::error::ErrorKind; 218 | const MESSAGE: &[u8] = b"this is super secret"; 219 | 220 | #[test] 221 | fn seed_encode_decode_round_trip() { 222 | let pair = XKey::new(); 223 | let s = pair.seed().unwrap(); 224 | let p = pair.public_key(); 225 | 226 | let pair2 = XKey::from_seed(s.as_str()).unwrap(); 227 | let s2 = pair2.seed().unwrap(); 228 | 229 | assert_eq!(s, s2); 230 | assert_eq!(p, pair2.public_key()); 231 | } 232 | 233 | #[test] 234 | fn roundtrip_encoding_go_compat() { 235 | // Seed and Public Key pair generated by Go nkeys library 236 | let seed = "SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZM"; 237 | let pk = "XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA"; 238 | 239 | let pair = XKey::from_seed(seed).unwrap(); 240 | 241 | assert_eq!(pair.seed().unwrap(), seed); 242 | assert_eq!(pair.public_key(), pk); 243 | } 244 | 245 | #[test] 246 | fn from_seed_rejects_bad_prefix() { 247 | let seed = "SZAIB67JMUPS5OKP6BZNCFTIMHOTS6JIX2C53TLSNEROIRFBJLSK3NUOVY"; 248 | let pair = XKey::from_seed(seed); 249 | assert!(pair.is_err()); 250 | if let Err(e) = pair { 251 | assert_eq!(e.kind(), ErrorKind::InvalidPrefix); 252 | } 253 | } 254 | 255 | #[test] 256 | fn from_seed_rejects_bad_length() { 257 | let seed = "SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZMA"; 258 | let pair = XKey::from_seed(seed); 259 | assert!(pair.is_err()); 260 | if let Err(e) = pair { 261 | assert_eq!(e.kind(), ErrorKind::InvalidKeyLength); 262 | } 263 | } 264 | 265 | #[test] 266 | fn from_seed_rejects_invalid_encoding() { 267 | let badseed = "SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS!GNHFXO5D4JJZM"; 268 | let pair = XKey::from_seed(badseed); 269 | assert!(pair.is_err()); 270 | if let Err(e) = pair { 271 | assert_eq!(e.kind(), ErrorKind::CodecFailure); 272 | } 273 | } 274 | 275 | #[test] 276 | fn public_key_round_trip() { 277 | let src_pk = "XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA"; 278 | let account = XKey::from_public_key(src_pk).unwrap(); 279 | let pk = account.public_key(); 280 | assert_eq!(pk, src_pk); 281 | } 282 | 283 | #[test] 284 | fn has_proper_prefix() { 285 | let module = XKey::new(); 286 | assert!(module.seed().unwrap().starts_with("SX")); 287 | assert!(module.public_key().starts_with('X')); 288 | } 289 | 290 | #[test] 291 | fn xkeys_convert_to_public() { 292 | let sender_pub = 293 | XKey::from_public_key("XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA") 294 | .unwrap(); 295 | let sender = 296 | XKey::from_seed("SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZM").unwrap(); 297 | 298 | assert_eq!(sender.public_key(), sender_pub.public_key()); 299 | } 300 | 301 | #[test] 302 | fn seal_and_open() { 303 | let sender = XKey::new(); 304 | let receiver = XKey::new(); 305 | 306 | let boxed = sender.seal(MESSAGE, &receiver).unwrap(); 307 | 308 | let res = receiver.open(&boxed, &sender).unwrap(); 309 | assert_eq!(MESSAGE, res.as_slice()); 310 | } 311 | 312 | #[test] 313 | fn tamper_version() { 314 | let sender = XKey::new(); 315 | let receiver = XKey::new(); 316 | 317 | let mut boxed = sender.seal(MESSAGE, &receiver).unwrap(); 318 | 319 | // Tamper with message 320 | boxed[0] += 1; 321 | 322 | let err = receiver.open(&boxed, &sender).unwrap_err(); 323 | assert_eq!(err.kind(), ErrorKind::InvalidPrefix); 324 | } 325 | 326 | #[test] 327 | fn tamper_message() { 328 | let sender = XKey::new(); 329 | let receiver = XKey::new(); 330 | 331 | let mut boxed = sender.seal(MESSAGE, &receiver).unwrap(); 332 | 333 | // Tamper with message 334 | boxed[XKEY_VERSION_V1.len() + 1] += 1; 335 | 336 | let err = receiver.open(&boxed, &sender).unwrap_err(); 337 | assert_eq!(err.kind(), ErrorKind::InvalidPayload); 338 | } 339 | 340 | #[test] 341 | fn wrong_key() { 342 | let sender = XKey::new(); 343 | let receiver = XKey::new(); 344 | let random_key = XKey::new(); 345 | 346 | let boxed = sender.seal(MESSAGE, &receiver).unwrap(); 347 | 348 | let err = random_key.open(&boxed, &sender).unwrap_err(); 349 | assert_eq!(err.kind(), ErrorKind::InvalidPayload); 350 | } 351 | 352 | #[test] 353 | fn open_from_go() { 354 | let receiver = 355 | XKey::from_seed("SXAHGC56LJFTSRXFC653AT7XZU6WGYIXU4XFPMCT62GGHFLUCSPVYP764M").unwrap(); 356 | let sender = 357 | XKey::from_public_key("XBUJMZHVOPQ2SK5VD3TY4VNBPVU2YFGRLK6EFPEPSMVDUYEBSROWZCEA") 358 | .unwrap(); 359 | let raw_sender = 360 | XKey::from_seed("SXAKIYZX2POLIHZ5W5YZEWVTH24NLEUETBW3TKIVYRSS3GNHFXO5D4JJZM").unwrap(); 361 | assert_eq!(sender.public_key(), raw_sender.public_key()); 362 | 363 | // Message generated with nkeys Go library 364 | let boxed = [ 365 | 0x78, 0x6b, 0x76, 0x31, 0x46, 0x76, 0x98, 0xf9, 0x87, 0x3, 0x50, 0x2f, 0x42, 0x41, 366 | 0xb7, 0xa7, 0x34, 0x72, 0x98, 0x0, 0x92, 0x9f, 0x6d, 0x9, 0x4b, 0x6, 0xc6, 0xe3, 0x4a, 367 | 0x78, 0xde, 0x49, 0x9e, 0xe7, 0xde, 0xbb, 0xac, 0x94, 0x77, 0x55, 0x6f, 0x3f, 0xbb, 368 | 0xe9, 0xf, 0xfd, 0x67, 0x8b, 0xc6, 0x29, 0xe5, 0xb7, 0xcc, 0x7c, 0x57, 0x40, 0x4d, 369 | 0x92, 0x38, 0x46, 0xcf, 0x1, 0x2, 0x26, 370 | ]; 371 | 372 | let out = receiver.open(&boxed, &raw_sender).unwrap(); 373 | assert_eq!(std::str::from_utf8(&out), Ok("this is super secret")); 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # nkeys 2 | //! 3 | //! The `nkeys` is a Rust port of the official NATS [Go](https://github.com/nats-io/nkeys) nkeys implementation. 4 | //! 5 | //! Nkeys provides library functions to create ed25519 keys using the special prefix encoding system used by 6 | //! NATS 2.0+ security. 7 | //! 8 | //! # Examples 9 | //! ``` 10 | //! use nkeys::KeyPair; 11 | //! 12 | //! // Create a user key pair 13 | //! let user = KeyPair::new_user(); 14 | //! 15 | //! // Sign some data with the user's full key pair 16 | //! let msg = "this is super secret".as_bytes(); 17 | //! let sig = user.sign(&msg).unwrap(); 18 | //! let res = user.verify(msg, sig.as_slice()); 19 | //! assert!(res.is_ok()); 20 | //! 21 | //! // Access the encoded seed (the information that needs to be kept safe/secret) 22 | //! let seed = user.seed().unwrap(); 23 | //! // Access the public key, which can be safely shared 24 | //! let pk = user.public_key(); 25 | //! 26 | //! // Create a full User who can sign and verify from a private seed. 27 | //! let user = KeyPair::from_seed(&seed); 28 | //! 29 | //! // Create a user that can only verify and not sign 30 | //! let user = KeyPair::from_public_key(&pk).unwrap(); 31 | //! assert!(user.seed().is_err()); 32 | //! ``` 33 | //! 34 | //! # Notes 35 | //! The following is a list of the valid prefixes / key pair types available. Note that there are more 36 | //! key pair types available in this crate than there are in the original Go implementation for NATS. 37 | //! * **N** - Server 38 | //! * **C** - Cluster 39 | //! * **O** - Operator 40 | //! * **A** - Account 41 | //! * **U** - User 42 | //! * **M** - Module 43 | //! * **V** - Service / Service Provider 44 | //! * **P** - Private Key 45 | //! * **X** - Curve Key (X25519) 46 | 47 | #![allow(dead_code)] 48 | 49 | use std::fmt::{self, Debug}; 50 | 51 | use crc::{extract_crc, push_crc, valid_checksum}; 52 | use ed25519_dalek::{SecretKey, Signer, SigningKey, Verifier, VerifyingKey}; 53 | use rand::prelude::*; 54 | 55 | #[cfg(feature = "xkeys")] 56 | mod xkeys; 57 | 58 | #[cfg(feature = "xkeys")] 59 | pub use xkeys::XKey; 60 | 61 | const ENCODED_SEED_LENGTH: usize = 58; 62 | const ENCODED_PUBKEY_LENGTH: usize = 56; 63 | 64 | const PREFIX_BYTE_SEED: u8 = 18 << 3; 65 | const PREFIX_BYTE_PRIVATE: u8 = 15 << 3; 66 | const PREFIX_BYTE_SERVER: u8 = 13 << 3; 67 | const PREFIX_BYTE_CLUSTER: u8 = 2 << 3; 68 | const PREFIX_BYTE_OPERATOR: u8 = 14 << 3; 69 | const PREFIX_BYTE_MODULE: u8 = 12 << 3; 70 | const PREFIX_BYTE_ACCOUNT: u8 = 0; 71 | const PREFIX_BYTE_USER: u8 = 20 << 3; 72 | const PREFIX_BYTE_SERVICE: u8 = 21 << 3; 73 | const PREFIX_BYTE_CURVE: u8 = 23 << 3; 74 | const PREFIX_BYTE_UNKNOWN: u8 = 25 << 3; 75 | 76 | const PUBLIC_KEY_PREFIXES: [u8; 8] = [ 77 | PREFIX_BYTE_ACCOUNT, 78 | PREFIX_BYTE_CLUSTER, 79 | PREFIX_BYTE_OPERATOR, 80 | PREFIX_BYTE_SERVER, 81 | PREFIX_BYTE_USER, 82 | PREFIX_BYTE_MODULE, 83 | PREFIX_BYTE_SERVICE, 84 | PREFIX_BYTE_CURVE, 85 | ]; 86 | 87 | type Result = std::result::Result; 88 | 89 | /// The main interface used for reading and writing _nkey-encoded_ key pairs, including 90 | /// seeds and public keys. 91 | #[derive(Clone)] 92 | pub struct KeyPair { 93 | kp_type: KeyPairType, 94 | sk: Option, //rawkey_kind: RawKeyKind, 95 | signing_key: Option, 96 | pk: VerifyingKey, 97 | } 98 | 99 | impl Debug for KeyPair { 100 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 101 | write!(f, "KeyPair ({:?})", self.kp_type) 102 | } 103 | } 104 | 105 | /// The authoritative list of valid key pair types that are used for cryptographically secure 106 | /// identities 107 | #[derive(Debug, Clone, PartialEq)] 108 | pub enum KeyPairType { 109 | /// A server identity 110 | Server, 111 | /// A cluster (group of servers) identity 112 | Cluster, 113 | /// An operator (vouches for accounts) identity 114 | Operator, 115 | /// An account (vouches for users) identity 116 | Account, 117 | /// A user identity 118 | User, 119 | /// A module identity - can represent an opaque component, etc. 120 | Module, 121 | /// A service / service provider identity 122 | Service, 123 | /// CurveKeys (X25519) 124 | Curve, 125 | } 126 | 127 | impl std::str::FromStr for KeyPairType { 128 | type Err = crate::error::Error; 129 | 130 | fn from_str(s: &str) -> ::std::result::Result { 131 | let tgt = s.to_uppercase(); 132 | 133 | match tgt.as_ref() { 134 | "SERVER" => Ok(KeyPairType::Server), 135 | "CLUSTER" => Ok(KeyPairType::Cluster), 136 | "OPERATOR" => Ok(KeyPairType::Operator), 137 | "ACCOUNT" => Ok(KeyPairType::Account), 138 | "USER" => Ok(KeyPairType::User), 139 | "SERVICE" => Ok(KeyPairType::Service), 140 | "MODULE" => Ok(KeyPairType::Module), 141 | "CURVE" => Ok(KeyPairType::Curve), 142 | _ => Ok(KeyPairType::Module), // Do not crash the app if user input was wrong 143 | } 144 | } 145 | } 146 | 147 | impl From for KeyPairType { 148 | fn from(prefix_byte: u8) -> KeyPairType { 149 | match prefix_byte { 150 | PREFIX_BYTE_SERVER => KeyPairType::Server, 151 | PREFIX_BYTE_CLUSTER => KeyPairType::Cluster, 152 | PREFIX_BYTE_OPERATOR => KeyPairType::Operator, 153 | PREFIX_BYTE_ACCOUNT => KeyPairType::Account, 154 | PREFIX_BYTE_USER => KeyPairType::User, 155 | PREFIX_BYTE_MODULE => KeyPairType::Module, 156 | PREFIX_BYTE_SERVICE => KeyPairType::Service, 157 | PREFIX_BYTE_CURVE => KeyPairType::Curve, 158 | _ => KeyPairType::Operator, 159 | } 160 | } 161 | } 162 | 163 | impl KeyPair { 164 | /// Creates a new key pair of the given type. 165 | /// 166 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 167 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 168 | #[cfg(not(target_arch = "wasm32"))] 169 | pub fn new(kp_type: KeyPairType) -> KeyPair { 170 | // If this unwrap fails, then the library is invalid, so the unwrap is OK here 171 | Self::new_from_raw(kp_type, generate_seed_rand()).unwrap() 172 | } 173 | 174 | /// Create a new keypair using a pre-existing set of random bytes. 175 | /// 176 | /// Returns an error if there is an issue using the bytes to generate the key 177 | /// NOTE: These bytes should be generated from a cryptographically secure random source. 178 | pub fn new_from_raw(kp_type: KeyPairType, random_bytes: [u8; 32]) -> Result { 179 | let signing_key = SigningKey::from_bytes(&random_bytes); 180 | Ok(KeyPair { 181 | kp_type, 182 | pk: signing_key.verifying_key(), 183 | signing_key: Some(signing_key), 184 | sk: Some(random_bytes), 185 | }) 186 | } 187 | 188 | /// Creates a new user key pair with a seed that has a **U** prefix 189 | /// 190 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 191 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 192 | #[cfg(not(target_arch = "wasm32"))] 193 | pub fn new_user() -> KeyPair { 194 | Self::new(KeyPairType::User) 195 | } 196 | 197 | /// Creates a new account key pair with a seed that has an **A** prefix 198 | /// 199 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 200 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 201 | #[cfg(not(target_arch = "wasm32"))] 202 | pub fn new_account() -> KeyPair { 203 | Self::new(KeyPairType::Account) 204 | } 205 | 206 | /// Creates a new operator key pair with a seed that has an **O** prefix 207 | /// 208 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 209 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 210 | #[cfg(not(target_arch = "wasm32"))] 211 | pub fn new_operator() -> KeyPair { 212 | Self::new(KeyPairType::Operator) 213 | } 214 | 215 | /// Creates a new cluster key pair with a seed that has the **C** prefix 216 | /// 217 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 218 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 219 | #[cfg(not(target_arch = "wasm32"))] 220 | pub fn new_cluster() -> KeyPair { 221 | Self::new(KeyPairType::Cluster) 222 | } 223 | 224 | /// Creates a new server key pair with a seed that has the **N** prefix 225 | /// 226 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 227 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 228 | #[cfg(not(target_arch = "wasm32"))] 229 | pub fn new_server() -> KeyPair { 230 | Self::new(KeyPairType::Server) 231 | } 232 | 233 | /// Creates a new module (e.g. WebAssembly) key pair with a seed that has the **M** prefix 234 | /// 235 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 236 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 237 | #[cfg(not(target_arch = "wasm32"))] 238 | pub fn new_module() -> KeyPair { 239 | Self::new(KeyPairType::Module) 240 | } 241 | 242 | /// Creates a new service / service provider key pair with a seed that has the **V** prefix 243 | /// 244 | /// NOTE: This is not available if using on a wasm32-unknown-unknown target due to the lack of 245 | /// rand support. Use [`new_from_raw`](KeyPair::new_from_raw) instead 246 | #[cfg(not(target_arch = "wasm32"))] 247 | pub fn new_service() -> KeyPair { 248 | Self::new(KeyPairType::Service) 249 | } 250 | 251 | /// Returns the encoded, human-readable public key of this key pair 252 | pub fn public_key(&self) -> String { 253 | encode(&self.kp_type, self.pk.as_bytes()) 254 | } 255 | 256 | /// Attempts to sign the given input with the key pair's seed 257 | pub fn sign(&self, input: &[u8]) -> Result> { 258 | if let Some(ref seed) = self.signing_key { 259 | let sig = seed.sign(input); 260 | Ok(sig.to_bytes().to_vec()) 261 | } else { 262 | Err(err!(SignatureError, "Cannot sign without a seed key")) 263 | } 264 | } 265 | 266 | /// Attempts to verify that the given signature is valid for the given input 267 | pub fn verify(&self, input: &[u8], sig: &[u8]) -> Result<()> { 268 | if sig.len() != ed25519::Signature::BYTE_SIZE { 269 | return Err(err!( 270 | InvalidSignatureLength, 271 | "Signature did not match expected length" 272 | )); 273 | } 274 | 275 | let mut fixedsig = [0; ed25519::Signature::BYTE_SIZE]; 276 | fixedsig.copy_from_slice(sig); 277 | let insig = ed25519::Signature::from_bytes(&fixedsig); 278 | 279 | match self.pk.verify(input, &insig) { 280 | Ok(()) => Ok(()), 281 | Err(e) => Err(e.into()), 282 | } 283 | } 284 | 285 | /// Attempts to return the encoded, human-readable string for this key pair's seed. 286 | /// Remember that this value should be treated as a secret. Do not store it for 287 | /// any longer than necessary 288 | pub fn seed(&self) -> Result { 289 | if let Some(ref seed) = self.sk { 290 | Ok(encode_seed(&self.kp_type, seed)) 291 | } else { 292 | Err(err!(IncorrectKeyType, "This keypair has no seed")) 293 | } 294 | } 295 | 296 | /// Attempts to produce a public-only key pair from the given encoded public key string 297 | pub fn from_public_key(source: &str) -> Result { 298 | let (prefix, bytes) = from_public_key(source)?; 299 | 300 | let pk = VerifyingKey::from_bytes(&bytes) 301 | .map_err(|_| err!(VerifyError, "Could not read public key"))?; 302 | 303 | Ok(KeyPair { 304 | kp_type: KeyPairType::from(prefix), 305 | pk, 306 | sk: None, 307 | signing_key: None, 308 | }) 309 | } 310 | 311 | /// Attempts to produce a full key pair from the given encoded seed string 312 | pub fn from_seed(source: &str) -> Result { 313 | let (ty, seed) = decode_seed(source)?; 314 | 315 | let signing_key = SigningKey::from_bytes(&seed); 316 | 317 | Ok(KeyPair { 318 | kp_type: KeyPairType::from(ty), 319 | pk: signing_key.verifying_key(), 320 | sk: Some(seed), 321 | signing_key: Some(signing_key), 322 | }) 323 | } 324 | 325 | /// Returns the type of this key pair. 326 | pub fn key_pair_type(&self) -> KeyPairType { 327 | self.kp_type.clone() 328 | } 329 | } 330 | 331 | fn decode_raw(raw: &[u8]) -> Result> { 332 | let mut b32_decoded = data_encoding::BASE32_NOPAD.decode(raw)?; 333 | 334 | let checksum = extract_crc(&mut b32_decoded)?; 335 | let v_checksum = valid_checksum(&b32_decoded, checksum); 336 | if !v_checksum { 337 | Err(err!(ChecksumFailure, "Checksum mismatch")) 338 | } else { 339 | Ok(b32_decoded) 340 | } 341 | } 342 | 343 | /// Returns the prefix byte and the underlying public key bytes 344 | /// NOTE: This is considered an advanced use case, it's generally recommended to stick with [`KeyPair::from_public_key`] instead. 345 | pub fn from_public_key(source: &str) -> Result<(u8, [u8; 32])> { 346 | if source.len() != ENCODED_PUBKEY_LENGTH { 347 | let l = source.len(); 348 | return Err(err!(InvalidKeyLength, "Bad key length: {}", l)); 349 | } 350 | 351 | let source_bytes = source.as_bytes(); 352 | let mut raw = decode_raw(source_bytes)?; 353 | 354 | let prefix = raw[0]; 355 | if !valid_public_key_prefix(prefix) { 356 | return Err(err!( 357 | InvalidPrefix, 358 | "Not a valid public key prefix: {}", 359 | raw[0] 360 | )); 361 | } 362 | raw.remove(0); 363 | 364 | let mut public_key = [0u8; 32]; 365 | public_key.copy_from_slice(&raw[..]); 366 | 367 | Ok((prefix, public_key)) 368 | } 369 | 370 | /// Attempts to decode the provided base32 encoded string into a valid prefix byte and the private key seed bytes. 371 | /// NOTE: This is considered an advanced use case, it's generally recommended to stick with [`KeyPair::from_seed`] instead. 372 | pub fn decode_seed(source: &str) -> Result<(u8, [u8; 32])> { 373 | if source.len() != ENCODED_SEED_LENGTH { 374 | let l = source.len(); 375 | return Err(err!(InvalidKeyLength, "Bad seed length: {}", l)); 376 | } 377 | 378 | let source_bytes = source.as_bytes(); 379 | let raw = decode_raw(source_bytes)?; 380 | 381 | let b1 = raw[0] & 248; 382 | if b1 != PREFIX_BYTE_SEED { 383 | return Err(err!( 384 | InvalidPrefix, 385 | "Incorrect byte prefix: {}", 386 | source.chars().next().unwrap() 387 | )); 388 | } 389 | 390 | let b2 = (raw[0] & 7) << 5 | ((raw[1] & 248) >> 3); 391 | 392 | let mut seed = [0u8; 32]; 393 | seed.copy_from_slice(&raw[2..]); 394 | 395 | Ok((b2, seed)) 396 | } 397 | 398 | fn generate_seed_rand() -> [u8; 32] { 399 | let mut rng = rand::thread_rng(); 400 | rng.gen::<[u8; 32]>() 401 | } 402 | 403 | fn get_prefix_byte(kp_type: &KeyPairType) -> u8 { 404 | match kp_type { 405 | KeyPairType::Server => PREFIX_BYTE_SERVER, 406 | KeyPairType::Account => PREFIX_BYTE_ACCOUNT, 407 | KeyPairType::Cluster => PREFIX_BYTE_CLUSTER, 408 | KeyPairType::Operator => PREFIX_BYTE_OPERATOR, 409 | KeyPairType::User => PREFIX_BYTE_USER, 410 | KeyPairType::Module => PREFIX_BYTE_MODULE, 411 | KeyPairType::Service => PREFIX_BYTE_SERVICE, 412 | KeyPairType::Curve => PREFIX_BYTE_CURVE, 413 | } 414 | } 415 | 416 | fn valid_public_key_prefix(prefix: u8) -> bool { 417 | PUBLIC_KEY_PREFIXES.to_vec().contains(&prefix) 418 | } 419 | 420 | fn encode_seed(ty: &KeyPairType, seed: &[u8]) -> String { 421 | let prefix_byte = get_prefix_byte(ty); 422 | 423 | let b1 = PREFIX_BYTE_SEED | prefix_byte >> 5; 424 | let b2 = (prefix_byte & 31) << 3; 425 | 426 | encode_prefix(&[b1, b2], seed) 427 | } 428 | 429 | fn encode(ty: &KeyPairType, key: &[u8]) -> String { 430 | let prefix_byte = get_prefix_byte(ty); 431 | encode_prefix(&[prefix_byte], key) 432 | } 433 | 434 | fn encode_prefix(prefix: &[u8], key: &[u8]) -> String { 435 | let mut raw = Vec::with_capacity(prefix.len() + key.len() + 2); 436 | raw.extend_from_slice(prefix); 437 | raw.extend_from_slice(key); 438 | push_crc(&mut raw); 439 | 440 | data_encoding::BASE32_NOPAD.encode(&raw[..]) 441 | } 442 | 443 | #[cfg(test)] 444 | mod tests { 445 | use super::*; 446 | use crate::error::ErrorKind; 447 | 448 | #[test] 449 | fn validate_decode_seed() { 450 | let input_bytes = generate_seed_rand(); 451 | let seed = encode_seed(&KeyPairType::User, input_bytes.as_slice()); 452 | 453 | let (prefix, decoded_bytes) = decode_seed(&seed).unwrap(); 454 | 455 | assert_eq!(prefix, PREFIX_BYTE_USER); 456 | assert_eq!(decoded_bytes, input_bytes); 457 | } 458 | 459 | #[test] 460 | fn validate_from_public_key() { 461 | let input_bytes = generate_seed_rand(); 462 | let public_key = encode(&KeyPairType::User, input_bytes.as_slice()); 463 | 464 | let (prefix, decoded_bytes) = from_public_key(&public_key).unwrap(); 465 | 466 | assert_eq!(prefix, PREFIX_BYTE_USER); 467 | assert_eq!(decoded_bytes, input_bytes); 468 | } 469 | 470 | #[test] 471 | fn seed_encode_decode_round_trip() { 472 | let pair = KeyPair::new_user(); 473 | let s = pair.seed().unwrap(); 474 | let p = pair.public_key(); 475 | 476 | let pair2 = KeyPair::from_seed(s.as_str()).unwrap(); 477 | let s2 = pair2.seed().unwrap(); 478 | 479 | assert_eq!(s, s2); 480 | assert_eq!(p, pair2.public_key()); 481 | } 482 | 483 | #[test] 484 | fn roundtrip_encoding_go_compat() { 485 | // Seed and Public Key pair generated by Go nkeys library 486 | let seed = "SAAPN4W3EG6KCJGUQTKTJ5GSB5NHK5CHAJL4DBGFUM3HHROI4XUEP4OBK4"; 487 | let pk = "ACODERUVFFAWZQDSS6SBIACUA5O6SXF7HJ3YTYXBALHZP3P7R4BUO4J2"; 488 | 489 | let pair = KeyPair::from_seed(seed).unwrap(); 490 | 491 | assert_eq!(pair.seed().unwrap(), seed); 492 | assert_eq!(pair.public_key(), pk); 493 | } 494 | 495 | #[test] 496 | fn from_seed_rejects_bad_prefix() { 497 | let seed = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; 498 | let pair = KeyPair::from_seed(seed); 499 | assert!(pair.is_err()); 500 | if let Err(e) = pair { 501 | assert_eq!(e.kind(), ErrorKind::InvalidPrefix); 502 | } 503 | } 504 | 505 | #[test] 506 | fn from_seed_rejects_bad_checksum() { 507 | let seed = "FAAPN4W3EG6KCJGUQTKTJ5GSB5NHK5CHAJL4DBGFUM3HHROI4XUEP4OBK4"; 508 | let pair = KeyPair::from_seed(seed); 509 | assert!(pair.is_err()); 510 | if let Err(e) = pair { 511 | assert_eq!(e.kind(), ErrorKind::ChecksumFailure); 512 | } 513 | } 514 | 515 | #[test] 516 | fn from_seed_rejects_bad_length() { 517 | let seed = "SAAPN4W3EG6KCJGUQTKTJ5GSB5NHK5CHAJL4DBGFUM3SAAPN4W3EG6KCJGUQTKTJ5GSB5NHK5"; 518 | let pair = KeyPair::from_seed(seed); 519 | assert!(pair.is_err()); 520 | if let Err(e) = pair { 521 | assert_eq!(e.kind(), ErrorKind::InvalidKeyLength); 522 | } 523 | } 524 | 525 | #[test] 526 | fn from_seed_rejects_invalid_encoding() { 527 | let badseed = "SAAPN4W3EG6KCJGUQTKTJ5!#B5NHK5CHAJL4DBGFUM3HHROI4XUEP4OBK4"; 528 | let pair = KeyPair::from_seed(badseed); 529 | assert!(pair.is_err()); 530 | if let Err(e) = pair { 531 | assert_eq!(e.kind(), ErrorKind::CodecFailure); 532 | } 533 | } 534 | 535 | #[test] 536 | fn sign_and_verify() { 537 | let user = KeyPair::new_user(); 538 | let msg = b"this is super secret"; 539 | 540 | let sig = user.sign(msg).unwrap(); 541 | 542 | let res = user.verify(msg, sig.as_slice()); 543 | assert!(res.is_ok()); 544 | } 545 | 546 | #[test] 547 | fn sign_and_verify_rejects_mismatched_sig() { 548 | let user = KeyPair::new_user(); 549 | let msg = b"this is super secret"; 550 | 551 | let sig = user.sign(msg).unwrap(); 552 | let res = user.verify(b"this doesn't match the message", sig.as_slice()); 553 | assert!(res.is_err()); 554 | } 555 | 556 | #[test] 557 | fn sign_and_verify_rejects_invalid_signature_length() { 558 | let kp = KeyPair::new_user(); 559 | let res = kp.verify(&[], &[]); 560 | assert!(res.is_err()); 561 | if let Err(e) = res { 562 | assert_eq!(e.kind(), ErrorKind::InvalidSignatureLength); 563 | } 564 | } 565 | 566 | #[test] 567 | fn from_public_key_rejects_bad_length() { 568 | let public_key = "ACARVGW77LDNWYXBAH62YKKQRVHYOTKKDDVVJVOISOU75WQPXOO7N3"; 569 | let pair = KeyPair::from_public_key(public_key); 570 | assert!(pair.is_err()); 571 | if let Err(e) = pair { 572 | assert_eq!(e.kind(), ErrorKind::InvalidKeyLength); 573 | } 574 | } 575 | 576 | #[test] 577 | fn from_public_key_rejects_bad_prefix() { 578 | let public_key = "ZCO4XYNKEN7ZFQ42BHYCBYI3K7USOGG43C2DIJZYWSQ2YEMBOZWN6PYH"; 579 | let pair = KeyPair::from_public_key(public_key); 580 | assert!(pair.is_err()); 581 | if let Err(e) = pair { 582 | assert_eq!(e.kind(), ErrorKind::InvalidPrefix); 583 | } 584 | } 585 | 586 | #[test] 587 | fn public_key_round_trip() { 588 | let account = 589 | KeyPair::from_public_key("ACODERUVFFAWZQDSS6SBIACUA5O6SXF7HJ3YTYXBALHZP3P7R4BUO4J2") 590 | .unwrap(); 591 | let pk = account.public_key(); 592 | assert_eq!( 593 | pk, 594 | "ACODERUVFFAWZQDSS6SBIACUA5O6SXF7HJ3YTYXBALHZP3P7R4BUO4J2" 595 | ); 596 | } 597 | 598 | #[test] 599 | fn module_has_proper_prefix() { 600 | let module = KeyPair::new_module(); 601 | assert!(module.seed().unwrap().starts_with("SM")); 602 | assert!(module.public_key().starts_with('M')); 603 | } 604 | 605 | #[test] 606 | fn service_has_proper_prefix() { 607 | let service = KeyPair::new_service(); 608 | assert!(service.seed().unwrap().starts_with("SV")); 609 | assert!(service.public_key().starts_with('V')); 610 | } 611 | 612 | #[test] 613 | fn can_get_key_type() { 614 | let from_pub = 615 | KeyPair::from_public_key("UBCXCMGAZQZN55X5TTTWMB5CZNZIKJHEDZJOJ3TV63NKPJ6FRXSR2ZO4") 616 | .unwrap(); 617 | let from_seed = 618 | KeyPair::from_seed("SCANU5JGFEPJ2XNFQ6YMDRHMNFAL6ZT3DCU3ZMMHHML7GLFE3YIH5TBM6E") 619 | .unwrap(); 620 | 621 | assert!( 622 | matches!(from_pub.key_pair_type(), KeyPairType::User), 623 | "Expected the key type to be {:?}, found {:?}", 624 | KeyPairType::User, 625 | from_pub.key_pair_type() 626 | ); 627 | assert!( 628 | matches!(from_seed.key_pair_type(), KeyPairType::Cluster), 629 | "Expected the key type to be {:?}, found {:?}", 630 | KeyPairType::Cluster, 631 | from_seed.key_pair_type() 632 | ); 633 | } 634 | } 635 | 636 | mod crc; 637 | pub mod error; 638 | --------------------------------------------------------------------------------