Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: replace Luhn checksum with DammSum #4639

Merged
merged 5 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions applications/tari_app_utilities/src/utilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ pub fn setup_runtime() -> Result<Runtime, ExitError> {

/// Returns a CommsPublicKey from either a emoji id or a public key
pub fn parse_emoji_id_or_public_key(key: &str) -> Option<CommsPublicKey> {
EmojiId::str_to_pubkey(&key.trim().replace('|', ""))
EmojiId::from_emoji_string(&key.trim().replace('|', ""))
.map(|emoji_id| emoji_id.to_public_key())
.or_else(|_| CommsPublicKey::from_hex(key))
.ok()
}
Expand Down Expand Up @@ -79,8 +80,8 @@ impl FromStr for UniPublicKey {
type Err = UniIdError;

fn from_str(key: &str) -> Result<Self, Self::Err> {
if let Ok(public_key) = EmojiId::str_to_pubkey(&key.trim().replace('|', "")) {
Ok(Self(public_key))
if let Ok(emoji_id) = EmojiId::from_emoji_string(&key.trim().replace('|', "")) {
Ok(Self(emoji_id.to_public_key()))
} else if let Ok(public_key) = PublicKey::from_hex(key) {
Ok(Self(public_key))
} else {
Expand Down Expand Up @@ -113,8 +114,8 @@ impl FromStr for UniNodeId {
type Err = UniIdError;

fn from_str(key: &str) -> Result<Self, Self::Err> {
if let Ok(public_key) = EmojiId::str_to_pubkey(&key.trim().replace('|', "")) {
Ok(Self::PublicKey(public_key))
if let Ok(emoji_id) = EmojiId::from_emoji_string(&key.trim().replace('|', "")) {
Ok(Self::PublicKey(emoji_id.to_public_key()))
} else if let Ok(public_key) = PublicKey::from_hex(key) {
Ok(Self::PublicKey(public_key))
} else if let Ok(node_id) = NodeId::from_hex(key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ impl CommandContext {
}
};

let eid = EmojiId::from_pubkey(&peer.public_key);
let eid = EmojiId::from_public_key(&peer.public_key).to_emoji_string();
println!("Emoji ID: {}", eid);
println!("Public Key: {}", peer.public_key);
println!("NodeId: {}", peer.node_id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ pub async fn command_runner(
},
Whois(args) => {
let public_key = args.public_key.into();
let emoji_id = EmojiId::from_pubkey(&public_key);
let emoji_id = EmojiId::from_public_key(&public_key).to_emoji_string();

println!("Public Key: {}", public_key.to_hex());
println!("Emoji ID : {}", emoji_id);
Expand Down
24 changes: 16 additions & 8 deletions applications/tari_console_wallet/src/ui/state/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ impl AppState {

let public_key = match CommsPublicKey::from_hex(public_key_or_emoji_id.as_str()) {
Ok(pk) => pk,
Err(_) => {
EmojiId::str_to_pubkey(public_key_or_emoji_id.as_str()).map_err(|_| UiError::PublicKeyParseError)?
},
Err(_) => EmojiId::from_emoji_string(public_key_or_emoji_id.as_str())
.map_err(|_| UiError::PublicKeyParseError)?
.to_public_key(),
};

let contact = Contact::new(alias, public_key, None, None);
Expand Down Expand Up @@ -250,7 +250,9 @@ impl AppState {
let mut inner = self.inner.write().await;
let public_key = match CommsPublicKey::from_hex(public_key.as_str()) {
Ok(pk) => pk,
Err(_) => EmojiId::str_to_pubkey(public_key.as_str()).map_err(|_| UiError::PublicKeyParseError)?,
Err(_) => EmojiId::from_emoji_string(public_key.as_str())
.map_err(|_| UiError::PublicKeyParseError)?
.to_public_key(),
};

inner.wallet.contacts_service.remove_contact(public_key).await?;
Expand All @@ -273,7 +275,9 @@ impl AppState {
let inner = self.inner.write().await;
let public_key = match CommsPublicKey::from_hex(public_key.as_str()) {
Ok(pk) => pk,
Err(_) => EmojiId::str_to_pubkey(public_key.as_str()).map_err(|_| UiError::PublicKeyParseError)?,
Err(_) => EmojiId::from_emoji_string(public_key.as_str())
.map_err(|_| UiError::PublicKeyParseError)?
.to_public_key(),
};

let output_features = OutputFeatures { ..Default::default() };
Expand Down Expand Up @@ -306,7 +310,9 @@ impl AppState {
let inner = self.inner.write().await;
let public_key = match CommsPublicKey::from_hex(public_key.as_str()) {
Ok(pk) => pk,
Err(_) => EmojiId::str_to_pubkey(public_key.as_str()).map_err(|_| UiError::PublicKeyParseError)?,
Err(_) => EmojiId::from_emoji_string(public_key.as_str())
.map_err(|_| UiError::PublicKeyParseError)?
.to_public_key(),
};

let output_features = OutputFeatures { ..Default::default() };
Expand Down Expand Up @@ -339,7 +345,9 @@ impl AppState {
let inner = self.inner.write().await;
let dest_pubkey = match CommsPublicKey::from_hex(dest_pubkey.as_str()) {
Ok(pk) => pk,
Err(_) => EmojiId::str_to_pubkey(dest_pubkey.as_str()).map_err(|_| UiError::PublicKeyParseError)?,
Err(_) => EmojiId::from_emoji_string(dest_pubkey.as_str())
.map_err(|_| UiError::PublicKeyParseError)?
.to_public_key(),
};

let output_features = OutputFeatures { ..Default::default() };
Expand Down Expand Up @@ -1087,7 +1095,7 @@ impl AppStateData {
base_node_selected: Peer,
base_node_config: PeerConfig,
) -> Self {
let eid = EmojiId::from_pubkey(node_identity.public_key()).to_string();
let eid = EmojiId::from_public_key(node_identity.public_key()).to_emoji_string();
let qr_link = format!("tari://{}/pubkey/{}", network, &node_identity.public_key().to_hex());
let code = QrCode::new(qr_link).unwrap();
let image = code
Expand Down
2 changes: 1 addition & 1 deletion applications/tari_console_wallet/src/ui/ui_contact.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl From<Contact> for UiContact {
Self {
alias: c.alias,
public_key: c.public_key.to_string(),
emoji_id: EmojiId::from_pubkey(&c.public_key).as_str().to_string(),
emoji_id: EmojiId::from_public_key(&c.public_key).to_emoji_string(),
last_seen: match c.last_seen {
Some(val) => DateTime::<Local>::from_utc(val, Local::now().offset().to_owned())
.format("%m-%dT%H:%M")
Expand Down
210 changes: 210 additions & 0 deletions base_layer/common_types/src/dammsum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
// Copyright 2020. The Tari Project
//
// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
// following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
// disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
// following disclaimer in the documentation and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
// products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use thiserror::Error;

/// Calculates a checksum using the [DammSum](https://github.com/cypherstack/dammsum) algorithm.
///
/// This approach uses a dictionary whose size must be `2^k` for some `k > 0`.
/// The algorithm accepts an array of arbitrary size, each of whose elements are integers in the range `[0, 2^k)`.
/// The checksum is a single element also within this range.
/// DammSum detects all single transpositions and substitutions.
///
/// Note that for this implementation, we add the additional restriction that `k == 8`.
/// This is only because DammSum requires us to provide the coefficients for a certain type of polynomial, and
/// because it's unlikely for the alphabet size to change for this use case.
/// See the linked repository for more information, or if you need a different dictionary size.

#[derive(Debug, Error, PartialEq)]
pub enum ChecksumError {
#[error("Input data is too short")]
InputDataTooShort,
#[error("Invalid checksum")]
InvalidChecksum,
}

// Fixed for a dictionary size of `2^8 == 256`
const COEFFICIENTS: [u8; 3] = [4, 3, 1];

/// Compute the DammSum checksum for an array, each of whose elements are in the range `[0, 2^8)`
pub fn compute_checksum(data: &Vec<u8>) -> u8 {
let mut mask = 1u8;

// Compute the bitmask (if possible)
for bit in COEFFICIENTS {
mask += 1u8 << bit;
}

// Perform the Damm algorithm
let mut result = 0u8;

for digit in data {
result ^= *digit; // add
let overflow = (result & (1 << 7)) != 0;
result <<= 1; // double
if overflow {
// reduce
result ^= mask;
}
}

result
}

/// Determine whether the array ends with a valid checksum
pub fn validate_checksum(data: &Vec<u8>) -> Result<(), ChecksumError> {
// Empty data is not allowed, nor data only consisting of a checksum
if data.len() < 2 {
return Err(ChecksumError::InputDataTooShort);
}

// It's sufficient to check the entire array against a zero checksum
match compute_checksum(data) {
0u8 => Ok(()),
_ => Err(ChecksumError::InvalidChecksum),
}
}

#[cfg(test)]
mod test {
use rand::Rng;

use crate::dammsum::{compute_checksum, validate_checksum, ChecksumError};

#[test]
/// Check that valid checksums validate
fn checksum_validate() {
const SIZE: usize = 33;

// Generate random data
let mut rng = rand::thread_rng();
let mut data: Vec<u8> = (0..SIZE).map(|_| rng.gen::<u8>()).collect();

// Compute and append the checksum
data.push(compute_checksum(&data));

// Validate
assert!(validate_checksum(&data).is_ok());
}

#[test]
/// Sanity check against memory-specific checksums
fn identical_checksum() {
const SIZE: usize = 33;

// Generate identical random data
let mut rng = rand::thread_rng();
let data_0: Vec<u8> = (0..SIZE).map(|_| rng.gen::<u8>()).collect();
let data_1 = data_0.clone();

// Compute the checksums
let check_0 = compute_checksum(&data_0);
let check_1 = compute_checksum(&data_1);

// They should be equal
assert_eq!(check_0, check_1);
}

#[test]
/// Sanity check for known distinct checksums
fn distinct_checksum() {
// Fix two inputs that must have a unique checksum
let data_0 = vec![0u8];
let data_1 = vec![1u8];

// Compute the checksums
let check_0 = compute_checksum(&data_0);
let check_1 = compute_checksum(&data_1);

// They should be distinct
assert!(check_0 != check_1);
}

#[test]
/// Test validation failure modes
fn failure_modes_validate() {
// Empty input data
let mut data: Vec<u8> = vec![];
assert_eq!(validate_checksum(&data), Err(ChecksumError::InputDataTooShort));

// Input data is only a checksum
data = vec![0u8];
assert_eq!(validate_checksum(&data), Err(ChecksumError::InputDataTooShort));
}

#[test]
/// Check that all single subtitutions are detected
fn substitutions() {
const SIZE: usize = 33;

// Generate random data
let mut rng = rand::thread_rng();
let mut data: Vec<u8> = (0..SIZE).map(|_| rng.gen::<u8>()).collect();

// Compute the checksum
data.push(compute_checksum(&data));

// Validate
assert!(validate_checksum(&data).is_ok());

// Check all substitutions in all positions
for j in 0..data.len() {
let mut data_ = data.clone();
for i in 0..=u8::MAX {
if data[j] == i {
continue;
}
data_[j] = i;

assert_eq!(validate_checksum(&data_), Err(ChecksumError::InvalidChecksum));
}
}
}

#[test]
/// Check that all single transpositions are detected
fn transpositions() {
const SIZE: usize = 33;

// Generate random data
let mut rng = rand::thread_rng();
let mut data: Vec<u8> = (0..SIZE).map(|_| rng.gen::<u8>()).collect();

// Compute the checksum
data.push(compute_checksum(&data));

// Validate
assert!(validate_checksum(&data).is_ok());

// Check all transpositions
for j in 0..(data.len() - 1) {
if data[j] == data[j + 1] {
continue;
}

let mut data_ = data.clone();
data_.swap(j, j + 1);

assert_eq!(validate_checksum(&data_), Err(ChecksumError::InvalidChecksum));
}
}
}
Loading